init commit
This commit is contained in:
166
server/web/app/handlers/channel.go
Normal file
166
server/web/app/handlers/channel.go
Normal file
@@ -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()),
|
||||
}))
|
||||
}
|
||||
115
server/web/app/handlers/node.go
Normal file
115
server/web/app/handlers/node.go
Normal file
@@ -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)
|
||||
}
|
||||
1
server/web/app/handlers/user.go
Normal file
1
server/web/app/handlers/user.go
Normal file
@@ -0,0 +1 @@
|
||||
package handlers
|
||||
19
server/web/app/models/channel.go
Normal file
19
server/web/app/models/channel.go
Normal file
@@ -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
|
||||
}
|
||||
14
server/web/app/models/node.go
Normal file
14
server/web/app/models/node.go
Normal file
@@ -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"`
|
||||
}
|
||||
14
server/web/app/models/user.go
Normal file
14
server/web/app/models/user.go
Normal file
@@ -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"`
|
||||
}
|
||||
10
server/web/auth/auth.go
Normal file
10
server/web/auth/auth.go
Normal file
@@ -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)
|
||||
}
|
||||
41
server/web/auth/context.go
Normal file
41
server/web/auth/context.go
Normal file
@@ -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
|
||||
97
server/web/auth/middleware.go
Normal file
97
server/web/auth/middleware.go
Normal file
@@ -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()),
|
||||
)
|
||||
}
|
||||
16
server/web/router/router.go
Normal file
16
server/web/router/router.go
Normal file
@@ -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)
|
||||
}
|
||||
44
server/web/web.go
Normal file
44
server/web/web.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user