mirror of https://github.com/TZGyn/shortener
rewrite redirect backend with go to do all redirect and geo data in one app
parent
01a515fbc7
commit
85662507be
@ -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,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…
Reference in New Issue