rewrite redirect backend with go to do all redirect and geo data in one app

main
TZGyn 1 year ago
parent 01a515fbc7
commit 85662507be
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

@ -1,10 +1,6 @@
node_modules # flyctl launch added from .gitignore
npm-debug.log data/*
Dockerfile* !data/README.txt
docker-compose* dist/*
.dockerignore !dist/README.txt
.git fly.toml
.gitignore
README.md
LICENSE
.vscode

@ -1,8 +1,7 @@
FALLBACK_URL=https://app.kon.sh FALLBACK_URL=https://app.kon.sh
DATABASE_URL=postgres://postgres:password@0.0.0.0:5432/link-shortener DATABASE_URL=postgres://postgres:password@0.0.0.0:5432/link-shortener
APP_URL=kon.sh APP_URL=kon.sh
GEOIPAPI= # self host geolite2 api url
# Geolite2 web service (1000 request per day limit for free account) # GeoLite2 License Key (auto update database)
GEOIPUPDATE_ACCOUNT_ID= GEOIPUPDATE_ACCOUNT_ID=
GEOIPUPDATE_LICENSE_KEY= GEOIPUPDATE_LICENSE_KEY=

@ -1,43 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. /data/*
!/data/README.txt
/dist/*
!/dist/README.txt
# dependencies /tmp
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env .env
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
**/*.trace
**/*.zip
**/*.tar.gz
**/*.tgz
**/*.log
package-lock.json
**/*.bun

@ -1,15 +0,0 @@
---
printWidth: 80
tabWidth: 4
useTabs: true
semi: false
singleQuote: true
quoteProps: consistent
jsxSingleQuote: true
trailingComma: es5
bracketSpacing: true
bracketSameLine: true
arrowParens: always
htmlWhitespaceSensitivity: strict
vueIndentScriptAndStyle: false
singleAttributePerLine: true

@ -1,17 +1,22 @@
FROM oven/bun ARG GO_VERSION=1
FROM golang:${GO_VERSION}-bookworm as builder
WORKDIR /usr/src/app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN go build -v -o /run-app .
WORKDIR /app
COPY package.json . FROM debian:bookworm
COPY bun.lockb .
RUN bun install --production RUN apt update
RUN apt install -y ca-certificates
COPY public public WORKDIR /app
COPY src src
COPY tsconfig.json .
ENV NODE_ENV production COPY public ./public
CMD ["bun", "src/index.ts"] COPY regexes.yaml .
COPY --from=builder /run-app .
EXPOSE 3000 CMD ["./run-app"]

@ -1,19 +0,0 @@
# Elysia with Bun runtime
## Getting Started
To get started with this template, simply paste this command into your terminal:
```bash
bun create elysia ./elysia-example
```
## Development
To start the development server run:
```bash
bun run dev
```
Open http://localhost:3000/ with your browser to see the result.

Binary file not shown.

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

@ -0,0 +1,79 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package db
import (
"github.com/jackc/pgx/v5/pgtype"
)
type EmailVerificationToken struct {
ID string
UserID int32
Email string
ExpiresAt pgtype.Timestamptz
}
type Project struct {
ID int32
Uuid pgtype.UUID
Name string
UserID int32
QrBackground string
QrForeground string
CustomDomain pgtype.Text
DomainStatus string
EnableCustomDomain bool
CustomIp pgtype.Text
CustomDomainID pgtype.Text
}
type Session struct {
ID string
UserID int32
ExpiresAt pgtype.Timestamptz
}
type Setting struct {
UserID int32
QrBackground pgtype.Text
QrForeground pgtype.Text
}
type Shortener struct {
ID int32
Link string
Code string
CreatedAt pgtype.Timestamp
UserID int32
ProjectID pgtype.Int4
Active bool
Ios bool
IosLink pgtype.Text
Android bool
AndroidLink pgtype.Text
}
type User struct {
ID int32
Uuid pgtype.UUID
Email string
Username pgtype.Text
Password string
CreatedAt pgtype.Timestamp
EmailVerified bool
}
type Visitor struct {
ID int32
ShortenerID int32
CreatedAt pgtype.Timestamp
CountryCode string
Country string
City string
DeviceType string
DeviceVendor string
Os string
Browser string
}

@ -0,0 +1,165 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: query.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createVisitor = `-- name: CreateVisitor :exec
INSERT INTO visitor (
shortener_id,
device_type,
device_vendor,
browser,
os,
country,
country_code,
city
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
type CreateVisitorParams struct {
ShortenerID int32
DeviceType string
DeviceVendor string
Browser string
Os string
Country string
CountryCode string
City string
}
func (q *Queries) CreateVisitor(ctx context.Context, arg CreateVisitorParams) error {
_, err := q.db.Exec(ctx, createVisitor,
arg.ShortenerID,
arg.DeviceType,
arg.DeviceVendor,
arg.Browser,
arg.Os,
arg.Country,
arg.CountryCode,
arg.City,
)
return err
}
const getShortener = `-- name: GetShortener :one
SELECT id, link, code, created_at, user_id, project_id, active, ios, ios_link, android, android_link
FROM shortener
WHERE code = $1
LIMIT 1
`
func (q *Queries) GetShortener(ctx context.Context, code string) (Shortener, error) {
row := q.db.QueryRow(ctx, getShortener, code)
var i Shortener
err := row.Scan(
&i.ID,
&i.Link,
&i.Code,
&i.CreatedAt,
&i.UserID,
&i.ProjectID,
&i.Active,
&i.Ios,
&i.IosLink,
&i.Android,
&i.AndroidLink,
)
return i, err
}
const getShortenerWithDomain = `-- name: GetShortenerWithDomain :one
SELECT shortener.id, shortener.link, shortener.code, shortener.created_at, shortener.user_id, shortener.project_id, shortener.active, shortener.ios, shortener.ios_link, shortener.android, shortener.android_link,
project.custom_domain as domain
FROM shortener
LEFT JOIN project ON project.id = shortener.project_id
WHERE shortener.code = $1
AND project.custom_domain = $2
AND project.enable_custom_domain IS TRUE
LIMIT 1
`
type GetShortenerWithDomainParams struct {
Code string
CustomDomain pgtype.Text
}
type GetShortenerWithDomainRow struct {
ID int32
Link string
Code string
CreatedAt pgtype.Timestamp
UserID int32
ProjectID pgtype.Int4
Active bool
Ios bool
IosLink pgtype.Text
Android bool
AndroidLink pgtype.Text
Domain pgtype.Text
}
func (q *Queries) GetShortenerWithDomain(ctx context.Context, arg GetShortenerWithDomainParams) (GetShortenerWithDomainRow, error) {
row := q.db.QueryRow(ctx, getShortenerWithDomain, arg.Code, arg.CustomDomain)
var i GetShortenerWithDomainRow
err := row.Scan(
&i.ID,
&i.Link,
&i.Code,
&i.CreatedAt,
&i.UserID,
&i.ProjectID,
&i.Active,
&i.Ios,
&i.IosLink,
&i.Android,
&i.AndroidLink,
&i.Domain,
)
return i, err
}
const listShorteners = `-- name: ListShorteners :many
SELECT id, link, code, created_at, user_id, project_id, active, ios, ios_link, android, android_link
FROM shortener
`
func (q *Queries) ListShorteners(ctx context.Context) ([]Shortener, error) {
rows, err := q.db.Query(ctx, listShorteners)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Shortener
for rows.Next() {
var i Shortener
if err := rows.Scan(
&i.ID,
&i.Link,
&i.Code,
&i.CreatedAt,
&i.UserID,
&i.ProjectID,
&i.Active,
&i.Ios,
&i.IosLink,
&i.Android,
&i.AndroidLink,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

@ -0,0 +1,29 @@
-- name: ListShorteners :many
SELECT *
FROM shortener;
-- name: GetShortener :one
SELECT *
FROM shortener
WHERE code = $1
LIMIT 1;
-- name: GetShortenerWithDomain :one
SELECT shortener.*,
project.custom_domain as domain
FROM shortener
LEFT JOIN project ON project.id = shortener.project_id
WHERE shortener.code = $1
AND project.custom_domain = $2
AND project.enable_custom_domain IS TRUE
LIMIT 1;
-- name: CreateVisitor :exec
INSERT INTO visitor (
shortener_id,
device_type,
device_vendor,
browser,
os,
country,
country_code,
city
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8);

@ -1,15 +0,0 @@
---
# docker-compose.yml
version: '3.9'
services:
app:
image: oven/bun
container_name: linkshortener_elysia_dev
# override default entrypoint allows us to do `bun install` before serving
entrypoint: []
# execute bun install before we start the dev server in watch mode
command: /bin/sh -c 'bun install && bun run --watch src/index.ts'
# expose the right ports
ports: [3000:3000]
# setup a host mounted volume to sync changes to the container
volumes: [./:/home/bun/app]

@ -0,0 +1,39 @@
module tzgyn/kon-redirect
go 1.22.5
require (
github.com/gofiber/fiber/v2 v2.52.5
github.com/jackc/pgx/v5 v5.6.0
github.com/joho/godotenv v1.5.1
github.com/mileusna/useragent v1.3.4
github.com/oschwald/geoip2-golang v1.11.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/robfig/cron/v3 v3.0.0
github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
)

@ -0,0 +1,115 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk=
github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 h1:SIKIoA4e/5Y9ZOl0DCe3eVMLPOQzJxgZpfdHHeauNTM=
github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,216 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"tzgyn/kon-redirect/db"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/joho/godotenv"
"github.com/mileusna/useragent"
"github.com/oschwald/geoip2-golang"
"github.com/patrickmn/go-cache"
"github.com/robfig/cron/v3"
"github.com/ua-parser/uap-go/uaparser"
)
func main() {
err := godotenv.Load()
if err != nil {
fmt.Println("No .env found")
}
parser, err := uaparser.New("./regexes.yaml")
if err != nil {
log.Fatal(err)
}
fmt.Println("Initializing GeoLite2 DB...")
_, err = os.Stat("./data/GeoLite2-City.mmdb")
if err != nil {
fmt.Println("GeoLite2 DB Not Found...")
err := downloadAndExtractDB()
if err != nil {
log.Fatal(err)
}
}
geodb, err := geoip2.Open("./data/GeoLite2-City.mmdb")
if err != nil {
log.Fatal("Failed to initialize GeoLite2 DB")
}
fmt.Println("Finished initializing GeoLite2 DB")
fmt.Println("Initializing postgres DB and cache...")
ctx := context.Background()
c := cache.New(1*time.Hour, 5*time.Minute)
c.Set("key", "value", cache.DefaultExpiration)
dbUrl := os.Getenv("DATABASE_URL")
if len(dbUrl) == 0 {
log.Fatal("DATABASE_URL not found")
}
conn, err := pgx.Connect(ctx, dbUrl)
if err != nil {
log.Fatal(err)
}
queries := db.New(conn)
fmt.Println("Finished initializing postgres DB and cache")
cache_client := cache.New(1*time.Hour, 5*time.Minute)
app := fiber.New()
app.Use(logger.New())
app.Use(limiter.New(limiter.Config{
Max: 20,
Expiration: 1 * time.Minute,
}))
app.Static("/", "./public")
fallbackurl := os.Getenv("FALLBACK_URL")
if len(fallbackurl) == 0 {
fallbackurl = "https://app.kon.sh"
}
appurl := os.Getenv("APP_URL")
if len(appurl) == 0 {
appurl = "kon.sh"
}
app.Get("/", func(c *fiber.Ctx) error {
return c.Redirect(fallbackurl)
})
app.Get("/:code", func(c *fiber.Ctx) error {
code := c.Params("code")
domain := c.Hostname()
uastring := c.GetReqHeaders()["User-Agent"][0]
ua := useragent.Parse(uastring)
client := parser.Parse(uastring)
redirecturl := ""
var shortenerId int32
if domain == appurl {
shortener, err := queries.GetShortener(ctx, code)
shortenerId = shortener.ID
if err != nil {
return c.Redirect(fallbackurl)
}
if ua.OS == "iOS" && shortener.Ios && len(shortener.IosLink.String) != 0 {
redirecturl = shortener.IosLink.String
} else if ua.OS == "Android" && shortener.Android && len(shortener.AndroidLink.String) != 0 {
redirecturl = shortener.AndroidLink.String
} else {
redirecturl = shortener.Link
}
} else {
shortener, err := queries.GetShortenerWithDomain(ctx, db.GetShortenerWithDomainParams{
Code: code,
CustomDomain: pgtype.Text{String: domain},
})
shortenerId = shortener.ID
if err != nil {
return c.Redirect(fallbackurl)
}
if ua.OS == "iOS" && shortener.Ios && len(shortener.IosLink.String) != 0 {
redirecturl = shortener.IosLink.String
} else if ua.OS == "Android" && shortener.Android && len(shortener.AndroidLink.String) != 0 {
redirecturl = shortener.AndroidLink.String
} else {
redirecturl = shortener.Link
}
}
ip := c.IPs()[0]
_, found := cache_client.Get(ip + "_" + string(shortenerId))
if found {
return c.Redirect(redirecturl)
}
cache_client.Set(ip+"_"+string(shortenerId), true, cache.DefaultExpiration)
record, err := getCity(geodb, ip)
if err != nil {
return c.Redirect(redirecturl)
}
devicetype := ""
if ua.Mobile {
devicetype = "mobile"
} else if ua.Tablet {
devicetype = "tablet"
} else if ua.Desktop {
devicetype = "desktop"
}
err = queries.CreateVisitor(ctx, db.CreateVisitorParams{
ShortenerID: shortenerId,
DeviceType: devicetype,
DeviceVendor: client.Device.Brand,
Browser: client.UserAgent.Family,
Os: client.Os.Family,
Country: record.Country.Names["en"],
CountryCode: record.Country.IsoCode,
City: record.City.Names["en"],
})
if err != nil {
return c.Redirect(redirecturl)
}
return c.Redirect(redirecturl)
})
cron := cron.New()
cron.AddFunc("@weekly", func() {
fmt.Println("Updating GeoLite2 DB...")
err := downloadAndExtractDB()
if err != nil {
log.Fatal(err)
}
fmt.Println("Finished updating GeoLite2 DB")
})
cron.Start()
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
go func() {
<-done
fmt.Println("Gracefully shutting down...")
_ = app.Shutdown()
}()
if err := app.Listen(":3000"); err != nil {
log.Panic(err)
}
fmt.Println("Running cleanup tasks...")
fmt.Println("Stopping cronjobs...")
cron.Stop()
geodb.Close()
conn.Close(ctx)
}

@ -1,26 +0,0 @@
{
"name": "elysia",
"version": "1.0.50",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun run --watch src/index.ts"
},
"dependencies": {
"@elysiajs/cors": "^0.6.0",
"@maxmind/geoip2-node": "^5.0.0",
"@types/pg": "^8.10.2",
"@types/ua-parser-js": "^0.7.39",
"elysia": "^1.1.5",
"elysia-rate-limit": "^4.1.0",
"kysely": "^0.26.3",
"nanoid": "^5.0.1",
"pg": "^8.11.3",
"ua-parser-js": "^1.0.37",
"zod": "^3.22.2"
},
"devDependencies": {
"bun-types": "latest",
"typescript": "^5.4.5"
},
"module": "src/index.js"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,10 @@
version: "2"
sql:
- engine: "postgresql"
queries: "db/query"
schema: "../frontend/drizzle"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"

@ -1,16 +0,0 @@
import { Database } from './types'
import { Pool } from 'pg'
import { Kysely, PostgresDialect } from 'kysely'
const dialect = new PostgresDialect({
pool: new Pool({
connectionString:
Bun.env.DATABASE_URL ??
'postgres://postgres:password@0.0.0.0:5432/link-shortener',
max: 10,
}),
})
export const db = new Kysely<Database>({
dialect,
})

@ -1,141 +0,0 @@
import { Elysia } from 'elysia'
import { db } from './database'
import { cors } from '@elysiajs/cors'
import { UAParser } from 'ua-parser-js'
import geoip2 from '@maxmind/geoip2-node'
import { rateLimit } from 'elysia-rate-limit'
import { LRUCache } from 'lru-cache'
const WebServiceClient = geoip2.WebServiceClient
const fallback_url = Bun.env.FALLBACK_URL ?? 'https://app.kon.sh'
const app_url = Bun.env.APP_URL ?? 'kon.sh'
const hosting_provider = Bun.env.HOSTING_PROVIDER
const clickLimiter = new LRUCache({
ttl: 60 * 60 * 60 * 1000, // 1 hr
ttlAutopurge: true,
})
const app = new Elysia().use(cors()).use(rateLimit({ duration: 1000 }))
app.get('/', ({ set }) => (set.redirect = fallback_url))
app.get('/invalid', () => 'Invalid Shortener')
app.get('/robots.txt', () => Bun.file('public/robots.txt'))
app.get(
'/:shortenerCode',
async ({ params: { shortenerCode }, set, request, cookie }) => {
try {
const request_domain = request.headers.get('host')
const domain = request_domain !== app_url ? request_domain : null
const ip = request.headers.get(
hosting_provider === 'fly.io'
? 'Fly-Client-IP'
: 'x-forwarded-for'
)
const query = db
.selectFrom('shortener')
.selectAll('shortener')
.where('shortener.code', '=', shortenerCode)
if (domain) {
query
.leftJoin('project', 'project.id', 'shortener.project_id')
.select(['project.custom_domain as domain'])
.where('project.custom_domain', '=', domain)
.where('project.enable_custom_domain', '=', true)
}
query.orderBy('created_at', 'desc')
const shortener = await query.execute()
const user_agent = request.headers.get('User-Agent')
const ua_parser = new UAParser(user_agent ?? '')
if (!shortener.length || !shortener[0].active) {
set.redirect = '/invalid'
return
}
if (
ua_parser.getOS().name === 'iOS' &&
shortener[0].ios &&
shortener[0].ios_link
) {
set.redirect = shortener[0].ios_link
} else if (
ua_parser.getOS().name === 'Android' &&
shortener[0].android &&
shortener[0].android_link
) {
set.redirect = shortener[0].android_link
} else {
set.redirect = shortener[0].link
}
const clickKey = `${ip}_${shortener[0].id}`
const clickLimited = clickLimiter.has(clickKey)
if (clickLimited) return
clickLimiter.set(clickKey, 1)
const geo = {
country: '',
country_code: '',
city: '',
}
if (Bun.env.GEOIPAPI) {
const response = await fetch(Bun.env.GEOIPAPI + '/' + ip)
const data = await response.json()
if (data.success && data.success == false) {
return
}
geo.country = data.Country.Names?.en || ''
geo.country_code = data.Country.IsoCode
geo.city = data.City.Names?.en || ''
} else {
const client = new WebServiceClient(
Bun.env.GEOIPUPDATE_ACCOUNT_ID || '',
Bun.env.GEOIPUPDATE_LICENSE_KEY || '',
{ host: 'geolite.info' }
)
const geolocation = await client.city(ip || '')
geo.country = geolocation.country?.names?.en || ''
geo.country_code = geolocation.country?.isoCode || ''
geo.city = geolocation.city?.names?.en || ''
}
const visitor_data = {
shortener_id: shortener[0].id,
device_type: ua_parser.getDevice().type || 'desktop',
device_vendor: ua_parser.getDevice().vendor,
browser: ua_parser.getBrowser().name,
os: ua_parser.getOS().name,
}
await db
.insertInto('visitor')
.values({ ...visitor_data, ...geo })
.execute()
} catch (error) {
console.error(error)
set.redirect = fallback_url
}
}
)
app.listen(Bun.env.PORT ?? 3000)
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
`${Bun.env.PGDATABASE}`
)
export type App = typeof app

@ -1,75 +0,0 @@
import {
ColumnType,
Generated,
Insertable,
Selectable,
Updateable,
} from 'kysely'
export type Timestamp = ColumnType<Date, Date | string, Date | string>
export interface Database {
shortener: ShortenerTable
visitor: VisitorTable
user: UserTable
project: ProjectTable
}
export interface ShortenerTable {
id: Generated<number>
link: string
ios: boolean
ios_link: string | null
android: boolean
android_link: string | null
code: string
active: boolean
created_at: ColumnType<Date, string | undefined, never>
project_id: number | null
}
export type Shortener = Selectable<ShortenerTable>
export type NewShortener = Insertable<ShortenerTable>
export type ShortenerUpdate = Updateable<ShortenerTable>
export interface VisitorTable {
id: Generated<number>
shortener_id: number
country: string
country_code: string
city: string
device_type: string | null
device_vendor: string | null
os: string | null
browser: string | null
created_at: ColumnType<Date, string | undefined, never>
}
export type Visitor = Selectable<VisitorTable>
export type NewVisitor = Insertable<VisitorTable>
export interface UserTable {
created_at: Generated<Timestamp>
email: string
id: Generated<number>
password: string
username: string
uuid: string
}
export type User = Selectable<UserTable>
export type NewUser = Insertable<UserTable>
export type UserUpdate = Updateable<UserTable>
export interface ProjectTable {
id: Generated<number>
uuid: Generated<string>
name: string
userId: number
custom_domain: string | null
enable_custom_domain: boolean
}
export type Project = Selectable<ProjectTable>
export type NewProject = Insertable<ProjectTable>
export type ProjectUpdate = Updateable<ProjectTable>

@ -1,105 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
"types": [
"bun-types"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

@ -0,0 +1,191 @@
package main
import (
"archive/tar"
"compress/gzip"
"encoding/base64"
"errors"
"io"
"net"
"net/http"
"os"
"path/filepath"
"github.com/oschwald/geoip2-golang"
)
func getCity(db *geoip2.Reader, queryIp string) (*geoip2.City, error) {
// If you are using strings that may be invalid, check that ip is not nil
ip := net.ParseIP(queryIp)
if ip == nil {
return nil, errors.New("invalid ip")
}
record, err := db.City(ip)
if err != nil {
return nil, errors.New("no data")
}
return record, nil
}
func downloadAndExtractDB() error {
_, err := os.ReadDir("./data")
if err != nil {
os.MkdirAll("./data", os.ModePerm)
}
_, err = os.ReadDir("./dist")
if err != nil {
os.MkdirAll("./dist", os.ModePerm)
}
err = downloadDB()
if err != nil {
return err
}
r, err := os.Open("./db.tar.gz")
if err != nil {
return err
}
err = Untar("./dist", r)
if err != nil {
return err
}
files, err := os.ReadDir("./dist")
if err != nil {
return err
}
fileInfo := files[0]
err = os.Rename("./dist/"+fileInfo.Name()+"/GeoLite2-City.mmdb", "./data/GeoLite2-City.mmdb")
if err != nil {
return err
}
err = os.RemoveAll("./dist/")
if err != nil {
return err
}
err = os.MkdirAll("./dist/", os.ModePerm)
if err != nil {
return err
}
err = os.Remove("./db.tar.gz")
if err != nil {
return err
}
return nil
}
func downloadDB() error {
accountID := os.Getenv("GEOIPUPDATE_ACCOUNT_ID")
if len(accountID) == 0 {
return errors.New("please provide ACCOUNT_ID as env")
}
licenseKey := os.Getenv("GEOIPUPDATE_LICENSE_KEY")
if len(licenseKey) == 0 {
return errors.New("please provide LICENSE_KEY as env")
}
out, err := os.Create("db.tar.gz")
if err != nil {
return err
}
defer out.Close()
auth := base64.StdEncoding.EncodeToString([]byte(accountID + ":" + licenseKey))
req, err := http.NewRequest(http.MethodGet, "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz", nil)
if err != nil {
return err
}
req.Header.Add("Authorization", "Basic "+auth)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
_, err = io.Copy(out, res.Body)
if err != nil {
return err
}
return nil
}
func Untar(dst string, r io.Reader) error {
gzr, err := gzip.NewReader(r)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
switch {
// if no more files are found return
case err == io.EOF:
return nil
// return any other error
case err != nil:
return err
// if the header is nil, just skip it (not sure how this happens)
case header == nil:
continue
}
// the target location where the dir/file should be created
target := filepath.Join(dst, header.Name)
// the following switch could also be done using fi.Mode(), not sure if there
// a benefit of using one vs. the other.
// fi := header.FileInfo()
// check the file type
switch header.Typeflag {
// if its a dir and it doesn't exist create it
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
}
// if it's a file create it
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return err
}
// copy over contents
if _, err := io.Copy(f, tr); err != nil {
return err
}
// manually close here after each file operation; defering would cause each file close
// to wait until all operations have completed.
f.Close()
}
}
}
Loading…
Cancel
Save