diff --git a/api/api.go b/api/api.go index fb44de6d..51e1304b 100644 --- a/api/api.go +++ b/api/api.go @@ -2,13 +2,11 @@ package api import ( "embed" - "net" "net/http" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/go-gost/core/auth" - "github.com/go-gost/core/service" ) var ( @@ -21,55 +19,17 @@ type Response struct { Msg string `json:"msg,omitempty"` } -type options struct { - accessLog bool - pathPrefix string - auther auth.Authenticator +type Options struct { + AccessLog bool + PathPrefix string + Auther auth.Authenticator } -type Option func(*options) - -func PathPrefixOption(pathPrefix string) Option { - return func(o *options) { - o.pathPrefix = pathPrefix - } -} - -func AccessLogOption(enable bool) Option { - return func(o *options) { - o.accessLog = enable - } -} - -func AutherOption(auther auth.Authenticator) Option { - return func(o *options) { - o.auther = auther - } -} - -type server struct { - s *http.Server - ln net.Listener - cclose chan struct{} -} - -func NewService(network, addr string, opts ...Option) (service.Service, error) { - if network == "" { - network = "tcp" - } - ln, err := net.Listen(network, addr) - if err != nil { - return nil, err +func Register(r *gin.Engine, opts *Options) { + if opts == nil { + opts = &Options{} } - var options options - for _, opt := range opts { - opt(&options) - } - - gin.SetMode(gin.ReleaseMode) - - r := gin.New() r.Use( cors.New((cors.Config{ AllowAllOrigins: true, @@ -79,55 +39,25 @@ func NewService(network, addr string, opts ...Option) (service.Service, error) { })), gin.Recovery(), ) - if options.accessLog { + if opts.AccessLog { r.Use(mwLogger()) } router := r.Group("") - if options.pathPrefix != "" { - router = router.Group(options.pathPrefix) + if opts.PathPrefix != "" { + router = router.Group(opts.PathPrefix) } router.StaticFS("/docs", http.FS(swaggerDoc)) config := router.Group("/config") - config.Use(mwBasicAuth(options.auther)) - registerConfig(config) + config.Use(mwBasicAuth(opts.Auther)) - return &server{ - s: &http.Server{ - Handler: r, - }, - ln: ln, - cclose: make(chan struct{}), - }, nil -} - -func (s *server) Serve() error { - return s.s.Serve(s.ln) -} - -func (s *server) Addr() net.Addr { - return s.ln.Addr() -} - -func (s *server) Close() error { - return s.s.Close() -} - -func (s *server) IsClosed() bool { - select { - case <-s.cclose: - return true - default: - return false - } -} - -func registerConfig(config *gin.RouterGroup) { config.GET("", getConfig) config.POST("", saveConfig) + config.POST("/reload", reloadConfig) + config.POST("/services", createService) config.PUT("/services/:service", updateService) config.DELETE("/services/:service", deleteService) diff --git a/api/config_reload.go b/api/config_reload.go new file mode 100644 index 00000000..f36a56ed --- /dev/null +++ b/api/config_reload.go @@ -0,0 +1,56 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/x/config" + "github.com/go-gost/x/config/loader" + "github.com/go-gost/x/config/parsing/parser" + "github.com/go-gost/x/registry" +) + +// swagger:parameters reloadConfigRequest +type reloadConfigRequest struct{} + +// successful operation. +// swagger:response reloadConfigResponse +type reloadConfigResponse struct { + Data Response +} + +func reloadConfig(ctx *gin.Context) { + // swagger:route POST /config/reload Reload reloadConfigRequest + // + // Hot reload config. + // + // Security: + // basicAuth: [] + // + // Responses: + // 200: reloadConfigResponse + + cfg, err := parser.Parse() + if err != nil { + writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, err.Error())) + return + } + + config.Set(cfg) + + if err := loader.Load(cfg); err != nil { + writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, err.Error())) + return + } + + for _, svc := range registry.ServiceRegistry().GetAll() { + svc := svc + go func() { + svc.Serve() + }() + } + + ctx.JSON(http.StatusOK, Response{ + Msg: "OK", + }) +} diff --git a/api/service/service.go b/api/service/service.go new file mode 100644 index 00000000..914e6eec --- /dev/null +++ b/api/service/service.go @@ -0,0 +1,96 @@ +package service + +import ( + "net" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/core/auth" + "github.com/go-gost/core/service" + "github.com/go-gost/x/api" +) + +type options struct { + accessLog bool + pathPrefix string + auther auth.Authenticator +} + +type Option func(*options) + +func PathPrefixOption(pathPrefix string) Option { + return func(o *options) { + o.pathPrefix = pathPrefix + } +} + +func AccessLogOption(enable bool) Option { + return func(o *options) { + o.accessLog = enable + } +} + +func AutherOption(auther auth.Authenticator) Option { + return func(o *options) { + o.auther = auther + } +} + +type server struct { + s *http.Server + ln net.Listener + cclose chan struct{} +} + +func NewService(network, addr string, opts ...Option) (service.Service, error) { + if network == "" { + network = "tcp" + } + ln, err := net.Listen(network, addr) + if err != nil { + return nil, err + } + + var options options + for _, opt := range opts { + opt(&options) + } + + gin.SetMode(gin.ReleaseMode) + + r := gin.New() + api.Register(r, &api.Options{ + AccessLog: options.accessLog, + PathPrefix: options.pathPrefix, + Auther: options.auther, + }) + + return &server{ + s: &http.Server{ + Handler: r, + }, + ln: ln, + cclose: make(chan struct{}), + }, nil +} + +func (s *server) Serve() error { + return s.s.Serve(s.ln) +} + +func (s *server) Addr() net.Addr { + return s.ln.Addr() +} + +func (s *server) Close() error { + return s.s.Close() +} + +func (s *server) IsClosed() bool { + select { + case <-s.cclose: + return true + default: + return false + } +} diff --git a/api/swagger.yaml b/api/swagger.yaml index e120dfbd..78db7a7a 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -41,7 +41,7 @@ definitions: reload: $ref: '#/definitions/Duration' reverse: - description: DEPRECATED by whitelist since beta.4 + description: 'Deprecated: use whitelist instead' type: boolean x-go-name: Reverse whitelist: @@ -102,7 +102,7 @@ definitions: reload: $ref: '#/definitions/Duration' reverse: - description: DEPRECATED by whitelist since beta.4 + description: 'Deprecated: use whitelist instead' type: boolean x-go-name: Reverse whitelist: @@ -309,11 +309,13 @@ definitions: filter: $ref: '#/definitions/NodeFilterConfig' host: - description: DEPRECATED by filter.host + description: 'Deprecated: use matcher instead' type: string x-go-name: Host http: $ref: '#/definitions/HTTPNodeConfig' + matcher: + $ref: '#/definitions/NodeMatcherConfig' metadata: additionalProperties: {} type: object @@ -325,11 +327,11 @@ definitions: type: string x-go-name: Network path: - description: DEPRECATED by filter.path + description: 'Deprecated: use matcher instead' type: string x-go-name: Path protocol: - description: DEPRECATED by filter.protocol + description: 'Deprecated: use matcher instead' type: string x-go-name: Protocol tls: @@ -343,7 +345,7 @@ definitions: type: string x-go-name: Hop name: - description: DEPRECATED by hop field + description: 'Deprecated: use hop instead' type: string x-go-name: Name nodes: @@ -355,6 +357,17 @@ definitions: $ref: '#/definitions/SelectorConfig' type: object x-go-package: github.com/go-gost/x/config + HTTPBodyRewriteConfig: + properties: + Match: + type: string + Replacement: + type: string + Type: + description: filter by MIME types + type: string + type: object + x-go-package: github.com/go-gost/x/config HTTPLoader: properties: timeout: @@ -371,20 +384,52 @@ definitions: header: additionalProperties: type: string + description: 'Deprecated: use requestHeader instead' type: object x-go-name: Header host: + description: rewrite host header type: string x-go-name: Host + requestHeader: + additionalProperties: + type: string + description: additional request header + type: object + x-go-name: RequestHeader + responseHeader: + additionalProperties: + type: string + description: additional response header + type: object + x-go-name: ResponseHeader rewrite: + description: 'Deprecated: use rewriteURL instead' items: $ref: '#/definitions/HTTPURLRewriteConfig' type: array x-go-name: Rewrite + rewriteBody: + description: rewrite response body + items: + $ref: '#/definitions/HTTPBodyRewriteConfig' + type: array + x-go-name: RewriteBody + rewriteURL: + description: rewrite URL + items: + $ref: '#/definitions/HTTPURLRewriteConfig' + type: array + x-go-name: RewriteURL type: object x-go-package: github.com/go-gost/x/config HTTPRecorder: properties: + header: + additionalProperties: + type: string + type: object + x-go-name: Header timeout: $ref: '#/definitions/Duration' url: @@ -458,6 +503,10 @@ definitions: interface: type: string x-go-name: Interface + metadata: + additionalProperties: {} + type: object + x-go-name: Metadata name: type: string x-go-name: Name @@ -737,6 +786,8 @@ definitions: interface: type: string x-go-name: Interface + matcher: + $ref: '#/definitions/NodeMatcherConfig' metadata: additionalProperties: {} type: object @@ -744,6 +795,9 @@ definitions: name: type: string x-go-name: Name + netns: + type: string + x-go-name: Netns network: type: string x-go-name: Network @@ -769,6 +823,17 @@ definitions: x-go-name: Protocol type: object x-go-package: github.com/go-gost/x/config + NodeMatcherConfig: + properties: + priority: + format: int64 + type: integer + x-go-name: Priority + rule: + type: string + x-go-name: Rule + type: object + x-go-package: github.com/go-gost/x/config ObserverConfig: properties: name: @@ -821,9 +886,10 @@ definitions: x-go-package: github.com/go-gost/x/config RecorderObject: properties: - Metadata: + metadata: additionalProperties: {} type: object + x-go-name: Metadata name: type: string x-go-name: Name @@ -850,6 +916,9 @@ definitions: type: type: string x-go-name: Type + username: + type: string + x-go-name: Username type: object x-go-package: github.com/go-gost/x/config RedisRecorder: @@ -870,6 +939,9 @@ definitions: type: type: string x-go-name: Type + username: + type: string + x-go-name: Username type: object x-go-package: github.com/go-gost/x/config ResolverConfig: @@ -983,7 +1055,7 @@ definitions: type: string x-go-name: Hosts interface: - description: DEPRECATED by metadata.interface since beta.5 + description: 'Deprecated: use metadata.interface instead' type: string x-go-name: Interface limiter: @@ -1139,6 +1211,11 @@ definitions: x-go-package: github.com/go-gost/x/config TLSOptions: properties: + alpn: + items: + type: string + type: array + x-go-name: ALPN cipherSuites: items: type: string @@ -1834,6 +1911,18 @@ paths: summary: Update recorder by name, the recorder must already exist. tags: - Recorder + /config/reload: + post: + operationId: reloadConfigRequest + responses: + "200": + $ref: '#/responses/reloadConfigResponse' + security: + - basicAuth: + - '[]' + summary: Hot reload config. + tags: + - Reload /config/resolvers: post: operationId: createResolverRequest @@ -2325,6 +2414,12 @@ responses: Config: {} schema: $ref: '#/definitions/Config' + reloadConfigResponse: + description: successful operation. + headers: + Data: {} + schema: + $ref: '#/definitions/Response' saveConfigResponse: description: successful operation. headers: diff --git a/config/loader/loader.go b/config/loader/loader.go new file mode 100644 index 00000000..7273271d --- /dev/null +++ b/config/loader/loader.go @@ -0,0 +1,232 @@ +package loader + +import ( + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + "github.com/go-gost/x/config/parsing" + admission_parser "github.com/go-gost/x/config/parsing/admission" + auth_parser "github.com/go-gost/x/config/parsing/auth" + bypass_parser "github.com/go-gost/x/config/parsing/bypass" + chain_parser "github.com/go-gost/x/config/parsing/chain" + hop_parser "github.com/go-gost/x/config/parsing/hop" + hosts_parser "github.com/go-gost/x/config/parsing/hosts" + ingress_parser "github.com/go-gost/x/config/parsing/ingress" + limiter_parser "github.com/go-gost/x/config/parsing/limiter" + logger_parser "github.com/go-gost/x/config/parsing/logger" + observer_parser "github.com/go-gost/x/config/parsing/observer" + recorder_parser "github.com/go-gost/x/config/parsing/recorder" + resolver_parser "github.com/go-gost/x/config/parsing/resolver" + router_parser "github.com/go-gost/x/config/parsing/router" + sd_parser "github.com/go-gost/x/config/parsing/sd" + service_parser "github.com/go-gost/x/config/parsing/service" + "github.com/go-gost/x/registry" +) + +var ( + defaultLoader *loader = &loader{} +) + +func Load(cfg *config.Config) error { + return defaultLoader.Load(cfg) +} + +type loader struct{} + +func (l *loader) Load(cfg *config.Config) error { + logCfg := cfg.Log + if logCfg == nil { + logCfg = &config.LogConfig{} + } + logger.SetDefault(logger_parser.ParseLogger(&config.LoggerConfig{Log: logCfg})) + + tlsCfg, err := parsing.BuildDefaultTLSConfig(cfg.TLS) + if err != nil { + return err + } + parsing.SetDefaultTLSConfig(tlsCfg) + + if err := register(cfg); err != nil { + return err + } + + return nil +} + +func register(cfg *config.Config) error { + if cfg == nil { + return nil + } + + for name := range registry.LoggerRegistry().GetAll() { + registry.LoggerRegistry().Unregister(name) + } + for _, loggerCfg := range cfg.Loggers { + if err := registry.LoggerRegistry().Register(loggerCfg.Name, logger_parser.ParseLogger(loggerCfg)); err != nil { + return err + } + } + + for name := range registry.AutherRegistry().GetAll() { + registry.AutherRegistry().Unregister(name) + } + for _, autherCfg := range cfg.Authers { + if err := registry.AutherRegistry().Register(autherCfg.Name, auth_parser.ParseAuther(autherCfg)); err != nil { + return err + } + } + + for name := range registry.AdmissionRegistry().GetAll() { + registry.AdmissionRegistry().Unregister(name) + } + for _, admissionCfg := range cfg.Admissions { + if err := registry.AdmissionRegistry().Register(admissionCfg.Name, admission_parser.ParseAdmission(admissionCfg)); err != nil { + return err + } + } + + for name := range registry.BypassRegistry().GetAll() { + registry.BypassRegistry().Unregister(name) + } + for _, bypassCfg := range cfg.Bypasses { + if err := registry.BypassRegistry().Register(bypassCfg.Name, bypass_parser.ParseBypass(bypassCfg)); err != nil { + return err + } + } + + for name := range registry.ResolverRegistry().GetAll() { + registry.ResolverRegistry().Unregister(name) + } + for _, resolverCfg := range cfg.Resolvers { + r, err := resolver_parser.ParseResolver(resolverCfg) + if err != nil { + return err + } + if err := registry.ResolverRegistry().Register(resolverCfg.Name, r); err != nil { + return err + } + } + + for name := range registry.HostsRegistry().GetAll() { + registry.HostsRegistry().Unregister(name) + } + for _, hostsCfg := range cfg.Hosts { + if err := registry.HostsRegistry().Register(hostsCfg.Name, hosts_parser.ParseHostMapper(hostsCfg)); err != nil { + return err + } + } + + for name := range registry.IngressRegistry().GetAll() { + registry.IngressRegistry().Unregister(name) + } + for _, ingressCfg := range cfg.Ingresses { + if err := registry.IngressRegistry().Register(ingressCfg.Name, ingress_parser.ParseIngress(ingressCfg)); err != nil { + return err + } + } + + for name := range registry.RouterRegistry().GetAll() { + registry.RouterRegistry().Unregister(name) + } + for _, routerCfg := range cfg.Routers { + if err := registry.RouterRegistry().Register(routerCfg.Name, router_parser.ParseRouter(routerCfg)); err != nil { + return err + } + } + + for name := range registry.SDRegistry().GetAll() { + registry.SDRegistry().Unregister(name) + } + for _, sdCfg := range cfg.SDs { + if err := registry.SDRegistry().Register(sdCfg.Name, sd_parser.ParseSD(sdCfg)); err != nil { + return err + } + } + + for name := range registry.ObserverRegistry().GetAll() { + registry.ObserverRegistry().Unregister(name) + } + for _, observerCfg := range cfg.Observers { + if err := registry.ObserverRegistry().Register(observerCfg.Name, observer_parser.ParseObserver(observerCfg)); err != nil { + return err + } + } + + for name := range registry.RecorderRegistry().GetAll() { + registry.RecorderRegistry().Unregister(name) + } + for _, recorderCfg := range cfg.Recorders { + if err := registry.RecorderRegistry().Register(recorderCfg.Name, recorder_parser.ParseRecorder(recorderCfg)); err != nil { + return err + } + } + + for name := range registry.TrafficLimiterRegistry().GetAll() { + registry.TrafficLimiterRegistry().Unregister(name) + } + for _, limiterCfg := range cfg.Limiters { + if err := registry.TrafficLimiterRegistry().Register(limiterCfg.Name, limiter_parser.ParseTrafficLimiter(limiterCfg)); err != nil { + return err + } + } + + for name := range registry.ConnLimiterRegistry().GetAll() { + registry.ConnLimiterRegistry().Unregister(name) + } + for _, limiterCfg := range cfg.CLimiters { + if err := registry.ConnLimiterRegistry().Register(limiterCfg.Name, limiter_parser.ParseConnLimiter(limiterCfg)); err != nil { + return err + } + } + + for name := range registry.RateLimiterRegistry().GetAll() { + registry.RateLimiterRegistry().Unregister(name) + } + for _, limiterCfg := range cfg.RLimiters { + if err := registry.RateLimiterRegistry().Register(limiterCfg.Name, limiter_parser.ParseRateLimiter(limiterCfg)); err != nil { + return err + } + } + + for name := range registry.HopRegistry().GetAll() { + registry.HopRegistry().Unregister(name) + } + for _, hopCfg := range cfg.Hops { + hop, err := hop_parser.ParseHop(hopCfg, logger.Default()) + if err != nil { + return err + } + if err := registry.HopRegistry().Register(hopCfg.Name, hop); err != nil { + return err + } + } + + for name := range registry.ChainRegistry().GetAll() { + registry.ChainRegistry().Unregister(name) + } + for _, chainCfg := range cfg.Chains { + c, err := chain_parser.ParseChain(chainCfg, logger.Default()) + if err != nil { + return err + } + if err := registry.ChainRegistry().Register(chainCfg.Name, c); err != nil { + return err + } + } + + for name := range registry.ServiceRegistry().GetAll() { + registry.ServiceRegistry().Unregister(name) + } + for _, svcCfg := range cfg.Services { + svc, err := service_parser.ParseService(svcCfg) + if err != nil { + return err + } + if svc != nil { + if err := registry.ServiceRegistry().Register(svcCfg.Name, svc); err != nil { + return err + } + } + } + + return nil +} diff --git a/config/parsing/parser/parser.go b/config/parsing/parser/parser.go new file mode 100644 index 00000000..2a596429 --- /dev/null +++ b/config/parsing/parser/parser.go @@ -0,0 +1,206 @@ +package parser + +import ( + "encoding/json" + "os" + "strings" + + "github.com/go-gost/core/logger" + "github.com/go-gost/x/config" + "github.com/go-gost/x/config/cmd" + xmd "github.com/go-gost/x/metadata" + mdutil "github.com/go-gost/x/metadata/util" +) + +var ( + defaultParser = &parser{} +) + +func Init(args Args) { + defaultParser = &parser{ + args: args, + } +} + +func Parse() (*config.Config, error) { + return defaultParser.Parse() +} + +type Args struct { + CfgFile string + Services []string + Nodes []string + Debug bool + Trace bool + ApiAddr string + MetricsAddr string +} + +type parser struct { + args Args +} + +func (p *parser) Parse() (*config.Config, error) { + cfg := &config.Config{} + + cfgFile := p.args.CfgFile + if cfgFile != "" { + cfgFile = strings.TrimSpace(cfgFile) + if strings.HasPrefix(cfgFile, "{") && strings.HasSuffix(cfgFile, "}") { + if err := json.Unmarshal([]byte(cfgFile), cfg); err != nil { + return nil, err + } + } else { + if err := cfg.ReadFile(cfgFile); err != nil { + return nil, err + } + } + } + + cmdCfg, err := cmd.BuildConfigFromCmd(p.args.Services, p.args.Nodes) + if err != nil { + return nil, err + } + cfg = mergeConfig(cfg, cmdCfg) + + if len(cfg.Services) == 0 && p.args.ApiAddr == "" && cfg.API == nil { + if err := cfg.Load(); err != nil { + return nil, err + } + } + + if v := os.Getenv("GOST_LOGGER_LEVEL"); v != "" { + cfg.Log = &config.LogConfig{ + Level: v, + } + } + if v := os.Getenv("GOST_API"); v != "" { + cfg.API = &config.APIConfig{ + Addr: v, + } + } + if v := os.Getenv("GOST_METRICS"); v != "" { + cfg.Metrics = &config.MetricsConfig{ + Addr: v, + } + } + if v := os.Getenv("GOST_PROFILING"); v != "" { + cfg.Profiling = &config.ProfilingConfig{ + Addr: v, + } + } + + if p.args.Debug || p.args.Trace { + if cfg.Log == nil { + cfg.Log = &config.LogConfig{} + } + + cfg.Log.Level = string(logger.DebugLevel) + if p.args.Trace { + cfg.Log.Level = string(logger.TraceLevel) + } + } + + if p.args.ApiAddr != "" { + cfg.API = &config.APIConfig{ + Addr: p.args.ApiAddr, + } + if url, _ := cmd.Norm(p.args.ApiAddr); url != nil { + cfg.API.Addr = url.Host + if url.User != nil { + username := url.User.Username() + password, _ := url.User.Password() + cfg.API.Auth = &config.AuthConfig{ + Username: username, + Password: password, + } + } + m := map[string]any{} + for k, v := range url.Query() { + if len(v) > 0 { + m[k] = v[0] + } + } + md := xmd.NewMetadata(m) + cfg.API.PathPrefix = mdutil.GetString(md, "pathPrefix") + cfg.API.AccessLog = mdutil.GetBool(md, "accesslog") + } + } + if p.args.MetricsAddr != "" { + cfg.Metrics = &config.MetricsConfig{ + Addr: p.args.MetricsAddr, + } + if url, _ := cmd.Norm(p.args.MetricsAddr); url != nil { + cfg.Metrics.Addr = url.Host + if url.User != nil { + username := url.User.Username() + password, _ := url.User.Password() + cfg.Metrics.Auth = &config.AuthConfig{ + Username: username, + Password: password, + } + } + m := map[string]any{} + for k, v := range url.Query() { + if len(v) > 0 { + m[k] = v[0] + } + } + md := xmd.NewMetadata(m) + cfg.Metrics.Path = mdutil.GetString(md, "path") + } + } + + return cfg, nil +} + +func mergeConfig(cfg1, cfg2 *config.Config) *config.Config { + if cfg1 == nil { + return cfg2 + } + if cfg2 == nil { + return cfg1 + } + + cfg := &config.Config{ + Services: append(cfg1.Services, cfg2.Services...), + Chains: append(cfg1.Chains, cfg2.Chains...), + Hops: append(cfg1.Hops, cfg2.Hops...), + Authers: append(cfg1.Authers, cfg2.Authers...), + Admissions: append(cfg1.Admissions, cfg2.Admissions...), + Bypasses: append(cfg1.Bypasses, cfg2.Bypasses...), + Resolvers: append(cfg1.Resolvers, cfg2.Resolvers...), + Hosts: append(cfg1.Hosts, cfg2.Hosts...), + Ingresses: append(cfg1.Ingresses, cfg2.Ingresses...), + SDs: append(cfg1.SDs, cfg2.SDs...), + Recorders: append(cfg1.Recorders, cfg2.Recorders...), + Limiters: append(cfg1.Limiters, cfg2.Limiters...), + CLimiters: append(cfg1.CLimiters, cfg2.CLimiters...), + RLimiters: append(cfg1.RLimiters, cfg2.RLimiters...), + Loggers: append(cfg1.Loggers, cfg2.Loggers...), + Routers: append(cfg1.Routers, cfg2.Routers...), + Observers: append(cfg1.Observers, cfg2.Observers...), + TLS: cfg1.TLS, + Log: cfg1.Log, + API: cfg1.API, + Metrics: cfg1.Metrics, + Profiling: cfg1.Profiling, + } + if cfg2.TLS != nil { + cfg.TLS = cfg2.TLS + } + if cfg2.Log != nil { + cfg.Log = cfg2.Log + } + if cfg2.API != nil { + cfg.API = cfg2.API + } + if cfg2.Metrics != nil { + cfg.Metrics = cfg2.Metrics + } + if cfg2.Profiling != nil { + cfg.Profiling = cfg2.Profiling + } + + return cfg +} diff --git a/handler/api/handler.go b/handler/api/handler.go new file mode 100644 index 00000000..e45d0e82 --- /dev/null +++ b/handler/api/handler.go @@ -0,0 +1,90 @@ +package api + +import ( + "context" + "net" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-gost/core/handler" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/x/api" + "github.com/go-gost/x/registry" +) + +func init() { + registry.HandlerRegistry().Register("api", NewHandler) +} + +type apiHandler struct { + handler http.Handler + md metadata + options handler.Options +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &apiHandler{ + options: options, + } +} + +func (h *apiHandler) Init(md md.Metadata) (err error) { + if err = h.parseMetadata(md); err != nil { + return + } + + gin.SetMode(gin.ReleaseMode) + + r := gin.New() + api.Register(r, &api.Options{ + AccessLog: h.md.accesslog, + PathPrefix: h.md.pathPrefix, + Auther: h.options.Auther, + }) + h.handler = r + + return +} + +func (h *apiHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { + l := &singleConnListener{ + conn: make(chan net.Conn, 1), + } + l.send(conn) + + s := http.Server{ + Handler: h.handler, + } + s.Serve(l) + + return s.Shutdown(ctx) +} + +type singleConnListener struct { + conn chan net.Conn +} + +func (l *singleConnListener) Accept() (net.Conn, error) { + if conn, ok := <-l.conn; ok { + return conn, nil + } + return nil, net.ErrClosed +} + +func (l *singleConnListener) Close() error { + return nil +} + +func (l *singleConnListener) Addr() net.Addr { + return &net.TCPAddr{} +} + +func (l *singleConnListener) send(conn net.Conn) { + l.conn <- conn + close(l.conn) +} diff --git a/handler/api/metadata.go b/handler/api/metadata.go new file mode 100644 index 00000000..4005aad6 --- /dev/null +++ b/handler/api/metadata.go @@ -0,0 +1,17 @@ +package api + +import ( + mdata "github.com/go-gost/core/metadata" + mdutil "github.com/go-gost/x/metadata/util" +) + +type metadata struct { + accesslog bool + pathPrefix string +} + +func (h *apiHandler) parseMetadata(md mdata.Metadata) (err error) { + h.md.accesslog = mdutil.GetBool(md, "api.accessLog", "accessLog") + h.md.pathPrefix = mdutil.GetString(md, "api.pathPrefix", "pathPrefix") + return +} diff --git a/handler/metrics/handler.go b/handler/metrics/handler.go index 47f07a3f..936edc83 100644 --- a/handler/metrics/handler.go +++ b/handler/metrics/handler.go @@ -1,10 +1,9 @@ -package file +package metrics import ( "context" "net" "net/http" - "sync" "time" "github.com/go-gost/core/handler" @@ -20,8 +19,7 @@ func init() { type metricsHandler struct { handler http.Handler - server *http.Server - ln *singleConnListener + mux *http.ServeMux md metadata options handler.Options } @@ -42,32 +40,33 @@ func (h *metricsHandler) Init(md md.Metadata) (err error) { return } - xmetrics.Init(xmetrics.NewMetrics()) + xmetrics.Enable(true) + h.handler = promhttp.Handler() mux := http.NewServeMux() mux.Handle(h.md.path, http.HandlerFunc(h.handleFunc)) - h.server = &http.Server{ - Handler: mux, - } - - h.ln = &singleConnListener{ - conn: make(chan net.Conn), - done: make(chan struct{}), - } - go h.server.Serve(h.ln) + h.mux = mux return } func (h *metricsHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { - h.ln.send(conn) + l := &singleConnListener{ + conn: make(chan net.Conn, 1), + } + l.send(conn) - return nil + s := http.Server{ + Handler: h.mux, + } + s.Serve(l) + + return s.Shutdown(ctx) } func (h *metricsHandler) Close() error { - return h.server.Close() + return nil } func (h *metricsHandler) handleFunc(w http.ResponseWriter, r *http.Request) { @@ -88,47 +87,29 @@ func (h *metricsHandler) handleFunc(w http.ResponseWriter, r *http.Request) { "remote": r.RemoteAddr, "duration": time.Since(start), }) - log.Debugf("%s %s", r.Method, r.RequestURI) + log.Infof("%s %s", r.Method, r.RequestURI) } type singleConnListener struct { conn chan net.Conn - addr net.Addr - done chan struct{} - mu sync.Mutex } func (l *singleConnListener) Accept() (net.Conn, error) { - select { - case conn := <-l.conn: + if conn, ok := <-l.conn; ok { return conn, nil - - case <-l.done: - return nil, net.ErrClosed } + return nil, net.ErrClosed } func (l *singleConnListener) Close() error { - l.mu.Lock() - defer l.mu.Unlock() - - select { - case <-l.done: - default: - close(l.done) - } - return nil } func (l *singleConnListener) Addr() net.Addr { - return l.addr + return &net.TCPAddr{} } func (l *singleConnListener) send(conn net.Conn) { - select { - case l.conn <- conn: - case <-l.done: - return - } + l.conn <- conn + close(l.conn) } diff --git a/handler/metrics/metadata.go b/handler/metrics/metadata.go index bff20a40..272f9ead 100644 --- a/handler/metrics/metadata.go +++ b/handler/metrics/metadata.go @@ -1,4 +1,4 @@ -package file +package metrics import ( mdata "github.com/go-gost/core/metadata" diff --git a/metrics/metrics.go b/metrics/metrics.go index b12dd72d..a37bd4df 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -1,6 +1,10 @@ package metrics -import "github.com/go-gost/core/metrics" +import ( + "sync/atomic" + + "github.com/go-gost/core/metrics" +) const ( // Number of services. Labels: host. @@ -24,29 +28,35 @@ const ( ) var ( - global metrics.Metrics = Noop() + defaultMetrics metrics.Metrics = NewMetrics() + enabled atomic.Bool ) -func Init(m metrics.Metrics) { - if m != nil { - global = m - } else { - global = Noop() - } +func Enable(b bool) { + enabled.Store(b) } func IsEnabled() bool { - return global != Noop() + return enabled.Load() } func GetCounter(name metrics.MetricName, labels metrics.Labels) metrics.Counter { - return global.Counter(name, labels) + if IsEnabled() { + return defaultMetrics.Counter(name, labels) + } + return noop.Counter(name, labels) } func GetGauge(name metrics.MetricName, labels metrics.Labels) metrics.Gauge { - return global.Gauge(name, labels) + if IsEnabled() { + return defaultMetrics.Gauge(name, labels) + } + return noop.Gauge(name, labels) } func GetObserver(name metrics.MetricName, labels metrics.Labels) metrics.Observer { - return global.Observer(name, labels) + if IsEnabled() { + return defaultMetrics.Observer(name, labels) + } + return noop.Observer(name, labels) }