commit 10a4f010ce3cb1d02a33bce8d6fa9a5c6b6b3349 Author: luorijun Date: Wed Feb 19 14:23:58 2025 +0800 init commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c6c7652 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +PORT=8080 +SECRET=testing123 + +DB_HOST=localhost +DB_PORT=5432 +DB_DATABASE=app +DB_USERNAME=proxy +DB_PASSWORD=proxy +DB_TIMEZONE=Asia/Shanghai diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9668f99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +### Go template +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +bin/ +dist/ +build/ + +### editor + +.idea/ +.vscode/ + +### project + +.env +.env.* +!.env.example \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4321b8f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM ubuntu:20.04 + +WORKDIR /app + +COPY ./bin/proxy_linux_amd64 /app/proxy +RUN chmod +x /app/proxy + +EXPOSE $PORT + +CMD ["/app/proxy"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d9667f --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +## todo + +docker 脚本和环境变量文件都放在 scripts 目录下 + +client 配置环境变量(或者命令行参数) + +测试流程完成后合并项目 + +考虑是否需要将 devices 表改名成 nodes + +### 长期 + +考虑一下连接安全性 + +内部接口 http2 或者 protobuf + +考虑重构服务,集成 socks 解析,鉴权,转发 + +## 开发相关 + +### 环境变量 + +新增环境变量时,需要确保两个位置正确传递了变量 + +1. 系统环境或本地 `.env` 文件正确配置了变量值 +2. `docker-compose.yml` 文件中给服务容器正确传递了变量 + +### 更新测试环境 + +1. 构建项目 +2. 使用测试配置远程启动 docker + +## 转发服务 diff --git a/gen.go b/gen.go new file mode 100644 index 0000000..ad183db --- /dev/null +++ b/gen.go @@ -0,0 +1,31 @@ +package main + +import ( + "gorm.io/gen" + "proxy-server/server/orm" +) + +const ( + Host = "localhost" + Port = "5432" + Username = "gorm" + Password = "gorm" + Database = "gorm" + Timezone = "Asia/Shanghai" +) + +func main() { + g := gen.NewGenerator(gen.Config{ + OutPath: "../../temp-out", + Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, + }) + + orm.Init() + g.UseDB(orm.DB) + + g.ApplyBasic( + g.GenerateAllTable()..., + ) + + g.Execute() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c41f59f --- /dev/null +++ b/go.mod @@ -0,0 +1,59 @@ +module proxy-server + +go 1.23 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/google/gopacket v1.1.19 + github.com/joho/godotenv v1.5.1 + github.com/lmittmann/tint v1.0.7 + github.com/mattn/go-colorable v0.1.14 + github.com/pkg/errors v0.9.1 + gorm.io/driver/postgres v1.5.11 + gorm.io/gen v0.3.26 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/bytedance/sonic v1.12.8 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c // indirect + gorm.io/driver/mysql v1.4.4 // indirect + gorm.io/hints v1.1.0 // indirect + gorm.io/plugin/dbresolver v1.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..28c0f52 --- /dev/null +++ b/go.sum @@ -0,0 +1,170 @@ +github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= +github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +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.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y= +github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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.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= +gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c h1:jWdr7cHgl8c/ua5vYbR2WhSp+NQmzhsj0xoY3foTzW8= +gorm.io/datatypes v1.1.1-0.20230130040222-c43177d3cf8c/go.mod h1:SH2K9R+2RMjuX1CkCONrPwoe9JzVv2hkQvEu4bXGojE= +gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= +gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ= +gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.1.6/go.mod h1:W8LmC/6UvVbHKah0+QOC7Ja66EaZXHwUTjgXY8YNWX8= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= +gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= +gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= +gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= +gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw= +gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y= +gorm.io/plugin/dbresolver v1.5.0 h1:XVHLxh775eP0CqVh3vcfJtYqja3uFl5Wr3cKlY8jgDY= +gorm.io/plugin/dbresolver v1.5.0/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/main.go b/main.go new file mode 100644 index 0000000..69fe366 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "proxy-server/server" +) + +func main() { + server.Start() +} diff --git a/pkg/resp/resp.go b/pkg/resp/resp.go new file mode 100644 index 0000000..1aa1449 --- /dev/null +++ b/pkg/resp/resp.go @@ -0,0 +1,21 @@ +package resp + +type Data struct { + Error bool + Cause string + Data interface{} +} + +func Done(data interface{}) *Data { + return &Data{ + Error: false, + Data: data, + } +} + +func Fail(cause string) *Data { + return &Data{ + Error: true, + Cause: cause, + } +} diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..4131777 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,42 @@ +Remove-Job * + +$tasks = 0 +$start = Get-Date + +$tasks++ +Write-Output "building proxy for windows amd64..." +Start-Job -ScriptBlock { + $env:GOOS = "windows"; $env:GOARCH = "amd64"; go build -ldflags '-w -s' -o bin/proxy_win_amd64.exe main.go +} | Out-Null + +$tasks++ +Write-Output "building proxy for linux amd64..." +Start-Job -ScriptBlock { + $env:GOOS = "linux"; $env:GOARCH = "amd64"; go build -ldflags '-w -s' -o bin/proxy_linux_amd64 main.go +} | Out-Null + +$tasks++ +Write-Output "building proxy for linux arm64..." +Start-Job -ScriptBlock { + $env:GOOS = "linux"; $env:GOARCH = "arm64"; go build -ldflags '-w -s' -o bin/proxy_linux_arm64 main.go +} | Out-Null + +# Wait for all jobs to complete +while ($tasks -gt 0) +{ + foreach ($job in Get-Job) + { + if ($job.State -eq "Completed") + { + $tasks-- + $job | Receive-Job + $job | Remove-Job + } + } +} + +$end = Get-Date + +Write-Output "build completed" +Write-Output "time taken: $( ($end - $start).TotalSeconds ) seconds" +Write-Output "output files are in ./bin/" \ No newline at end of file diff --git a/scripts/dev/docker-compose.yaml b/scripts/dev/docker-compose.yaml new file mode 100644 index 0000000..f19da7a --- /dev/null +++ b/scripts/dev/docker-compose.yaml @@ -0,0 +1,38 @@ +name: proxy-server + +services: + + frp: + container_name: proxy-server-dev-frp + build: + context: ./frp + dockerfile: Dockerfile + ports: + - "18080:18080" + - "20000-20100:20000-20100" + networks: + - proxy-server-dev + restart: always + + postgres: + container_name: proxy-server-dev-postgres + image: postgres:17 + environment: + POSTGRES_DB: $DB_DATABASE + POSTGRES_USER: $DB_USERNAME + POSTGRES_PASSWORD: $DB_PASSWORD + ports: + - "${DB_PORT}:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - proxy-server-dev + restart: always + +networks: + proxy-server-dev: + driver: bridge + +volumes: + postgres-data: + name: proxy-server-dev-postgres-data \ No newline at end of file diff --git a/scripts/dev/frp/Dockerfile b/scripts/dev/frp/Dockerfile new file mode 100644 index 0000000..2552e8d --- /dev/null +++ b/scripts/dev/frp/Dockerfile @@ -0,0 +1,10 @@ +FROM ubuntu:20.04 + +WORKDIR /app + +COPY frps frps +COPY frps.toml frps.toml + +EXPOSE 18080 + +CMD ["./frps", "-c", "frps.toml"] \ No newline at end of file diff --git a/scripts/dev/frp/frps b/scripts/dev/frp/frps new file mode 100644 index 0000000..2a4a864 Binary files /dev/null and b/scripts/dev/frp/frps differ diff --git a/scripts/dev/frp/frps.toml b/scripts/dev/frp/frps.toml new file mode 100644 index 0000000..d8dff09 --- /dev/null +++ b/scripts/dev/frp/frps.toml @@ -0,0 +1,14 @@ +bindPort = 18080 +transport.tcpMux = true + +[[httpPlugins]] +name = "chan-req" +addr = "host.docker.internal:8080" +path = "/chan/request" +ops = ["NewUserConn"] + +[[httpPlugins]] +name = "chan-test" +addr = "host.docker.internal:8080" +path = "/chan/test" +ops = ["NewProxy", "NewWorkConn"] \ No newline at end of file diff --git a/scripts/sql/init.sql b/scripts/sql/init.sql new file mode 100644 index 0000000..670a2a5 --- /dev/null +++ b/scripts/sql/init.sql @@ -0,0 +1,73 @@ +-- nodes +DROP TABLE IF EXISTS nodes; +CREATE TABLE nodes ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + provider VARCHAR(255) NOT NULL, + location VARCHAR(255) NOT NULL, + ip_address VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); +CREATE INDEX devices_provider_index ON nodes (provider); +CREATE INDEX devices_location_index ON nodes (location); + +-- users +DROP TABLE IF EXISTS users; +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + password VARCHAR(255) NOT NULL, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + phone VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- user_ips +DROP TABLE IF EXISTS user_ips; +CREATE TABLE user_ips ( + id SERIAL PRIMARY KEY, + user_id int NOT NULL REFERENCES users (id) + ON UPDATE CASCADE + ON DELETE CASCADE, + ip_address VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); +CREATE INDEX user_ips_user_id_index ON user_ips (user_id); +CREATE INDEX user_ips_ip_address_index ON user_ips (ip_address); + +-- channel +DROP TABLE IF EXISTS channels; +CREATE TABLE channels ( + id SERIAL PRIMARY KEY, + user_id int NOT NULL REFERENCES users (id) + ON UPDATE CASCADE + ON DELETE CASCADE, + node_id int NOT NULL REFERENCES nodes (id) -- + ON UPDATE CASCADE -- + ON DELETE SET NULL, -- 节点删除后,用户侧需要保留提取记录 + forward_snapshot VARCHAR(255), + protocol VARCHAR(255), + auth_ip bool, + auth_pass bool, + username VARCHAR(255), + password VARCHAR(255), + expiration TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); +CREATE INDEX channel_user_id_index ON channels (user_id); +CREATE INDEX channel_node_id_index ON channels (node_id); + +-- ==================== +-- 填充数据 +-- ==================== + + diff --git a/scripts/sql/临时迁移记录.txt b/scripts/sql/临时迁移记录.txt new file mode 100644 index 0000000..bec9e8b --- /dev/null +++ b/scripts/sql/临时迁移记录.txt @@ -0,0 +1,4 @@ +==================== +删除 channels.target_snapshot 不需要目标地址快照 +添加 channels.auth_ip 是否验证白名单 +添加 channels.auth_pass 是否验证密码 \ No newline at end of file diff --git a/scripts/test/docker-compose.yaml b/scripts/test/docker-compose.yaml new file mode 100644 index 0000000..6979adc --- /dev/null +++ b/scripts/test/docker-compose.yaml @@ -0,0 +1,59 @@ +name: proxy-server + +services: + + frp: + container_name: proxy-server-dev-frp + build: + context: ./frp + dockerfile: Dockerfile + ports: + - "18080:18080" + - "20000-20100:20000-20100" + networks: + - proxy-server-test + restart: always + + postgres: + container_name: proxy-server-dev-postgres + image: postgres:17 + environment: + POSTGRES_DB: $DB_DATABASE + POSTGRES_USER: $DB_USERNAME + POSTGRES_PASSWORD: $DB_PASSWORD + ports: + - "${DB_PORT}:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - proxy-server-test + restart: always + + service: + container_name: proxy-server-dev-service + build: + context: ../.. + dockerfile: Dockerfile + environment: + PORT: $PORT + DB_HOST: $DB_HOST + DB_PORT: $DB_PORT + DB_DATABASE: $DB_DATABASE + DB_USERNAME: $DB_USERNAME + DB_PASSWORD: $DB_PASSWORD + DB_TIMEZONE: $DB_TIMEZONE + ports: + - "${PORT}:${PORT}" + networks: + - proxy-server-test + depends_on: + - postgres + restart: always + +networks: + proxy-server-test: + driver: bridge + +volumes: + postgres-data: + name: proxy-server-test-postgres-data \ No newline at end of file diff --git a/scripts/test/frp/Dockerfile b/scripts/test/frp/Dockerfile new file mode 100644 index 0000000..2552e8d --- /dev/null +++ b/scripts/test/frp/Dockerfile @@ -0,0 +1,10 @@ +FROM ubuntu:20.04 + +WORKDIR /app + +COPY frps frps +COPY frps.toml frps.toml + +EXPOSE 18080 + +CMD ["./frps", "-c", "frps.toml"] \ No newline at end of file diff --git a/scripts/test/frp/frps b/scripts/test/frp/frps new file mode 100644 index 0000000..2a4a864 Binary files /dev/null and b/scripts/test/frp/frps differ diff --git a/scripts/test/frp/frps.toml b/scripts/test/frp/frps.toml new file mode 100644 index 0000000..e9f3d4c --- /dev/null +++ b/scripts/test/frp/frps.toml @@ -0,0 +1,8 @@ +bindPort = 18080 +transport.tcpMux = true + +[[httpPlugins]] +name = "chan-req" +addr = "service:8080" +path = "/chan/request" +ops = ["NewUserConn"] diff --git a/server/monitor/monitor.go b/server/monitor/monitor.go new file mode 100644 index 0000000..fa4ced3 --- /dev/null +++ b/server/monitor/monitor.go @@ -0,0 +1,61 @@ +package monitor + +import ( + "context" + "encoding/hex" + "github.com/google/gopacket" + "github.com/google/gopacket/pcap" + "github.com/pkg/errors" + "log/slog" +) + +func Start(ctx context.Context, errCh chan error) { + + // 打开一个网络接口 + device, err := pcap.OpenLive("WLAN", 1600, true, pcap.BlockForever) + if err != nil { + b, er := hex.DecodeString("\\xbb") + slog.Debug("b", b, er) + errCh <- errors.Wrap(err, "打开网络接口失败") + return + } + defer device.Close() + + err = device.SetBPFFilter("tcp") + if err != nil { + errCh <- errors.Wrap(err, "设置 BPF 过滤器失败") + return + } + + err = device.SetDirection(pcap.DirectionIn) + if err != nil { + errCh <- errors.Wrap(err, "设置捕获方向失败") + return + } + + // 创建一个数据包源 + source := gopacket.NewPacketSource(device, device.LinkType()) + source.NoCopy = true + source.Lazy = true + + for { + select { + case <-ctx.Done(): + slog.Debug("monitor 被动结束") + errCh <- nil + return + + case packet := <-source.Packets(): + handle(packet) + } + } +} + +func handle(packet gopacket.Packet) { + slog.Debug("Packet: ", packet) + slog.Debug("Layers: ", packet.Layers()) + slog.Debug("Application: ", packet.ApplicationLayer()) + slog.Debug("Transport: ", packet.TransportLayer()) + slog.Debug("Network: ", packet.NetworkLayer()) + slog.Debug("Link: ", packet.LinkLayer()) +} diff --git a/server/orm/orm.go b/server/orm/orm.go new file mode 100644 index 0000000..152e0ab --- /dev/null +++ b/server/orm/orm.go @@ -0,0 +1,34 @@ +package orm + +import ( + "fmt" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "os" +) + +var DB *gorm.DB + +func Init() { + Host := os.Getenv("DB_HOST") + Port := os.Getenv("DB_PORT") + Database := os.Getenv("DB_DATABASE") + Username := os.Getenv("DB_USERNAME") + Password := os.Getenv("DB_PASSWORD") + Timezone := os.Getenv("DB_TIMEZONE") + + dsn := fmt.Sprintf( + "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable TimeZone=%s", + Host, Port, Username, Password, Database, Timezone, + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.Default, + }) + if err != nil { + panic(err) + } + + DB = db +} diff --git a/server/service.go b/server/service.go new file mode 100644 index 0000000..e3f597c --- /dev/null +++ b/server/service.go @@ -0,0 +1,71 @@ +package server + +import ( + "context" + "github.com/joho/godotenv" + "github.com/lmittmann/tint" + "github.com/mattn/go-colorable" + "log/slog" + "os" + "proxy-server/server/orm" + "proxy-server/server/web" +) + +func Start() { + defer func() { + err := recover() + if err != nil { + slog.Error("服务由于意外的 panic 导致退出", err) + } + }() + + ctx := context.Background() + + // 初始化环境变量 + err := godotenv.Load() + if err != nil { + slog.Debug("没有本地环境变量文件") + } + + // 配置日志 + writer := colorable.NewColorable(os.Stdout) + logger := slog.New(tint.NewHandler(writer, &tint.Options{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { + err, ok := attr.Value.Any().(error) + if !ok { + return attr + } + return tint.Err(err) + }, + })) + slog.SetDefault(logger) + + // 初始化公共组件 + orm.Init() + + // 启动子服务 + goCount := 1 + errChan := make(chan error, goCount) + ctxC, cancel := context.WithCancel(ctx) + defer cancel() + + go web.Start(ctxC, errChan) + //go monitor.Start(ctxC, errChan) + slog.Info("服务启动成功") + + // 监听异常 + well := true + for i := 0; i < goCount; i++ { + err := <-errChan + if err != nil { + slog.Error("服务异常退出", err) + if well { // 第一次出错时取消其他服务 + well = false + cancel() + } + } + } + close(errChan) + slog.Info("服务已全部退出") +} diff --git a/server/web/app/handlers/channel.go b/server/web/app/handlers/channel.go new file mode 100644 index 0000000..df0f421 --- /dev/null +++ b/server/web/app/handlers/channel.go @@ -0,0 +1,166 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "log/slog" + "proxy-server/pkg/resp" + "proxy-server/server/orm" + "proxy-server/server/web/app/models" + "strings" + "time" +) + +// region frp 接口 + +type FrpData struct { + Reject bool + RejectReason string + Unchange bool +} + +func ChanRequest(c *gin.Context) { + type Body struct { + Content struct { + ProxyName string `json:"proxy_name"` + ProxyType string `json:"proxy_type"` + RemoteAddr string `json:"remote_addr"` + User interface{} + } + } + + op := c.Query("op") + if op != "NewUserConn" { + _ = c.Error(errors.New("不支持的操作")) + return + } + + id := c.GetHeader("X-Frp-Reqid") + if id == "" { + _ = c.Error(errors.New("请求头中缺少 X-Frp-Reqid")) + return + } + + var body Body + err := c.ShouldBindJSON(&body) + if err != nil { + _ = c.Error(errors.Wrap(err, "解析请求正文失败")) + return + } + content := body.Content + + // 检查此 ip 是否有权限访问目标 node + clientIp := strings.Split(content.RemoteAddr, ":")[0] + targetNode := content.ProxyName + slog.Debug(id + " 客户端 " + clientIp + " 请求连接到 " + targetNode) + + var channels []models.Channel + err = orm.DB. + Joins("INNER JOIN public.nodes n ON channels.node_id = n.id AND n.name = ?", targetNode). + Joins("INNER JOIN public.users u ON channels.user_id = u.id"). + Joins("INNER JOIN public.user_ips ip ON u.id = ip.user_id AND ip.ip_address = ?", clientIp). + Find(&channels).Error + if err != nil { + _ = c.Error(errors.Wrap(err, "查询用户权限失败")) + return + } + + // 返回响应 + rsCount := len(channels) + if rsCount > 1 { + slog.Warn(clientIp + " + " + targetNode + "的组合有多个权限结果,这是不应当存在的") + } + + if rsCount == 0 { + slog.Debug(id + " 没有权限") + reject(c) + return + } + channel := channels[0] + if channel.Expiration.Before(time.Now()) { + slog.Debug(id + " 权限已过期") + reject(c) + return + } + + slog.Debug(id + " 通过验证") + confirm(c) +} + +func ChanTest(c *gin.Context) { + var body map[string]interface{} + err := c.ShouldBindJSON(&body) + if err != nil { + slog.Error("解析请求正文失败", err) + } + for k, v := range body { + slog.Debug("map", "key: ", k, " value: ", v) + } + confirm(c) +} + +func confirm(c *gin.Context) { + c.JSON(200, FrpData{ + Reject: false, + Unchange: true, + }) +} + +func reject(c *gin.Context) { + c.JSON(401, FrpData{ + Reject: true, + RejectReason: "客户端没有权限访问该节点", + }) +} + +// endregion + +func ChanAuth(c *gin.Context) { + type Body struct { + Username string `json:"username"` + Password string `json:"password"` + } + type Data struct { + Timeout uint64 `json:"timeout"` + } + + var body Body + err := c.ShouldBindJSON(&body) + if err != nil { + _ = c.Error(err) + c.JSON(400, resp.Fail("请求参数错误")) + return + } + + // 查找通道 + var result *models.Channel + orm.DB. + Model(&models.Channel{}). + Where(&models.Channel{ + Username: body.Username, + Password: body.Password, + }). + First(&result) + if result == nil { + _ = c.Error(errors.New("用户信息不存在")) + c.JSON(401, resp.Fail("账号密码错误")) + return + } + + // 验证账号密码 todo 哈希密码验证 + if result.Username != body.Username || result.Password != body.Password { + _ = c.Error(errors.New("账号密码错误")) + c.JSON(401, resp.Fail("账号密码错误")) + return + } + + // 计算到期时间 + timeout := result.Expiration.Sub(time.Now()) + + // todo 保存会话 对于大量短连接的情况,考虑如何保存连接会话信息 + + // 返回结果 + c.JSON(200, resp.Done(Data{ + Timeout: uint64(timeout.Seconds()), + })) +} diff --git a/server/web/app/handlers/node.go b/server/web/app/handlers/node.go new file mode 100644 index 0000000..a99547c --- /dev/null +++ b/server/web/app/handlers/node.go @@ -0,0 +1,115 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "gorm.io/gorm" + "os" + "proxy-server/server/orm" + "proxy-server/server/web/app/models" +) + +type NodeRegisterReq struct { + Name string + Secret string +} + +func NodeRegister(c *gin.Context) { + + // 请求参数 + var req NodeRegisterReq + err := c.ShouldBind(&req) + if err != nil { + _ = c.Error(errors.Wrap(err, "参数解析错误")) + return + } + + // 验证 secret + secret := os.Getenv("SECRET") + if req.Secret != secret { + _ = c.Error(errors.New("拒绝连接")) + return + } + + // 注册节点 + // todo 查询运营商和地区 + err = orm.DB.Transaction(func(tx *gorm.DB) error { + + // 查询节点是否已存在 + var count int64 + err := orm.DB.Where(&models.Node{ + Name: req.Name, + }).Count(&count).Error + if err != nil { + return err + } + + // 不存在则注册 + if count == 0 { + ipAddress := c.ClientIP() + node := models.Node{ + Name: req.Name, + Provider: "", + Location: "", + IPAddress: ipAddress, + } + err = orm.DB.Create(&node).Error + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + _ = c.Error(errors.Wrap(err, "注册节点失败")) + return + } + + c.Status(200) +} + +type NodeReportReq struct { + Name string +} + +func NodeReport(c *gin.Context) { + + // 请求参数 + var req NodeReportReq + err := c.ShouldBind(&req) + if err != nil { + _ = c.Error(errors.Wrap(err, "参数解析错误")) + return + } + + // 上报节点信息 + err = orm.DB.Transaction(func(tx *gorm.DB) error { + + // 查询节点 + var node models.Node + err = orm.DB.Where(&models.Node{ + Name: req.Name, + }).First(&node).Error + if err != nil { + return err + } + + // 更新节点信息 + ipAddress := c.ClientIP() + if ipAddress != node.IPAddress { + err = orm.DB.Model(&node).Update("ip_address", ipAddress).Error + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + _ = c.Error(errors.Wrap(err, "上报节点信息失败")) + return + } + + c.Status(200) +} diff --git a/server/web/app/handlers/user.go b/server/web/app/handlers/user.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/server/web/app/handlers/user.go @@ -0,0 +1 @@ +package handlers diff --git a/server/web/app/models/channel.go b/server/web/app/models/channel.go new file mode 100644 index 0000000..7551991 --- /dev/null +++ b/server/web/app/models/channel.go @@ -0,0 +1,19 @@ +package models + +import ( + "gorm.io/gorm" + "time" +) + +// Channel 连接认证模型 +type Channel struct { + gorm.Model + UserId uint + NodeId uint + Protocol string + Username string + Password string + AuthIp bool + AuthPass bool + Expiration time.Time +} diff --git a/server/web/app/models/node.go b/server/web/app/models/node.go new file mode 100644 index 0000000..349bc97 --- /dev/null +++ b/server/web/app/models/node.go @@ -0,0 +1,14 @@ +package models + +import "gorm.io/gorm" + +// Node 客户端模型 +type Node struct { + gorm.Model + Name string + Provider string + Location string + IPAddress string + + Channels []Channel `gorm:"foreignKey:NodeId"` +} diff --git a/server/web/app/models/user.go b/server/web/app/models/user.go new file mode 100644 index 0000000..d29f664 --- /dev/null +++ b/server/web/app/models/user.go @@ -0,0 +1,14 @@ +package models + +import "gorm.io/gorm" + +type User struct { + gorm.Model + Password string + Username string + Email string + Phone string + Name string + + Channels []Channel `gorm:"foreignKey:UserId"` +} diff --git a/server/web/auth/auth.go b/server/web/auth/auth.go new file mode 100644 index 0000000..9ffc6e2 --- /dev/null +++ b/server/web/auth/auth.go @@ -0,0 +1,10 @@ +package auth + +import "github.com/gin-gonic/gin" + +type Config struct { +} + +func Apply(r *gin.Engine, config *Config) { + r.Use(middleware) +} diff --git a/server/web/auth/context.go b/server/web/auth/context.go new file mode 100644 index 0000000..ff737ab --- /dev/null +++ b/server/web/auth/context.go @@ -0,0 +1,41 @@ +package auth + +type Context interface { + Permissions() map[string]struct{} + PermitAll(permissions ...string) bool + PermitAny(permissions ...string) bool +} + +// region DeviceContext + +type DeviceContext struct { + ID uint + IpAddress string + Permissions map[string]struct{} +} + +func (c DeviceContext) PermitAny(permissions ...string) bool { + if _, exist := c.Permissions["*"]; exist { + return true + } + for _, permission := range permissions { + if _, ok := c.Permissions[permission]; ok { + return true + } + } + return false +} + +func (c DeviceContext) PermitAll(permissions ...string) bool { + if _, exist := c.Permissions["*"]; exist { + return true + } + for _, permission := range permissions { + if _, ok := c.Permissions[permission]; !ok { + return false + } + } + return true +} + +// endregion diff --git a/server/web/auth/middleware.go b/server/web/auth/middleware.go new file mode 100644 index 0000000..84e38fc --- /dev/null +++ b/server/web/auth/middleware.go @@ -0,0 +1,97 @@ +package auth + +import ( + "encoding/base64" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "log/slog" + "net/http" + "os" + "proxy-server/pkg/resp" + "slices" + "strings" +) + +func middleware(c *gin.Context) { + + auth := check(c) + if auth { + secret, err := getSecret(c) + if err != nil { + slog.Error("认证失败", err) + fail400(c, err) + return + } + err = authenticate(c, secret) + if err != nil { + slog.Error("认证失败", err) + fail401(c, err) + return + } + } + + c.Next() +} + +var ( + securedPaths = []string{ + "/connect", + } +) + +func check(c *gin.Context) bool { + path := c.Request.URL.Path + if slices.Contains(securedPaths, path) { + return true + } + return false +} + +func getSecret(c *gin.Context) (string, error) { + + // 获取认证信息 + header := strings.Split(c.GetHeader("Authorization"), " ") + if len(header) != 2 { + return "", errors.New("无认证信息") + } + + // 检查认证类型 + schema := header[0] + if schema != "Secret" { + return "", errors.New("不支持的认证类型 " + schema) + } + + // 解码密钥 + parameters := header[1] + result, err := base64.URLEncoding.DecodeString(parameters) + if err != nil { + return "", errors.Wrap(err, "密钥解析失败") + } + + return string(result), nil +} + +func authenticate(_ *gin.Context, secret string) error { + if secret != os.Getenv("SECRET") { + return errors.New("认证失败") + } + return nil +} + +func fail400(c *gin.Context, err error) { + _ = c.Error(err) + c.Abort() + c.JSON( + http.StatusBadRequest, + resp.Fail(err.Error()), + ) +} + +func fail401(c *gin.Context, err error) { + _ = c.Error(err) + c.Abort() + c.JSON( + http.StatusUnauthorized, + resp.Fail(err.Error()), + ) +} diff --git a/server/web/router/router.go b/server/web/router/router.go new file mode 100644 index 0000000..0a84ca6 --- /dev/null +++ b/server/web/router/router.go @@ -0,0 +1,16 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "proxy-server/server/web/app/handlers" +) + +func Apply(r *gin.Engine) { + + r.POST("/node/register", handlers.NodeRegister) + r.POST("/node/report", handlers.NodeReport) + + r.POST("/chan/request", handlers.ChanRequest) + r.POST("/chan/auth", handlers.ChanAuth) + r.POST("/chan/test", handlers.ChanTest) +} diff --git a/server/web/web.go b/server/web/web.go new file mode 100644 index 0000000..d75bef6 --- /dev/null +++ b/server/web/web.go @@ -0,0 +1,44 @@ +package web + +import ( + "context" + "github.com/gin-gonic/gin" + "log/slog" + "net/http" + "os" + "proxy-server/server/web/auth" + "proxy-server/server/web/router" +) + +var server *http.Server + +func Start(ctx context.Context, errCh chan error) { + address := ":" + os.Getenv("PORT") + engine := gin.Default() + server = &http.Server{Addr: address, Handler: engine} + + // 监听关闭信号 + go func() { + <-ctx.Done() + slog.Info("web 服务被动关闭") + err := server.Shutdown(ctx) + if err != nil { + slog.Error("web 服务关闭失败", err) + return + } + }() + + // 配置中间件和路由 + auth.Apply(engine, nil) + router.Apply(engine) + + // 启动服务 + err := server.ListenAndServe() + if err != nil { + errCh <- err + return + } + + slog.Debug("web 服务主动结束") + errCh <- nil +}