From f1eaef8d69152e89a33fdf921a3f8c580228c3bc Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Tue, 8 Feb 2022 23:46:54 +0800 Subject: [PATCH] add web api --- cmd/gost/main.go | 32 ++++++- go.mod | 14 ++- go.sum | 43 +++++++-- pkg/api/api.go | 86 ++++++++++++++++++ pkg/api/config.go | 53 +++++++++++ pkg/api/config_bypass.go | 158 ++++++++++++++++++++++++++++++++ pkg/api/config_chain.go | 167 ++++++++++++++++++++++++++++++++++ pkg/api/config_hosts.go | 158 ++++++++++++++++++++++++++++++++ pkg/api/config_resolver.go | 166 ++++++++++++++++++++++++++++++++++ pkg/api/config_service.go | 181 +++++++++++++++++++++++++++++++++++++ pkg/api/doc.go | 17 ++++ pkg/api/error.go | 44 +++++++++ pkg/chain/resovle.go | 9 +- pkg/config/config.go | 27 ++++++ pkg/logger/gost_logger.go | 4 + pkg/registry/bypass.go | 17 +++- pkg/registry/chain.go | 17 +++- pkg/registry/hosts.go | 17 +++- pkg/registry/registry.go | 3 +- pkg/registry/resolver.go | 19 +++- pkg/registry/service.go | 5 + pkg/resolver/resolver.go | 5 + 22 files changed, 1209 insertions(+), 33 deletions(-) create mode 100644 pkg/api/api.go create mode 100644 pkg/api/config.go create mode 100644 pkg/api/config_bypass.go create mode 100644 pkg/api/config_chain.go create mode 100644 pkg/api/config_hosts.go create mode 100644 pkg/api/config_resolver.go create mode 100644 pkg/api/config_service.go create mode 100644 pkg/api/doc.go create mode 100644 pkg/api/error.go diff --git a/cmd/gost/main.go b/cmd/gost/main.go index 225b294..ce80558 100644 --- a/cmd/gost/main.go +++ b/cmd/gost/main.go @@ -3,11 +3,13 @@ package main import ( "flag" "fmt" + "net" "net/http" _ "net/http/pprof" "os" "runtime" + "github.com/go-gost/gost/pkg/api" "github.com/go-gost/gost/pkg/config" "github.com/go-gost/gost/pkg/logger" ) @@ -20,6 +22,7 @@ var ( services stringList nodes stringList debug bool + apiAddr string ) func init() { @@ -29,8 +32,9 @@ func init() { flag.Var(&nodes, "F", "chain node list") flag.StringVar(&cfgFile, "C", "", "configure file") flag.BoolVar(&printVersion, "V", false, "print version") - flag.BoolVar(&debug, "D", false, "debug mode") flag.StringVar(&outputFormat, "O", "", "output format, one of yaml|json format") + flag.BoolVar(&debug, "D", false, "debug mode") + flag.StringVar(&apiAddr, "api", "", "api server addr") flag.Parse() if printVersion { @@ -43,7 +47,7 @@ func init() { func main() { cfg := &config.Config{} var err error - if len(services) > 0 { + if len(services) > 0 || apiAddr != "" { cfg, err = buildConfigFromCmd(services, nodes) if debug && cfg != nil { if cfg.Log == nil { @@ -51,6 +55,11 @@ func main() { } cfg.Log.Level = string(logger.DebugLevel) } + if apiAddr != "" { + cfg.API = &config.APIConfig{ + Addr: apiAddr, + } + } } else { if cfgFile != "" { err = cfg.ReadFile(cfgFile) @@ -64,6 +73,8 @@ func main() { log = logFromConfig(cfg.Log) + logger.SetDefault(log) + if outputFormat != "" { if err := cfg.Write(os.Stdout, outputFormat); err != nil { log.Fatal(err) @@ -77,11 +88,24 @@ func main() { if addr == "" { addr = ":6060" } - log.Info("profiling serve on ", addr) + log.Info("profiling server on ", addr) log.Fatal(http.ListenAndServe(addr, nil)) }() } + if cfg.API != nil && cfg.API.Addr != "" { + ln, err := net.Listen("tcp", cfg.API.Addr) + if err != nil { + log.Fatal(err) + } + defer ln.Close() + + go func() { + log.Info("api server on ", ln.Addr()) + log.Fatal(api.Run(ln)) + }() + } + buildDefaultTLSConfig(cfg.TLS) services := buildService(cfg) @@ -89,5 +113,7 @@ func main() { go svc.Run() } + config.SetGlobal(cfg) + select {} } diff --git a/go.mod b/go.mod index 91b6b12..a79c021 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/coreos/go-iptables v0.5.0 // indirect github.com/docker/libcontainer v2.2.1+incompatible github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/gin-contrib/cors v1.3.1 + github.com/gin-gonic/gin v1.7.7 github.com/go-gost/gosocks4 v0.0.1 github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09 github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7 @@ -60,14 +62,24 @@ require ( golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/grpc v1.44.0 + google.golang.org/protobuf v1.27.1 gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/marten-seemann/qpack v0.2.1 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect - google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index 8f1227e..54dd5f7 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,13 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= +github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -127,6 +134,16 @@ github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7 h1:itaaJhQJ19kUXEB github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8= github.com/go-gost/tls-dissector v0.0.2-0.20211125135007-2b5d5bd9c07e h1:73NGqAs22ey3wJkIYVD/ACEoovuIuOlEzQTEoqrO5+U= github.com/go-gost/tls-dissector v0.0.2-0.20211125135007-2b5d5bd9c07e/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -247,6 +264,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -265,6 +285,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lucas-clemente/quic-go v0.24.0 h1:ToR7SIIEdrgOhgVTHvPgdVRJfgVy+N0wQAagH7L4d5g= github.com/lucas-clemente/quic-go v0.24.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -283,8 +306,10 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= @@ -304,8 +329,10 @@ github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 h1:ULR/QWMgcgRiZLUjSSJMU+fW+RDMstRdmnDWj9Q+AsA= github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= @@ -411,6 +438,10 @@ github.com/templexxx/xorsimd v0.4.1 h1:iUZcywbOYDRAZUasAs2eSCUW8eobuZDy0I9FJiORk github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/xtaci/kcp-go/v5 v5.6.1 h1:Pwn0aoeNSPF9dTS7IgiPXn0HEtaIlVb6y5UKWPsx8bI= @@ -543,8 +574,6 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -592,6 +621,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -640,14 +670,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -655,7 +685,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -823,7 +852,6 @@ google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71 h1:z+ErRPu0+KS02Td3fOAgdX+lnPDh/VyaABEJPD4JRQs= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 h1:YxHp5zqIcAShDEvRr5/0rVESVS+njYF68PSdazrNLJo= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -854,7 +882,6 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= @@ -879,6 +906,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..5e0a76d --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,86 @@ +package api + +import ( + "net" + "net/http" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/logger" +) + +var ( + apiServer = &http.Server{} +) + +func init() { + gin.SetMode(gin.ReleaseMode) + + r := gin.New() + r.Use( + cors.New((cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowHeaders: []string{"*"}, + })), + loggerHandler, + gin.Recovery(), + ) + + r.StaticFile("/swagger.yaml", "swagger.yaml") + + config := r.Group("/config") + { + config.GET("", getConfig) + + config.POST("/services", createService) + config.PUT("/services/:service", updateService) + config.DELETE("/services/:service", deleteService) + + config.POST("/chains", createChain) + config.PUT("/chains/:chain", updateChain) + config.DELETE("/chains/:chain", deleteChain) + + config.POST("/bypasses", createBypass) + config.PUT("/bypasses/:bypass", updateBypass) + config.DELETE("/bypasses/:bypass", deleteBypass) + + config.POST("/resolvers", createResolver) + config.PUT("/resolvers/:resolver", updateResolver) + config.DELETE("/resolvers/:resolver", deleteResolver) + + config.POST("/hosts", createHosts) + config.PUT("/hosts/:hosts", updateHosts) + config.DELETE("/hosts/:hosts", deleteHosts) + } + + apiServer.Handler = r +} + +type Response struct { + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` +} + +func Run(ln net.Listener) error { + return apiServer.Serve(ln) +} + +func loggerHandler(ctx *gin.Context) { + // start time + startTime := time.Now() + // Processing request + ctx.Next() + duration := time.Since(startTime) + + logger.Default().WithFields(map[string]interface{}{ + "kind": "api", + "method": ctx.Request.Method, + "uri": ctx.Request.RequestURI, + "code": ctx.Writer.Status(), + "client": ctx.ClientIP(), + "duration": duration, + }).Infof("| %3d | %13v | %15s | %-7s %s", + ctx.Writer.Status(), duration, ctx.ClientIP(), ctx.Request.Method, ctx.Request.RequestURI) +} diff --git a/pkg/api/config.go b/pkg/api/config.go new file mode 100644 index 0000000..b4f976b --- /dev/null +++ b/pkg/api/config.go @@ -0,0 +1,53 @@ +package api + +import ( + "bytes" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/config" +) + +// swagger:parameters getConfigRequest +type getConfigRequest struct { + // output format, one of yaml|json, default is json. + // in: query + Format string `form:"format" json:"format"` +} + +// successful operation. +// swagger:response getConfigResponse +type getConfigResponse struct { + Config *config.Config +} + +func getConfig(ctx *gin.Context) { + // swagger:route GET /config ConfigManagement getConfigRequest + // + // Get current config. + // + // Responses: + // 200: getConfigResponse + + var req getConfigRequest + ctx.ShouldBindQuery(&req) + + var resp getConfigResponse + resp.Config = config.Global() + + buf := &bytes.Buffer{} + switch req.Format { + case "yaml": + default: + req.Format = "json" + } + + resp.Config.Write(buf, req.Format) + + contentType := "application/json" + if req.Format == "yaml" { + contentType = "text/x-yaml" + } + + ctx.Data(http.StatusOK, contentType, buf.Bytes()) +} diff --git a/pkg/api/config_bypass.go b/pkg/api/config_bypass.go new file mode 100644 index 0000000..6a9d918 --- /dev/null +++ b/pkg/api/config_bypass.go @@ -0,0 +1,158 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/config" + "github.com/go-gost/gost/pkg/config/parsing" + "github.com/go-gost/gost/pkg/registry" +) + +// swagger:parameters createBypassRequest +type createBypassRequest struct { + // in: body + Data config.BypassConfig `json:"data"` +} + +// successful operation. +// swagger:response createBypassResponse +type createBypassResponse struct { + Data Response +} + +func createBypass(ctx *gin.Context) { + // swagger:route POST /config/bypasses ConfigManagement createBypassRequest + // + // create a new bypass, the name of bypass must be unique in bypass list. + // + // Responses: + // 200: createBypassResponse + + var req createBypassRequest + ctx.ShouldBindJSON(&req.Data) + + if req.Data.Name == "" { + writeError(ctx, ErrInvalid) + return + } + + v := parsing.ParseBypass(&req.Data) + + if err := registry.Bypass().Register(req.Data.Name, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + cfg.Bypasses = append(cfg.Bypasses, &req.Data) + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters updateBypassRequest +type updateBypassRequest struct { + // in: path + // required: true + Bypass string `uri:"bypass" json:"bypass"` + // in: body + Data config.BypassConfig `json:"data"` +} + +// successful operation. +// swagger:response updateBypassResponse +type updateBypassResponse struct { + Data Response +} + +func updateBypass(ctx *gin.Context) { + // swagger:route PUT /config/bypasses/{bypass} ConfigManagement updateBypassRequest + // + // update bypass by name, the bypass must already exist. + // + // Responses: + // 200: updateBypassResponse + + var req updateBypassRequest + ctx.ShouldBindUri(&req) + ctx.ShouldBindJSON(&req.Data) + + if !registry.Bypass().IsRegistered(req.Bypass) { + writeError(ctx, ErrNotFound) + return + } + + req.Data.Name = req.Bypass + + v := parsing.ParseBypass(&req.Data) + + registry.Bypass().Unregister(req.Bypass) + + if err := registry.Bypass().Register(req.Bypass, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + for i := range cfg.Bypasses { + if cfg.Bypasses[i].Name == req.Bypass { + cfg.Bypasses[i] = &req.Data + break + } + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters deleteBypassRequest +type deleteBypassRequest struct { + // in: path + // required: true + Bypass string `uri:"bypass" json:"bypass"` +} + +// successful operation. +// swagger:response deleteBypassResponse +type deleteBypassResponse struct { + Data Response +} + +func deleteBypass(ctx *gin.Context) { + // swagger:route DELETE /config/bypasses/{bypass} ConfigManagement deleteBypassRequest + // + // delete bypass by name. + // + // Responses: + // 200: deleteBypassResponse + + var req deleteBypassRequest + ctx.ShouldBindUri(&req) + + svc := registry.Bypass().Get(req.Bypass) + if svc == nil { + writeError(ctx, ErrNotFound) + return + } + registry.Bypass().Unregister(req.Bypass) + + cfg := config.Global() + bypasses := cfg.Bypasses + cfg.Bypasses = nil + for _, s := range bypasses { + if s.Name == req.Bypass { + continue + } + cfg.Bypasses = append(cfg.Bypasses, s) + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} diff --git a/pkg/api/config_chain.go b/pkg/api/config_chain.go new file mode 100644 index 0000000..f4a3a56 --- /dev/null +++ b/pkg/api/config_chain.go @@ -0,0 +1,167 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/config" + "github.com/go-gost/gost/pkg/config/parsing" + "github.com/go-gost/gost/pkg/registry" +) + +// swagger:parameters createChainRequest +type createChainRequest struct { + // in: body + Data config.ChainConfig `json:"data"` +} + +// successful operation. +// swagger:response createChainResponse +type createChainResponse struct { + Data Response +} + +func createChain(ctx *gin.Context) { + // swagger:route POST /config/chains ConfigManagement createChainRequest + // + // create a new chain, the name of chain must be unique in chain list. + // + // Responses: + // 200: createChainResponse + + var req createChainRequest + ctx.ShouldBindJSON(&req.Data) + + if req.Data.Name == "" { + writeError(ctx, ErrInvalid) + return + } + + v, err := parsing.ParseChain(&req.Data) + if err != nil { + writeError(ctx, ErrCreate) + return + } + + if err := registry.Chain().Register(req.Data.Name, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + cfg.Chains = append(cfg.Chains, &req.Data) + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters updateChainRequest +type updateChainRequest struct { + // in: path + // required: true + // chain name + Chain string `uri:"chain" json:"chain"` + // in: body + Data config.ChainConfig `json:"data"` +} + +// successful operation. +// swagger:response updateChainResponse +type updateChainResponse struct { + Data Response +} + +func updateChain(ctx *gin.Context) { + // swagger:route PUT /config/chains/{chain} ConfigManagement updateChainRequest + // + // update chain by name, the chain must already exist. + // + // Responses: + // 200: updateChainResponse + + var req updateChainRequest + ctx.ShouldBindUri(&req) + ctx.ShouldBindJSON(&req.Data) + + if !registry.Chain().IsRegistered(req.Chain) { + writeError(ctx, ErrNotFound) + return + } + + req.Data.Name = req.Chain + + v, err := parsing.ParseChain(&req.Data) + if err != nil { + writeError(ctx, ErrCreate) + return + } + + registry.Chain().Unregister(req.Chain) + + if err := registry.Chain().Register(req.Chain, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + for i := range cfg.Chains { + if cfg.Chains[i].Name == req.Chain { + cfg.Chains[i] = &req.Data + break + } + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters deleteChainRequest +type deleteChainRequest struct { + // in: path + // required: true + Chain string `uri:"chain" json:"chain"` +} + +// successful operation. +// swagger:response deleteChainResponse +type deleteChainResponse struct { + Data Response +} + +func deleteChain(ctx *gin.Context) { + // swagger:route DELETE /config/chains/{chain} ConfigManagement deleteChainRequest + // + // delete chain by name. + // + // Responses: + // 200: deleteChainResponse + + var req deleteChainRequest + ctx.ShouldBindUri(&req) + + svc := registry.Chain().Get(req.Chain) + if svc == nil { + writeError(ctx, ErrNotFound) + return + } + registry.Chain().Unregister(req.Chain) + + cfg := config.Global() + chains := cfg.Chains + cfg.Chains = nil + for _, s := range chains { + if s.Name == req.Chain { + continue + } + cfg.Chains = append(cfg.Chains, s) + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} diff --git a/pkg/api/config_hosts.go b/pkg/api/config_hosts.go new file mode 100644 index 0000000..b45d7cd --- /dev/null +++ b/pkg/api/config_hosts.go @@ -0,0 +1,158 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/config" + "github.com/go-gost/gost/pkg/config/parsing" + "github.com/go-gost/gost/pkg/registry" +) + +// swagger:parameters createHostsRequest +type createHostsRequest struct { + // in: body + Data config.HostsConfig `json:"data"` +} + +// successful operation. +// swagger:response createHostsResponse +type createHostsesponse struct { + Data Response +} + +func createHosts(ctx *gin.Context) { + // swagger:route POST /config/hosts ConfigManagement createHostsRequest + // + // create a new hosts, the name of the hosts must be unique in hosts list. + // + // Responses: + // 200: createHostsResponse + + var req createHostsRequest + ctx.ShouldBindJSON(&req.Data) + + if req.Data.Name == "" { + writeError(ctx, ErrInvalid) + return + } + + v := parsing.ParseHosts(&req.Data) + + if err := registry.Hosts().Register(req.Data.Name, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + cfg.Hosts = append(cfg.Hosts, &req.Data) + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters updateHostsRequest +type updateHostsRequest struct { + // in: path + // required: true + Hosts string `uri:"hosts" json:"hosts"` + // in: body + Data config.HostsConfig `json:"data"` +} + +// successful operation. +// swagger:response updateHostsResponse +type updateHostsResponse struct { + Data Response +} + +func updateHosts(ctx *gin.Context) { + // swagger:route PUT /config/hosts/{hosts} ConfigManagement updateHostsRequest + // + // update hosts by name, the hosts must already exist. + // + // Responses: + // 200: updateHostsResponse + + var req updateHostsRequest + ctx.ShouldBindUri(&req) + ctx.ShouldBindJSON(&req.Data) + + if !registry.Hosts().IsRegistered(req.Hosts) { + writeError(ctx, ErrNotFound) + return + } + + req.Data.Name = req.Hosts + + v := parsing.ParseHosts(&req.Data) + + registry.Hosts().Unregister(req.Hosts) + + if err := registry.Hosts().Register(req.Hosts, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + for i := range cfg.Hosts { + if cfg.Hosts[i].Name == req.Hosts { + cfg.Hosts[i] = &req.Data + break + } + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters deleteHostsRequest +type deleteHostsRequest struct { + // in: path + // required: true + Hosts string `uri:"hosts" json:"hosts"` +} + +// successful operation. +// swagger:response deleteHostsResponse +type deleteHostsResponse struct { + Data Response +} + +func deleteHosts(ctx *gin.Context) { + // swagger:route DELETE /config/hosts/{hosts} ConfigManagement deleteHostsRequest + // + // delete hosts by name. + // + // Responses: + // 200: deleteHostsResponse + + var req deleteHostsRequest + ctx.ShouldBindUri(&req) + + svc := registry.Hosts().Get(req.Hosts) + if svc == nil { + writeError(ctx, ErrNotFound) + return + } + registry.Hosts().Unregister(req.Hosts) + + cfg := config.Global() + hosts := cfg.Hosts + cfg.Hosts = nil + for _, s := range hosts { + if s.Name == req.Hosts { + continue + } + cfg.Hosts = append(cfg.Hosts, s) + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} diff --git a/pkg/api/config_resolver.go b/pkg/api/config_resolver.go new file mode 100644 index 0000000..6b4caf0 --- /dev/null +++ b/pkg/api/config_resolver.go @@ -0,0 +1,166 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/config" + "github.com/go-gost/gost/pkg/config/parsing" + "github.com/go-gost/gost/pkg/registry" +) + +// swagger:parameters createResolverRequest +type createResolverRequest struct { + // in: body + Data config.ResolverConfig `json:"data"` +} + +// successful operation. +// swagger:response createResolverResponse +type createResolverResponse struct { + Data Response +} + +func createResolver(ctx *gin.Context) { + // swagger:route POST /config/resolvers ConfigManagement createResolverRequest + // + // create a new resolver, the name of the resolver must be unique in resolver list. + // + // Responses: + // 200: createResolverResponse + + var req createResolverRequest + ctx.ShouldBindJSON(&req.Data) + + if req.Data.Name == "" { + writeError(ctx, ErrInvalid) + return + } + + v, err := parsing.ParseResolver(&req.Data) + if err != nil { + writeError(ctx, ErrCreate) + return + } + + if err := registry.Resolver().Register(req.Data.Name, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + cfg.Resolvers = append(cfg.Resolvers, &req.Data) + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters updateResolverRequest +type updateResolverRequest struct { + // in: path + // required: true + Resolver string `uri:"resolver" json:"resolver"` + // in: body + Data config.ResolverConfig `json:"data"` +} + +// successful operation. +// swagger:response updateResolverResponse +type updateResolverResponse struct { + Data Response +} + +func updateResolver(ctx *gin.Context) { + // swagger:route PUT /config/resolvers/{resolver} ConfigManagement updateResolverRequest + // + // update resolver by name, the resolver must already exist. + // + // Responses: + // 200: updateResolverResponse + + var req updateResolverRequest + ctx.ShouldBindUri(&req) + ctx.ShouldBindJSON(&req.Data) + + if !registry.Resolver().IsRegistered(req.Resolver) { + writeError(ctx, ErrNotFound) + return + } + + req.Data.Name = req.Resolver + + v, err := parsing.ParseResolver(&req.Data) + if err != nil { + writeError(ctx, ErrCreate) + return + } + + registry.Resolver().Unregister(req.Resolver) + + if err := registry.Resolver().Register(req.Resolver, v); err != nil { + writeError(ctx, ErrDup) + return + } + + cfg := config.Global() + for i := range cfg.Resolvers { + if cfg.Resolvers[i].Name == req.Resolver { + cfg.Resolvers[i] = &req.Data + break + } + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters deleteResolverRequest +type deleteResolverRequest struct { + // in: path + // required: true + Resolver string `uri:"resolver" json:"resolver"` +} + +// successful operation. +// swagger:response deleteResolverResponse +type deleteResolverResponse struct { + Data Response +} + +func deleteResolver(ctx *gin.Context) { + // swagger:route DELETE /config/resolvers/{resolver} ConfigManagement deleteResolverRequest + // + // delete resolver by name. + // + // Responses: + // 200: deleteResolverResponse + + var req deleteResolverRequest + ctx.ShouldBindUri(&req) + + svc := registry.Resolver().Get(req.Resolver) + if svc == nil { + writeError(ctx, ErrNotFound) + return + } + registry.Resolver().Unregister(req.Resolver) + + cfg := config.Global() + resolvers := cfg.Resolvers + cfg.Resolvers = nil + for _, s := range resolvers { + if s.Name == req.Resolver { + continue + } + cfg.Resolvers = append(cfg.Resolvers, s) + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} diff --git a/pkg/api/config_service.go b/pkg/api/config_service.go new file mode 100644 index 0000000..7a37847 --- /dev/null +++ b/pkg/api/config_service.go @@ -0,0 +1,181 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/gost/pkg/config" + "github.com/go-gost/gost/pkg/config/parsing" + "github.com/go-gost/gost/pkg/registry" +) + +// swagger:parameters createServiceRequest +type createServiceRequest struct { + // in: body + Data config.ServiceConfig `json:"data"` +} + +// successful operation. +// swagger:response createServiceResponse +type createServiceResponse struct { + Data Response +} + +func createService(ctx *gin.Context) { + // swagger:route POST /config/services ConfigManagement createServiceRequest + // + // create a new service, the name of the service must be unique in service list. + // + // Responses: + // 200: createServiceResponse + + var req createServiceRequest + ctx.ShouldBindJSON(&req.Data) + + if req.Data.Name == "" { + writeError(ctx, ErrInvalid) + return + } + + if registry.Service().IsRegistered(req.Data.Name) { + writeError(ctx, ErrDup) + return + } + + svc, err := parsing.ParseService(&req.Data) + if err != nil { + writeError(ctx, ErrCreate) + return + } + + if err := registry.Service().Register(req.Data.Name, svc); err != nil { + svc.Close() + writeError(ctx, ErrDup) + return + } + + go svc.Run() + + cfg := config.Global() + cfg.Services = append(cfg.Services, &req.Data) + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters updateServiceRequest +type updateServiceRequest struct { + // in: path + // required: true + Service string `uri:"service" json:"service"` + // in: body + Data config.ServiceConfig `json:"data"` +} + +// successful operation. +// swagger:response updateServiceResponse +type updateServiceResponse struct { + Data Response +} + +func updateService(ctx *gin.Context) { + // swagger:route PUT /config/services/{service} ConfigManagement updateServiceRequest + // + // update service by name, the service must already exist. + // + // Responses: + // 200: updateServiceResponse + + var req updateServiceRequest + ctx.ShouldBindUri(&req) + ctx.ShouldBindJSON(&req.Data) + + old := registry.Service().Get(req.Service) + if old == nil { + writeError(ctx, ErrNotFound) + return + } + old.Close() + + req.Data.Name = req.Service + + svc, err := parsing.ParseService(&req.Data) + if err != nil { + writeError(ctx, ErrCreate) + return + } + + registry.Service().Unregister(req.Service) + + if err := registry.Service().Register(req.Service, svc); err != nil { + svc.Close() + writeError(ctx, ErrDup) + return + } + + go svc.Run() + + cfg := config.Global() + for i := range cfg.Services { + if cfg.Services[i].Name == req.Service { + cfg.Services[i] = &req.Data + break + } + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} + +// swagger:parameters deleteServiceRequest +type deleteServiceRequest struct { + // in: path + // required: true + Service string `uri:"service" json:"service"` +} + +// successful operation. +// swagger:response deleteServiceResponse +type deleteServiceResponse struct { + Data Response +} + +func deleteService(ctx *gin.Context) { + // swagger:route DELETE /config/services/{service} ConfigManagement deleteServiceRequest + // + // delete service by name. + // + // Responses: + // 200: deleteServiceResponse + + var req deleteServiceRequest + ctx.ShouldBindUri(&req) + + svc := registry.Service().Get(req.Service) + if svc == nil { + writeError(ctx, ErrNotFound) + return + } + + registry.Service().Unregister(req.Service) + svc.Close() + + cfg := config.Global() + services := cfg.Services + cfg.Services = nil + for _, s := range services { + if s.Name == req.Service { + continue + } + cfg.Services = append(cfg.Services, s) + } + config.SetGlobal(cfg) + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} diff --git a/pkg/api/doc.go b/pkg/api/doc.go new file mode 100644 index 0000000..095da4f --- /dev/null +++ b/pkg/api/doc.go @@ -0,0 +1,17 @@ +// Documentation of GOST Web API. +// +// Schemes: http +// BasePath: / +// Version: 1.0.0 +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// SecurityDefinitions: +// api_key: +// +// swagger:meta +package api diff --git a/pkg/api/error.go b/pkg/api/error.go new file mode 100644 index 0000000..44340cb --- /dev/null +++ b/pkg/api/error.go @@ -0,0 +1,44 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" +) + +var ( + ErrInvalid = &Error{statusCode: http.StatusBadRequest, Code: 40001, Msg: "instance invalid"} + ErrDup = &Error{statusCode: http.StatusBadRequest, Code: 40002, Msg: "instance duplicated"} + ErrCreate = &Error{statusCode: http.StatusConflict, Code: 40003, Msg: "instance creation failed"} + ErrNotFound = &Error{statusCode: http.StatusBadRequest, Code: 40004, Msg: "instance not found"} +) + +// Error is an api error. +type Error struct { + statusCode int + Code int `json:"code"` + Msg string `json:"msg"` +} + +func (e *Error) Error() string { + b, _ := json.Marshal(e) + return string(b) +} + +func writeError(c *gin.Context, err error) { + // c.Set(HTTPResponseTag, err) + c.JSON(getStatusCode(err), err) +} + +func getStatusCode(err error) int { + if err == nil { + return http.StatusOK + } + if e, ok := err.(*Error); ok { + if e.statusCode >= http.StatusOK && e.statusCode < 600 { + return e.statusCode + } + } + return http.StatusInternalServerError +} diff --git a/pkg/chain/resovle.go b/pkg/chain/resovle.go index bad7573..59fe4a4 100644 --- a/pkg/chain/resovle.go +++ b/pkg/chain/resovle.go @@ -10,7 +10,7 @@ import ( "github.com/go-gost/gost/pkg/resolver" ) -func resolve(ctx context.Context, network, addr string, resolver resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) { +func resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) { if addr == "" { return addr, nil } @@ -30,9 +30,12 @@ func resolve(ctx context.Context, network, addr string, resolver resolver.Resolv } } - if resolver != nil { - ips, err := resolver.Resolve(ctx, network, host) + if r != nil { + ips, err := r.Resolve(ctx, network, host) if err != nil { + if err == resolver.ErrInvalid { + return addr, nil + } log.Error(err) } if len(ips) == 0 { diff --git a/pkg/config/config.go b/pkg/config/config.go index b8c119f..db7ae3e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "io" + "sync" "time" "github.com/spf13/viper" @@ -20,6 +21,27 @@ func init() { v.AddConfigPath(".") } +var ( + global = &Config{} + globalMux sync.RWMutex +) + +func Global() *Config { + globalMux.RLock() + defer globalMux.RUnlock() + + cfg := &Config{} + *cfg = *global + return cfg +} + +func SetGlobal(c *Config) { + globalMux.Lock() + defer globalMux.Unlock() + + global = c +} + type LogConfig struct { Output string `yaml:",omitempty" json:"output,omitempty"` Level string `yaml:",omitempty" json:"level,omitempty"` @@ -31,6 +53,10 @@ type ProfilingConfig struct { Enabled bool `json:"enabled"` } +type APIConfig struct { + Addr string `json:"addr"` +} + type TLSConfig struct { CertFile string `yaml:"certFile,omitempty" json:"certFile,omitempty"` KeyFile string `yaml:"keyFile,omitempty" json:"keyFile,omitempty"` @@ -163,6 +189,7 @@ type Config struct { TLS *TLSConfig `yaml:",omitempty" json:"tls,omitempty"` Log *LogConfig `yaml:",omitempty" json:"log,omitempty"` Profiling *ProfilingConfig `yaml:",omitempty" json:"profiling,omitempty"` + API *APIConfig `yaml:",omitempty" json:"api,omitempty"` } func (c *Config) Load() error { diff --git a/pkg/logger/gost_logger.go b/pkg/logger/gost_logger.go index 773ae11..4cc7a4c 100644 --- a/pkg/logger/gost_logger.go +++ b/pkg/logger/gost_logger.go @@ -16,6 +16,10 @@ func Default() Logger { return defaultLogger } +func SetDefault(logger Logger) { + defaultLogger = logger +} + type logger struct { logger *logrus.Entry } diff --git a/pkg/registry/bypass.go b/pkg/registry/bypass.go index b44b221..06cee2a 100644 --- a/pkg/registry/bypass.go +++ b/pkg/registry/bypass.go @@ -30,19 +30,28 @@ func (r *bypassRegistry) Unregister(name string) { r.m.Delete(name) } +func (r *bypassRegistry) IsRegistered(name string) bool { + _, ok := r.m.Load(name) + return ok +} + func (r *bypassRegistry) Get(name string) bypass.Bypass { - if _, ok := r.m.Load(name); !ok { - return nil - } return &bypassWrapper{name: name} } +func (r *bypassRegistry) get(name string) bypass.Bypass { + if v, ok := r.m.Load(name); ok { + return v.(bypass.Bypass) + } + return nil +} + type bypassWrapper struct { name string } func (w *bypassWrapper) Contains(addr string) bool { - bp := bypassReg.Get(w.name) + bp := bypassReg.get(w.name) if bp == nil { return false } diff --git a/pkg/registry/chain.go b/pkg/registry/chain.go index 77ad3d8..53096bb 100644 --- a/pkg/registry/chain.go +++ b/pkg/registry/chain.go @@ -30,19 +30,28 @@ func (r *chainRegistry) Unregister(name string) { r.m.Delete(name) } +func (r *chainRegistry) IsRegistered(name string) bool { + _, ok := r.m.Load(name) + return ok +} + func (r *chainRegistry) Get(name string) chain.Chainer { - if _, ok := r.m.Load(name); !ok { - return nil - } return &chainWrapper{name: name} } +func (r *chainRegistry) get(name string) chain.Chainer { + if v, ok := r.m.Load(name); ok { + return v.(chain.Chainer) + } + return nil +} + type chainWrapper struct { name string } func (w *chainWrapper) Route(network, address string) *chain.Route { - v := Chain().Get(w.name) + v := Chain().get(w.name) if v == nil { return nil } diff --git a/pkg/registry/hosts.go b/pkg/registry/hosts.go index b4685f8..3aea9ae 100644 --- a/pkg/registry/hosts.go +++ b/pkg/registry/hosts.go @@ -31,19 +31,28 @@ func (r *hostsRegistry) Unregister(name string) { r.m.Delete(name) } +func (r *hostsRegistry) IsRegistered(name string) bool { + _, ok := r.m.Load(name) + return ok +} + func (r *hostsRegistry) Get(name string) hosts.HostMapper { - if _, ok := r.m.Load(name); !ok { - return nil - } return &hostsWrapper{name: name} } +func (r *hostsRegistry) get(name string) hosts.HostMapper { + if v, ok := r.m.Load(name); ok { + return v.(hosts.HostMapper) + } + return nil +} + type hostsWrapper struct { name string } func (w *hostsWrapper) Lookup(network, host string) ([]net.IP, bool) { - v := Hosts().Get(w.name) + v := Hosts().get(w.name) if v == nil { return nil, false } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index b9aa7b0..5c0f494 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -11,8 +11,7 @@ import ( ) var ( - ErrDup = errors.New("registry: duplicate instance") - ErrNotFound = errors.New("registry: instance not found") + ErrDup = errors.New("registry: duplicate instance") ) type NewListener func(opts ...listener.Option) listener.Listener diff --git a/pkg/registry/resolver.go b/pkg/registry/resolver.go index 71af7fe..ad6c089 100644 --- a/pkg/registry/resolver.go +++ b/pkg/registry/resolver.go @@ -32,21 +32,30 @@ func (r *resolverRegistry) Unregister(name string) { r.m.Delete(name) } +func (r *resolverRegistry) IsRegistered(name string) bool { + _, ok := r.m.Load(name) + return ok +} + func (r *resolverRegistry) Get(name string) resolver.Resolver { - if _, ok := r.m.Load(name); !ok { - return nil - } return &resolverWrapper{name: name} } +func (r *resolverRegistry) get(name string) resolver.Resolver { + if v, ok := r.m.Load(name); ok { + return v.(resolver.Resolver) + } + return nil +} + type resolverWrapper struct { name string } func (w *resolverWrapper) Resolve(ctx context.Context, network, host string) ([]net.IP, error) { - r := Resolver().Get(w.name) + r := Resolver().get(w.name) if r == nil { - return nil, ErrNotFound + return nil, resolver.ErrInvalid } return r.Resolve(ctx, network, host) } diff --git a/pkg/registry/service.go b/pkg/registry/service.go index a0a2e2e..7c45f34 100644 --- a/pkg/registry/service.go +++ b/pkg/registry/service.go @@ -30,6 +30,11 @@ func (r *serviceRegistry) Unregister(name string) { r.m.Delete(name) } +func (r *serviceRegistry) IsRegistered(name string) bool { + _, ok := r.m.Load(name) + return ok +} + func (r *serviceRegistry) Get(name string) *service.Service { v, ok := r.m.Load(name) if !ok { diff --git a/pkg/resolver/resolver.go b/pkg/resolver/resolver.go index df2d0c6..5cc959d 100644 --- a/pkg/resolver/resolver.go +++ b/pkg/resolver/resolver.go @@ -2,9 +2,14 @@ package resolver import ( "context" + "errors" "net" ) +var ( + ErrInvalid = errors.New("resolver invalid") +) + type Resolver interface { // Resolve returns a slice of the host's IPv4 and IPv6 addresses. // The network should be 'ip', 'ip4' or 'ip6', default network is 'ip'.