From 47f354927cda2b2d416b9fe273e0c77e22b0784a Mon Sep 17 00:00:00 2001 From: gua Date: Fri, 13 Oct 2023 18:14:57 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E6=B7=BB=E5=8A=A0v2b?= =?UTF-8?q?oard=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 2 + Dockerfile | 2 +- app/cmd/server.go | 112 +++++++++++++++++++++++++++++++++++ app/cmd/server_test.yaml | 5 ++ core/server/config.go | 1 + extras/auth/v2board.go | 88 +++++++++++++++++++++++++++ extras/trafficlogger/http.go | 67 +++++++++++++++++++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 extras/auth/v2board.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b2a56fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +./github +build \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e49ad63..e2a4a51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,4 +36,4 @@ RUN set -ex \ COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria -ENTRYPOINT ["hysteria"] \ No newline at end of file +ENTRYPOINT ["hysteria" , "server", "-c", "/etc/hysteria/server.yaml"] \ No newline at end of file diff --git a/app/cmd/server.go b/app/cmd/server.go index 2e15a42..37eaa86 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/http" "net/http/httputil" @@ -40,6 +41,7 @@ func init() { } type serverConfig struct { + V2board *v2boardConfig `mapstructure:"v2board"` Listen string `mapstructure:"listen"` Obfs serverConfigObfs `mapstructure:"obfs"` TLS *serverConfigTLS `mapstructure:"tls"` @@ -57,6 +59,12 @@ type serverConfig struct { Masquerade serverConfigMasquerade `mapstructure:"masquerade"` } +type v2boardConfig struct { + ApiHost string `mapstructure:"apiHost"` + ApiKey string `mapstructure:"apiKey"` + NodeID uint `mapstructure:"nodeID"` +} + type serverConfigObfsSalamander struct { Password string `mapstructure:"password"` } @@ -582,6 +590,31 @@ func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error { } hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command} return nil + case "v2board": + // 定时获取用户列表并储存 + // 判断URL是否存在 + v2boardConfig := c.V2board + if v2boardConfig.ApiHost == "" || v2boardConfig.ApiKey == "" || v2boardConfig.NodeID == 0 { + return configError{Field: "auth.v2board", Err: errors.New("v2board config error")} + } + // 创建一个url.Values来存储查询参数 + queryParams := url.Values{} + + // 添加token、node_id和node_type参数 + queryParams.Add("token", v2boardConfig.ApiKey) + queryParams.Add("node_id", strconv.Itoa(int(v2boardConfig.NodeID))) + queryParams.Add("node_type", "hysteria") + + // 创建完整的URL,包括查询参数 + url := v2boardConfig.ApiHost + "/api/v1/server/UniProxy/user?" + queryParams.Encode() + + // 创建定时更新用户UUID协程 + go auth.UpdateUsers(url, time.Second*5) + + hyConfig.Authenticator = &auth.V2boardApiProvider{URL: url} + + return nil + default: return configError{Field: "auth.type", Err: errors.New("unsupported auth type")} } @@ -596,6 +629,15 @@ func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error { if c.TrafficStats.Listen != "" { tss := trafficlogger.NewTrafficStatsServer() hyConfig.TrafficLogger = tss + // 添加定时更新用户使用流量协程 + if c.V2board != nil && c.V2board.ApiHost != "" { + // 创建一个url.Values来存储查询参数 + queryParams := url.Values{} + queryParams.Add("token", c.V2board.ApiKey) + queryParams.Add("node_id", strconv.Itoa(int(c.V2board.NodeID))) + queryParams.Add("node_type", "hysteria") + go hyConfig.TrafficLogger.PushTrafficToV2boardInterval(c.V2board.ApiHost+"/api/v1/server/UniProxy/push?"+queryParams.Encode(), time.Second*60) + } go runTrafficStatsServer(c.TrafficStats.Listen, tss) } return nil @@ -707,6 +749,19 @@ func (c *serverConfig) Config() (*server.Config, error) { return hyConfig, nil } +type ResponseNodeInfo struct { + Host string `json:"host"` + ServerPort uint `json:"server_port"` + ServerName string `json:"server_name"` + UpMbps uint `json:"up_mbps"` + DownMbps uint `json:"down_mbps"` + Obfs string `json:"obfs"` + BaseConfig struct { + PushInterval int `json:"push_interval"` + PullInterval int `json:"pull_interval"` + } `json:"base_config"` +} + func runServer(cmd *cobra.Command, args []string) { logger.Info("server mode") @@ -717,11 +772,68 @@ func runServer(cmd *cobra.Command, args []string) { if err := viper.Unmarshal(&config); err != nil { logger.Fatal("failed to parse server config", zap.Error(err)) } + // 如果配置了v2board 则自动获取监听端口、obfs + if config.V2board != nil && config.V2board.ApiHost != "" { + // 创建一个url.Values来存储查询参数 + queryParams := url.Values{} + queryParams.Add("token", config.V2board.ApiKey) + queryParams.Add("node_id", strconv.Itoa(int(config.V2board.NodeID))) + queryParams.Add("node_type", "hysteria") + + // 创建完整的URL,包括查询参数 + nodeInfoUrl := config.V2board.ApiHost + "/api/v1/server/UniProxy/config?" + queryParams.Encode() + + // 发起 HTTP GET 请求 + resp, err := http.Get(nodeInfoUrl) + if err != nil { + // 处理错误 + fmt.Println("HTTP GET 请求出错:", err) + logger.Fatal("failed to client v2board api to get nodeInfo", zap.Error(err)) + } + defer resp.Body.Close() + // 读取响应数据 + body, err := io.ReadAll(resp.Body) + if err != nil { + logger.Fatal("failed to read v2board reaponse", zap.Error(err)) + } + // 解析JSON数据 + var responseNodeInfo ResponseNodeInfo + err = json.Unmarshal(body, &responseNodeInfo) + if err != nil { + logger.Fatal("failed to unmarshal v2board reaponse", zap.Error(err)) + } + // 给 hy的端口、obfs、上行下行进行赋值 + if responseNodeInfo.ServerPort != 0 { + config.Listen = ":" + strconv.Itoa(int(responseNodeInfo.ServerPort)) + } + if responseNodeInfo.DownMbps != 0 { + config.Bandwidth.Down = strconv.Itoa(int(responseNodeInfo.DownMbps)) + "Mbps" + } + if responseNodeInfo.UpMbps != 0 { + config.Bandwidth.Up = strconv.Itoa(int(responseNodeInfo.UpMbps)) + "Mbps" + } + if responseNodeInfo.Obfs != "" { + config.Obfs.Type = "salamander" + config.Obfs.Salamander.Password = responseNodeInfo.Obfs + } + + } hyConfig, err := config.Config() if err != nil { logger.Fatal("failed to load server config", zap.Error(err)) } + // // 添加定时更新用户使用流量协程 + // if config.V2board != nil && config.V2board.ApiHost != "" { + // // 创建一个url.Values来存储查询参数 + // queryParams := url.Values{} + // queryParams.Add("token", config.V2board.ApiKey) + // queryParams.Add("node_id", strconv.Itoa(int(config.V2board.NodeID))) + // queryParams.Add("node_type", "hysteria") + + // go hyConfig.TrafficLogger.PushTrafficToV2boardInterval(config.V2board.ApiHost+"/api/v1/server/UniProxy/push?"+queryParams.Encode(), time.Second*10) + // } + s, err := server.NewServer(hyConfig) if err != nil { logger.Fatal("failed to initialize server", zap.Error(err)) diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index f78b936..8cfdbcb 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -1,3 +1,8 @@ +v2board: + apiHost: http://localhost + apiKey: test + nodeID: 1 + listen: :8443 obfs: diff --git a/core/server/config.go b/core/server/config.go index f647f0d..6753018 100644 --- a/core/server/config.go +++ b/core/server/config.go @@ -196,4 +196,5 @@ type EventLogger interface { // The implementation of this interface must be thread-safe. type TrafficLogger interface { Log(id string, tx, rx uint64) (ok bool) + PushTrafficToV2boardInterval(url string, interval time.Duration) } diff --git a/extras/auth/v2board.go b/extras/auth/v2board.go new file mode 100644 index 0000000..e1f31a0 --- /dev/null +++ b/extras/auth/v2board.go @@ -0,0 +1,88 @@ +package auth + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "strconv" + "sync" + "time" + + "github.com/apernet/hysteria/core/server" +) + +var _ server.Authenticator = &V2boardApiProvider{} + +type V2boardApiProvider struct { + Client *http.Client + URL string +} + +// 用户列表 +var ( + users []User + lock sync.Mutex +) + +type User struct { + ID int `json:"id"` + UUID string `json:"uuid"` + SpeedLimit *uint32 `json:"speed_limit"` +} +type ResponseData struct { + Users []User `json:"users"` +} + +func getUserList(url string) ([]User, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var responseData ResponseData + err = json.NewDecoder(resp.Body).Decode(&responseData) + if err != nil { + return nil, err + } + + return responseData.Users, nil +} + +func UpdateUsers(url string, interval time.Duration) { + + fmt.Println("定时更新用户列表进程已开启") + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + userList, err := getUserList(url) + if err != nil { + fmt.Println("Error:", err) + continue + } + lock.Lock() + users = userList + lock.Unlock() + } + } +} + +// 验证代码 +func (v *V2boardApiProvider) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + + // 获取判断连接用户是否在用户列表内 + lock.Lock() + defer lock.Unlock() + + for _, s := range users { + if s.UUID == string(auth) { + return true, strconv.Itoa(s.ID) + } + } + return false, "" +} diff --git a/extras/trafficlogger/http.go b/extras/trafficlogger/http.go index bd22a0d..e06cc62 100644 --- a/extras/trafficlogger/http.go +++ b/extras/trafficlogger/http.go @@ -1,10 +1,14 @@ package trafficlogger import ( + "bytes" "encoding/json" + "errors" + "fmt" "net/http" "strconv" "sync" + "time" "github.com/apernet/hysteria/core/server" ) @@ -27,6 +31,69 @@ func NewTrafficStatsServer() TrafficStatsServer { } } +type TrafficPushRequest struct { + Data map[string][2]int64 +} + +// 定时提交用户流量情况 +func (s *trafficStatsServerImpl) PushTrafficToV2boardInterval(url string, interval time.Duration) { + fmt.Println("提交用户流量情况进程已开启") + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := s.PushTrafficToV2board(url); err != nil { + fmt.Println("提交用户流量情况失败:", err) + } + } + } +} + +// 向v2board 提交用户流量使用情况 +func (s *trafficStatsServerImpl) PushTrafficToV2board(url string) error { + s.Mutex.Lock() // 写锁,阻止其他操作 StatsMap 的并发访问 + defer s.Mutex.Unlock() // 确保在函数退出时释放写锁 + + // 创建一个请求对象并填充数据 + request := TrafficPushRequest{ + Data: make(map[string][2]int64), + } + for id, stats := range s.StatsMap { + request.Data[id] = [2]int64{int64(stats.Tx), int64(stats.Rx)} + } + // 如果不存在数据则跳过 + if len(request.Data) == 0 { + return nil + } + + // 将请求对象转换为JSON + jsonData, err := json.Marshal(request.Data) + if err != nil { + return err + } + + // 发起HTTP请求并提交数据 + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + fmt.Println(resp) + return err + } + defer resp.Body.Close() + + // 检查HTTP响应状态,处理错误等 + if resp.StatusCode != http.StatusOK { + return errors.New("HTTP request failed with status code: " + resp.Status) + } + + // 清空流量记录 + s.StatsMap = make(map[string]*trafficStatsEntry) + + return nil +} + type trafficStatsServerImpl struct { Mutex sync.RWMutex StatsMap map[string]*trafficStatsEntry