diff --git a/cmd/debug_page.go b/cmd/debug_page.go index 607ee5ca..dcb18225 100644 --- a/cmd/debug_page.go +++ b/cmd/debug_page.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/api" apiV1 "github.com/yusing/godoxy/internal/api/v1" agentApi "github.com/yusing/godoxy/internal/api/v1/agent" @@ -128,25 +129,31 @@ func listenDebugServer() { mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") w.WriteHeader(http.StatusOK) - w.Write([]byte(`🐙`)) + fmt.Fprint(w, `🐙`) }) mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler) mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler) - apiHandler := newApiHandler(mux) + apiHandler := newAPIHandler(mux) mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP) mux.Finalize() - go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Expires", "0") - mux.mux.ServeHTTP(w, r) - })) + go func() { + //nolint:gosec + err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Expires", "0") + mux.mux.ServeHTTP(w, r) + })) + if err != nil { + log.Err(err).Msg("Error starting debug server") + } + }() } -func newApiHandler(debugMux *debugMux) *gin.Engine { +func newAPIHandler(debugMux *debugMux) *gin.Engine { r := gin.New() r.Use(api.ErrorHandler()) r.Use(api.ErrorLoggingMiddleware()) diff --git a/internal/acl/types/context.go b/internal/acl/types/context.go index fe65b807..adaf61ef 100644 --- a/internal/acl/types/context.go +++ b/internal/acl/types/context.go @@ -4,7 +4,7 @@ import "context" type ContextKey struct{} -func SetCtx(ctx interface{ SetValue(any, any) }, acl ACL) { +func SetCtx(ctx interface{ SetValue(key any, value any) }, acl ACL) { ctx.SetValue(ContextKey{}, acl) } diff --git a/internal/agentpool/agent.go b/internal/agentpool/agent.go index b85aeb0d..8c0b2221 100644 --- a/internal/agentpool/agent.go +++ b/internal/agentpool/agent.go @@ -34,7 +34,10 @@ func newAgent(cfg *agent.AgentConfig) *Agent { if addr != agent.AgentHost+":443" { return nil, &net.AddrError{Err: "invalid address", Addr: addr} } - return net.DialTimeout("tcp", cfg.Addr, timeout) + dialer := &net.Dialer{ + Timeout: timeout, + } + return dialer.Dial("tcp", cfg.Addr) }, TLSConfig: cfg.TLSConfig(), ReadTimeout: 5 * time.Second, diff --git a/internal/agentpool/http_requests.go b/internal/agentpool/http_requests.go index 6b5735cf..131ecbbd 100644 --- a/internal/agentpool/http_requests.go +++ b/internal/agentpool/http_requests.go @@ -10,24 +10,24 @@ import ( "github.com/bytedance/sonic" "github.com/gorilla/websocket" "github.com/valyala/fasthttp" - "github.com/yusing/godoxy/agent/pkg/agent" + agentPkg "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/goutils/http/reverseproxy" ) -func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body) +func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body) if err != nil { return nil, err } - return cfg.httpClient.Do(req) + return agent.httpClient.Do(req) } -func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) { - req.URL.Host = agent.AgentHost +func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) { + req.URL.Host = agentPkg.AgentHost req.URL.Scheme = "https" - req.URL.Path = agent.APIEndpointBase + endpoint + req.URL.Path = agentPkg.APIEndpointBase + endpoint req.RequestURI = "" - resp, err := cfg.httpClient.Do(req) + resp, err := agent.httpClient.Do(req) if err != nil { return nil, err } @@ -40,20 +40,20 @@ type HealthCheckResponse struct { Latency time.Duration `json:"latency"` } -func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) { +func (agent *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) { req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) resp := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(resp) - req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query) + req.SetRequestURI(agentPkg.APIBaseURL + agentPkg.EndpointHealth + "?" + query) req.Header.SetMethod(fasthttp.MethodGet) req.Header.Set("Accept-Encoding", "identity") req.SetConnectionClose() start := time.Now() - err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout) + err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout) ret.Latency = time.Since(start) if err != nil { return ret, err @@ -71,14 +71,14 @@ func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Health return ret, nil } -func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) { - transport := cfg.Transport() +func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) { + transport := agent.Transport() dialer := websocket.Dialer{ NetDialContext: transport.DialContext, NetDialTLSContext: transport.DialTLSContext, } - return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{ - "Host": {agent.AgentHost}, + return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{ + "Host": {agentPkg.AgentHost}, }) } @@ -86,9 +86,9 @@ func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Co // // It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint // If the request has a query, it will be added to the proxy request's URL -func (cfg *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) { - rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport()) - req.URL.Host = agent.AgentHost +func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) { + rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport()) + req.URL.Host = agentPkg.AgentHost req.URL.Scheme = "https" req.URL.Path = endpoint req.RequestURI = "" diff --git a/internal/entrypoint/types/context.go b/internal/entrypoint/types/context.go index f2bde899..0322b584 100644 --- a/internal/entrypoint/types/context.go +++ b/internal/entrypoint/types/context.go @@ -6,7 +6,7 @@ import ( type ContextKey struct{} -func SetCtx(ctx interface{ SetValue(any, any) }, ep Entrypoint) { +func SetCtx(ctx interface{ SetValue(key any, value any) }, ep Entrypoint) { ctx.SetValue(ContextKey{}, ep) } diff --git a/internal/health/check/http.go b/internal/health/check/http.go index 6a597373..92985d31 100644 --- a/internal/health/check/http.go +++ b/internal/health/check/http.go @@ -30,7 +30,7 @@ var pinger = &fasthttp.Client{ DisableHeaderNamesNormalizing: true, DisablePathNormalizing: true, TLSConfig: &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, //nolint:gosec }, MaxConnsPerHost: 1000, NoDefaultUserAgentHeader: true, @@ -52,7 +52,7 @@ func HTTP(url *url.URL, method, path string, timeout time.Duration) (types.Healt respErr := pinger.DoTimeout(req, resp, timeout) lat := time.Since(start) - return processHealthResponse(lat, respErr, resp.StatusCode) + return processHealthResponse(lat, respErr, resp.StatusCode), nil } func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Duration) (types.HealthCheckResult, error) { @@ -88,7 +88,7 @@ func H2C(ctx context.Context, url *url.URL, method, path string, timeout time.Du defer resp.Body.Close() } - return processHealthResponse(lat, err, func() int { return resp.StatusCode }) + return processHealthResponse(lat, err, func() int { return resp.StatusCode }), nil } var userAgent = "GoDoxy/" + version.Get().String() @@ -101,20 +101,20 @@ func setCommonHeaders(setHeader func(key, value string)) { setHeader("Pragma", "no-cache") } -func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) (types.HealthCheckResult, error) { +func processHealthResponse(lat time.Duration, err error, getStatusCode func() int) types.HealthCheckResult { if err != nil { var tlsErr *tls.CertificateVerificationError if ok := errors.As(err, &tlsErr); !ok { return types.HealthCheckResult{ Latency: lat, Detail: err.Error(), - }, nil + } } return types.HealthCheckResult{ Latency: lat, Healthy: true, Detail: tlsErr.Error(), - }, nil + } } statusCode := getStatusCode() @@ -122,11 +122,11 @@ func processHealthResponse(lat time.Duration, err error, getStatusCode func() in return types.HealthCheckResult{ Latency: lat, Detail: http.StatusText(statusCode), - }, nil + } } return types.HealthCheckResult{ Latency: lat, Healthy: true, - }, nil + } } diff --git a/internal/homepage/icons/list/list_icons.go b/internal/homepage/icons/list/list_icons.go index fec83aa7..9579687d 100644 --- a/internal/homepage/icons/list/list_icons.go +++ b/internal/homepage/icons/list/list_icons.go @@ -263,6 +263,8 @@ func httpGetImpl(url string) ([]byte, func([]byte), error) { } /* + UpdateWalkxCodeIcons updates the icon map with the icons from walkxcode. + format: { diff --git a/internal/homepage/icons/url.go b/internal/homepage/icons/url.go index 7e99bfd8..b54f8218 100644 --- a/internal/homepage/icons/url.go +++ b/internal/homepage/icons/url.go @@ -106,21 +106,21 @@ func (u *URL) Parse(v string) error { func (u *URL) parse(v string, checkExists bool) error { if v == "" { - return ErrInvalidIconURL + return gperr.PrependSubject(ErrInvalidIconURL, "empty url") } slashIndex := strings.Index(v, "/") if slashIndex == -1 { - return ErrInvalidIconURL + return gperr.PrependSubject(ErrInvalidIconURL, v) } beforeSlash := v[:slashIndex] switch beforeSlash { case "http:", "https:": u.FullURL = &v u.Source = SourceAbsolute - case "@target", "": // @target/favicon.ico, /favicon.ico + case "@target", "": // @target/favicon.ico, /favicon.ico url := v[slashIndex:] if url == "/" { - return fmt.Errorf("%w: empty path", ErrInvalidIconURL) + return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("%s", "empty path") } u.FullURL = &url u.Source = SourceRelative @@ -132,16 +132,16 @@ func (u *URL) parse(v string, checkExists bool) error { } parts := strings.Split(v[slashIndex+1:], ".") if len(parts) != 2 { - return fmt.Errorf("%w: expect %s/., e.g. %s/adguard-home.webp", ErrInvalidIconURL, beforeSlash, beforeSlash) + return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("expect %s/., e.g. %s/adguard-home.webp", beforeSlash, beforeSlash) } reference, format := parts[0], strings.ToLower(parts[1]) if reference == "" || format == "" { - return ErrInvalidIconURL + return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("empty reference or format") } switch format { case "svg", "png", "webp": default: - return fmt.Errorf("%w: invalid image format, expect svg/png/webp", ErrInvalidIconURL) + return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("invalid image format, expect svg/png/webp") } isLight, isDark := false, false if strings.HasSuffix(reference, "-light") { @@ -159,7 +159,7 @@ func (u *URL) parse(v string, checkExists bool) error { IsDark: isDark, } if checkExists && !u.HasIcon() { - return fmt.Errorf("%w: no such icon %s.%s from %s", ErrInvalidIconURL, reference, format, u.Source) + return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("no such icon from %s", u.Source) } default: return gperr.PrependSubject(ErrInvalidIconURL, v) diff --git a/internal/homepage/integrations/qbittorrent/client.go b/internal/homepage/integrations/qbittorrent/client.go index 756ceb77..28e28476 100644 --- a/internal/homepage/integrations/qbittorrent/client.go +++ b/internal/homepage/integrations/qbittorrent/client.go @@ -2,6 +2,7 @@ package qbittorrent import ( "context" + "errors" "fmt" "io" "net/http" @@ -9,18 +10,29 @@ import ( "github.com/bytedance/sonic" "github.com/yusing/godoxy/internal/homepage/widgets" + strutils "github.com/yusing/goutils/strings" ) type Client struct { URL string Username string - Password string + Password strutils.Redacted } func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error { c.URL = url - c.Username = cfg["username"].(string) - c.Password = cfg["password"].(string) + + username, ok := cfg["username"].(string) + if !ok { + return errors.New("username is not a string") + } + c.Username = username + + password, ok := cfg["password"].(string) + if !ok { + return errors.New("password is not a string") + } + c.Password = strutils.Redacted(password) _, err := c.Version(ctx) if err != nil { @@ -37,7 +49,7 @@ func (c *Client) doRequest(ctx context.Context, method, endpoint string, query u } if c.Username != "" && c.Password != "" { - req.SetBasicAuth(c.Username, c.Password) + req.SetBasicAuth(c.Username, c.Password.String()) } resp, err := widgets.HTTPClient.Do(req) diff --git a/internal/idlewatcher/handle_http_debug.go b/internal/idlewatcher/handle_http_debug.go index 98108d65..2076aff0 100644 --- a/internal/idlewatcher/handle_http_debug.go +++ b/internal/idlewatcher/handle_http_debug.go @@ -62,6 +62,6 @@ func DebugHandler(rw http.ResponseWriter, r *http.Request) { } } default: - w.writeLoadingPage(rw) + _ = w.writeLoadingPage(rw) } } diff --git a/internal/idlewatcher/watcher.go b/internal/idlewatcher/watcher.go index 5d5e7fd5..aede7dcc 100644 --- a/internal/idlewatcher/watcher.go +++ b/internal/idlewatcher/watcher.go @@ -32,6 +32,8 @@ import ( ) type ( + Config = types.IdlewatcherConfig + routeHelper struct { route types.Route rp *reverseproxy.ReverseProxy @@ -52,7 +54,7 @@ type ( l zerolog.Logger - cfg *types.IdlewatcherConfig + cfg *Config provider synk.Value[idlewatcher.Provider] @@ -104,7 +106,7 @@ const reqTimeout = 3 * time.Second // prevents dependencies from being stopped automatically. const neverTick = time.Duration(1<<63 - 1) -func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig) (*Watcher, error) { +func NewWatcher(parent task.Parent, r types.Route, cfg *Config) (*Watcher, error) { key := cfg.Key() watcherMapMu.RLock() @@ -193,7 +195,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig) depCfg := depRoute.IdlewatcherConfig() if depCfg == nil { - depCfg = new(types.IdlewatcherConfig) + depCfg = new(Config) depCfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies } else if depCfg.IdleTimeout > 0 && depCfg.IdleTimeout != neverTick { diff --git a/internal/logging/accesslog/config.go b/internal/logging/accesslog/config.go index 944fd41b..d039a266 100644 --- a/internal/logging/accesslog/config.go +++ b/internal/logging/accesslog/config.go @@ -17,16 +17,19 @@ type ( } // @name AccessLoggerConfigBase ACLLoggerConfig struct { ConfigBase + LogAllowed bool `json:"log_allowed"` } // @name ACLLoggerConfig RequestLoggerConfig struct { ConfigBase + Format Format `json:"format" validate:"oneof=common combined json"` Filters Filters `json:"filters"` Fields Fields `json:"fields"` } // @name RequestLoggerConfig Config struct { ConfigBase + acl *ACLLoggerConfig req *RequestLoggerConfig } diff --git a/internal/logging/accesslog/file_access_logger.go b/internal/logging/accesslog/file_access_logger.go index 7f09dbba..b54e0afe 100644 --- a/internal/logging/accesslog/file_access_logger.go +++ b/internal/logging/accesslog/file_access_logger.go @@ -27,6 +27,9 @@ type ( } fileAccessLogger struct { + RequestFormatter + ACLLogFormatter + task *task.Task cfg *Config @@ -41,9 +44,6 @@ type ( errRateLimiter *rate.Limiter logger zerolog.Logger - - RequestFormatter - ACLLogFormatter } ) diff --git a/internal/logging/accesslog/mock_file.go b/internal/logging/accesslog/mock_file.go index 25c9ad63..ca554847 100644 --- a/internal/logging/accesslog/mock_file.go +++ b/internal/logging/accesslog/mock_file.go @@ -36,9 +36,9 @@ func (m *MockFile) Len() int64 { func (m *MockFile) Content() []byte { buf := bytes.NewBuffer(nil) - m.Seek(0, io.SeekStart) + _, _ = m.Seek(0, io.SeekStart) _, _ = buf.ReadFrom(m.File) - m.Seek(0, io.SeekStart) + _, _ = m.Seek(0, io.SeekStart) return buf.Bytes() } diff --git a/internal/logging/accesslog/retention.go b/internal/logging/accesslog/retention.go index fdec969a..188cc7cf 100644 --- a/internal/logging/accesslog/retention.go +++ b/internal/logging/accesslog/retention.go @@ -10,9 +10,9 @@ import ( ) type Retention struct { - Days uint64 `json:"days,omitempty"` - Last uint64 `json:"last,omitempty"` - KeepSize uint64 `json:"keep_size,omitempty"` + Days int64 `json:"days,omitempty" validate:"min=0"` + Last int64 `json:"last,omitempty" validate:"min=0"` + KeepSize int64 `json:"keep_size,omitempty" validate:"min=0"` } // @name LogRetention var ( @@ -39,9 +39,9 @@ func (r *Retention) Parse(v string) (err error) { } switch split[0] { case "last": - r.Last, err = strconv.ParseUint(split[1], 10, 64) + r.Last, err = strconv.ParseInt(split[1], 10, 64) default: // days|weeks|months - n, err := strconv.ParseUint(split[0], 10, 64) + n, err := strconv.ParseInt(split[0], 10, 64) if err != nil { return err } diff --git a/internal/logging/accesslog/rotate.go b/internal/logging/accesslog/rotate.go index cee7deb9..c68f169d 100644 --- a/internal/logging/accesslog/rotate.go +++ b/internal/logging/accesslog/rotate.go @@ -85,7 +85,7 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate switch { case config.Last > 0: - shouldStop = func() bool { return result.NumLinesKeep-result.NumLinesInvalid == int(config.Last) } + shouldStop = func() bool { return int64(result.NumLinesKeep-result.NumLinesInvalid) == config.Last } // not needed to parse time for last N lines case config.Days > 0: cutoff := mockable.TimeNow().AddDate(0, 0, -int(config.Days)+1) @@ -227,7 +227,7 @@ func rotateLogFileBySize(file supportRotate, config *Retention, result *RotateRe result.OriginalSize = fileSize - keepSize := int64(config.KeepSize) + keepSize := config.KeepSize if keepSize >= fileSize { result.NumBytesKeep = fileSize return false, nil diff --git a/internal/maxmind/maxmind.go b/internal/maxmind/maxmind.go index 0ab8e77e..5b7ddd52 100644 --- a/internal/maxmind/maxmind.go +++ b/internal/maxmind/maxmind.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "context" "errors" "fmt" "io" @@ -99,7 +100,7 @@ func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) error { if !valid { cfg.Logger().Info().Msg("MaxMind DB not found/invalid, downloading...") - if err = cfg.download(); err != nil { + if err = cfg.download(parent.Context()); err != nil { return fmt.Errorf("%w: %w", ErrDownloadFailure, err) } } else { @@ -128,7 +129,7 @@ func (cfg *MaxMind) scheduleUpdate(parent task.Parent) { ticker := time.NewTicker(updateInterval) cfg.loadLastUpdate() - cfg.update() + cfg.update(task.Context()) defer func() { ticker.Stop() @@ -143,15 +144,18 @@ func (cfg *MaxMind) scheduleUpdate(parent task.Parent) { case <-task.Context().Done(): return case <-ticker.C: - cfg.update() + cfg.update(task.Context()) } } } -func (cfg *MaxMind) update() { +func (cfg *MaxMind) update(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, updateTimeout) + defer cancel() + // check for update cfg.Logger().Info().Msg("checking for MaxMind DB update...") - remoteLastModified, err := cfg.checkLastest() + remoteLastModified, err := cfg.checkLastest(ctx) if err != nil { cfg.Logger().Err(err).Msg("failed to check MaxMind DB update") return @@ -165,15 +169,15 @@ func (cfg *MaxMind) update() { Time("latest", remoteLastModified.Local()). Time("current", cfg.lastUpdate). Msg("MaxMind DB update available") - if err = cfg.download(); err != nil { + if err = cfg.download(ctx); err != nil { cfg.Logger().Err(err).Msg("failed to update MaxMind DB") return } cfg.Logger().Info().Msg("MaxMind DB updated") } -func (cfg *MaxMind) doReq(method string) (*http.Response, error) { - req, err := http.NewRequest(method, cfg.dbURL(), nil) +func (cfg *MaxMind) doReq(ctx context.Context, method string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, cfg.dbURL(), nil) if err != nil { return nil, err } @@ -185,34 +189,36 @@ func (cfg *MaxMind) doReq(method string) (*http.Response, error) { return resp, nil } -func (cfg *MaxMind) checkLastest() (lastModifiedT *time.Time, err error) { - resp, err := cfg.doReq(http.MethodHead) +func (cfg *MaxMind) checkLastest(ctx context.Context) (lastModifiedT time.Time, err error) { + resp, err := cfg.doReq(ctx, http.MethodHead) if err != nil { - return nil, err + return time.Time{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode) + return time.Time{}, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode) } lastModified := resp.Header.Get("Last-Modified") if lastModified == "" { - cfg.Logger().Warn().Msg("MaxMind responded no last modified time, update skipped") - return nil, nil + return time.Time{}, nil } lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified) if err != nil { cfg.Logger().Warn().Err(err).Msg("MaxMind responded invalid last modified time, update skipped") - return nil, err + return time.Time{}, err } - return &lastModifiedTime, nil + return lastModifiedTime, nil } -func (cfg *MaxMind) download() error { - resp, err := cfg.doReq(http.MethodGet) +func (cfg *MaxMind) download(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, updateTimeout) + defer cancel() + + resp, err := cfg.doReq(ctx, http.MethodGet) if err != nil { return err } diff --git a/internal/maxmind/maxmind_test.go b/internal/maxmind/maxmind_test.go index 0c59675d..79a9d05c 100644 --- a/internal/maxmind/maxmind_test.go +++ b/internal/maxmind/maxmind_test.go @@ -72,7 +72,7 @@ func mockMaxMindDBOpen(t *testing.T) { func Test_MaxMindConfig_doReq(t *testing.T) { cfg := testCfg() mockDoReq(t, cfg) - resp, err := cfg.doReq(http.MethodGet) + resp, err := cfg.doReq(t.Context(), http.MethodGet) if err != nil { t.Fatalf("newReq() error = %v", err) } @@ -85,7 +85,7 @@ func Test_MaxMindConfig_checkLatest(t *testing.T) { cfg := testCfg() mockDoReq(t, cfg) - latest, err := cfg.checkLastest() + latest, err := cfg.checkLastest(t.Context()) if err != nil { t.Fatalf("checkLatest() error = %v", err) } @@ -100,7 +100,7 @@ func Test_MaxMindConfig_download(t *testing.T) { mockMaxMindDBOpen(t) mockDoReq(t, cfg) - err := cfg.download() + err := cfg.download(t.Context()) if err != nil { t.Fatalf("download() error = %v", err) } diff --git a/internal/metrics/systeminfo/system_info.go b/internal/metrics/systeminfo/system_info.go index 02f47a7e..ce2ffd83 100644 --- a/internal/metrics/systeminfo/system_info.go +++ b/internal/metrics/systeminfo/system_info.go @@ -51,19 +51,6 @@ const ( SystemInfoAggregateModeSensorTemperature SystemInfoAggregateMode = "sensor_temperature" // @name SystemInfoAggregateModeSensorTemperature ) -var allQueries = []SystemInfoAggregateMode{ - SystemInfoAggregateModeCPUAverage, - SystemInfoAggregateModeMemoryUsage, - SystemInfoAggregateModeMemoryUsagePercent, - SystemInfoAggregateModeDisksReadSpeed, - SystemInfoAggregateModeDisksWriteSpeed, - SystemInfoAggregateModeDisksIOPS, - SystemInfoAggregateModeDiskUsage, - SystemInfoAggregateModeNetworkSpeed, - SystemInfoAggregateModeNetworkTransfer, - SystemInfoAggregateModeSensorTemperature, -} - var Poller = period.NewPoller("system_info", getSystemInfo, aggregate) func isNoDataAvailable(err error) bool { diff --git a/internal/metrics/systeminfo/system_info_test.go b/internal/metrics/systeminfo/system_info_test.go index d41c6a97..93f7900e 100644 --- a/internal/metrics/systeminfo/system_info_test.go +++ b/internal/metrics/systeminfo/system_info_test.go @@ -123,6 +123,18 @@ func TestSerialize(t *testing.T) { for i := range 5 { entries[i] = testInfo } + var allQueries = []SystemInfoAggregateMode{ + SystemInfoAggregateModeCPUAverage, + SystemInfoAggregateModeMemoryUsage, + SystemInfoAggregateModeMemoryUsagePercent, + SystemInfoAggregateModeDisksReadSpeed, + SystemInfoAggregateModeDisksWriteSpeed, + SystemInfoAggregateModeDisksIOPS, + SystemInfoAggregateModeDiskUsage, + SystemInfoAggregateModeNetworkSpeed, + SystemInfoAggregateModeNetworkTransfer, + SystemInfoAggregateModeSensorTemperature, + } for _, query := range allQueries { t.Run(string(query), func(t *testing.T) { _, result := aggregate(entries, url.Values{"aggregate": []string{string(query)}}) diff --git a/internal/metrics/uptime/uptime.go b/internal/metrics/uptime/uptime.go index dba3cda1..773a506c 100644 --- a/internal/metrics/uptime/uptime.go +++ b/internal/metrics/uptime/uptime.go @@ -3,6 +3,7 @@ package uptime import ( "context" "errors" + "math" "net/url" "slices" "time" @@ -70,7 +71,7 @@ func aggregateStatuses(entries []StatusByAlias, query url.Values) (int, Aggregat for alias, status := range entry.Map { statuses[alias] = append(statuses[alias], Status{ Status: status.Status, - Latency: int32(status.Latency.Milliseconds()), + Latency: int32(min(math.MaxInt32, status.Latency.Milliseconds())), //nolint:gosec Timestamp: entry.Timestamp, }) } @@ -134,7 +135,6 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated { status := types.StatusUnknown if state := config.ActiveState.Load(); state != nil { - // FIXME: pass ctx to getRoute r, ok := entrypoint.FromCtx(state.Context()).GetRoute(alias) if ok { mon := r.HealthMonitor() diff --git a/internal/net/gphttp/loadbalancer/round_robin.go b/internal/net/gphttp/loadbalancer/round_robin.go index 2f228d56..59deef17 100644 --- a/internal/net/gphttp/loadbalancer/round_robin.go +++ b/internal/net/gphttp/loadbalancer/round_robin.go @@ -8,7 +8,7 @@ import ( ) type roundRobin struct { - index atomic.Uint32 + index atomic.Uint64 } var _ impl = (*roundRobin)(nil) @@ -21,6 +21,6 @@ func (lb *roundRobin) ChooseServer(srvs types.LoadBalancerServers, r *http.Reque if len(srvs) == 0 { return nil } - index := (lb.index.Add(1) - 1) % uint32(len(srvs)) + index := (lb.index.Add(1) - 1) % uint64(len(srvs)) return srvs[index] } diff --git a/internal/net/gphttp/middleware/captcha/hcaptcha.go b/internal/net/gphttp/middleware/captcha/hcaptcha.go index 351572a4..f3be9ef3 100644 --- a/internal/net/gphttp/middleware/captcha/hcaptcha.go +++ b/internal/net/gphttp/middleware/captcha/hcaptcha.go @@ -22,12 +22,14 @@ type HcaptchaProvider struct { Secret string `json:"secret" validate:"required"` } -// https://docs.hcaptcha.com/#content-security-policy-settings +// CSPDirectives returns the CSP directives for the Hcaptcha provider. +// See: https://docs.hcaptcha.com/#content-security-policy-settings func (p *HcaptchaProvider) CSPDirectives() []string { return []string{"script-src", "frame-src", "style-src", "connect-src"} } -// https://docs.hcaptcha.com/#content-security-policy-settings +// CSPSources returns the CSP sources for the Hcaptcha provider. +// See: https://docs.hcaptcha.com/#content-security-policy-settings func (p *HcaptchaProvider) CSPSources() []string { return []string{ "https://hcaptcha.com", diff --git a/internal/net/gphttp/middleware/middleware.go b/internal/net/gphttp/middleware/middleware.go index 4643a9bd..828cb171 100644 --- a/internal/net/gphttp/middleware/middleware.go +++ b/internal/net/gphttp/middleware/middleware.go @@ -34,11 +34,11 @@ type ( } Middleware struct { + commonOptions + name string construct ImplNewFunc impl any - - commonOptions } ByPriority []*Middleware @@ -196,7 +196,12 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r * if exec, ok := m.impl.(ResponseModifier); ok { lrm := httputils.NewLazyResponseModifier(w, needsBuffering) - defer lrm.FlushRelease() + defer func() { + _, err := lrm.FlushRelease() + if err != nil { + m.LogError(r).Err(err).Msg("failed to flush response") + } + }() next(lrm, r) // Skip modification if response wasn't buffered (non-HTML content) @@ -225,7 +230,9 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r * // override the content length and body if changed if currentResp.Body != currentBody { - rm.SetBody(currentResp.Body) + if err := rm.SetBody(currentResp.Body); err != nil { + m.LogError(r).Err(err).Msg("failed to set response body") + } } } else { next(w, r) @@ -239,12 +246,14 @@ func needsBuffering(header http.Header) bool { } func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event { + //nolint:zerologlint return log.Warn().Str("middleware", m.name). Str("host", req.Host). Str("path", req.URL.Path) } func (m *Middleware) LogError(req *http.Request) *zerolog.Event { + //nolint:zerologlint return log.Error().Str("middleware", m.name). Str("host", req.Host). Str("path", req.URL.Path) diff --git a/internal/net/gphttp/middleware/test_utils.go b/internal/net/gphttp/middleware/test_utils_test.go similarity index 100% rename from internal/net/gphttp/middleware/test_utils.go rename to internal/net/gphttp/middleware/test_utils_test.go diff --git a/internal/notif/body.go b/internal/notif/body.go index d87bddbe..3685a6dd 100644 --- a/internal/notif/body.go +++ b/internal/notif/body.go @@ -20,6 +20,7 @@ type ( ) type ( + //nolint:recvcheck FieldsBody []LogField ListBody []string MessageBody string @@ -106,6 +107,7 @@ func (m MessageBodyBytes) Format(format LogFormat) ([]byte, error) { switch format { case LogFormatRawJSON: return sonic.Marshal(string(m)) + default: } return m, nil } diff --git a/internal/notif/ntfy.go b/internal/notif/ntfy.go index e45a51f4..87155164 100644 --- a/internal/notif/ntfy.go +++ b/internal/notif/ntfy.go @@ -7,6 +7,8 @@ import ( gperr "github.com/yusing/goutils/errs" ) +// Ntfy is a provider for ntfy. +// // See https://docs.ntfy.sh/publish type Ntfy struct { ProviderBase diff --git a/internal/proxmox/lxc_command.go b/internal/proxmox/lxc_command.go index 0259eff9..348d12e1 100644 --- a/internal/proxmox/lxc_command.go +++ b/internal/proxmox/lxc_command.go @@ -3,6 +3,7 @@ package proxmox import ( "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -10,7 +11,7 @@ import ( "github.com/luthermonson/go-proxmox" ) -var ErrNoSession = fmt.Errorf("no session found, make sure username and password are set") +var ErrNoSession = errors.New("no session found, make sure username and password are set") // closeTransportConnections forces close idle HTTP connections to prevent goroutine leaks. // This is needed because the go-proxmox library's TermWebSocket closer doesn't close diff --git a/internal/route/common.go b/internal/route/common.go index 4c6a4376..b3e8c46c 100644 --- a/internal/route/common.go +++ b/internal/route/common.go @@ -2,6 +2,7 @@ package route import ( "context" + "errors" "fmt" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" @@ -17,7 +18,7 @@ func checkExists(ctx context.Context, r types.Route) error { } ep := entrypoint.FromCtx(ctx) if ep == nil { - return fmt.Errorf("entrypoint not found in context") + return errors.New("entrypoint not found in context") } var ( existing types.Route diff --git a/internal/route/route.go b/internal/route/route.go index 749d3939..28f0c0e9 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -372,6 +372,9 @@ func (r *Route) validateRules() error { r.Rules = rules } case "file", "": + if !strutils.IsValidFilename(src.Path) { + return fmt.Errorf("invalid rule file path %q", src.Path) + } content, err := os.ReadFile(src.Path) if err != nil { return fmt.Errorf("failed to read rule file %q: %w", src.Path, err) diff --git a/internal/route/routes/context.go b/internal/route/routes/context.go index 812cfa21..b0e10ea6 100644 --- a/internal/route/routes/context.go +++ b/internal/route/routes/context.go @@ -31,6 +31,7 @@ func WithRouteContext(r *http.Request, route types.HTTPRoute) *http.Request { // we don't want to copy the request object every fucking requests // return r.WithContext(context.WithValue(r.Context(), routeContextKey, route)) ctxFieldPtr := (*context.Context)(unsafe.Add(unsafe.Pointer(r), ctxFieldOffset)) + //nolint:fatcontext *ctxFieldPtr = &RouteContext{ Context: r.Context(), Route: route, diff --git a/internal/route/rules/help.go b/internal/route/rules/help.go index 52b84786..e8bfb5c7 100644 --- a/internal/route/rules/help.go +++ b/internal/route/rules/help.go @@ -125,7 +125,7 @@ func helpVar(varExpr string) string { } /* -Generate help string as error, e.g. +Error generates help string as error, e.g. rewrite from: the path to rewrite, must start with / diff --git a/internal/route/rules/parser.go b/internal/route/rules/parser.go index 83265323..174babe6 100644 --- a/internal/route/rules/parser.go +++ b/internal/route/rules/parser.go @@ -158,13 +158,14 @@ func parse(v string) (subject string, args []string, err error) { buf.WriteRune('$') } - if quote != 0 { + switch { + case quote != 0: err = ErrUnterminatedQuotes - } else if brackets != 0 { + case brackets != 0: err = ErrUnterminatedBrackets - } else if inEnvVar { + case inEnvVar: err = ErrUnterminatedEnvVar - } else { + default: flush(false) } if len(missingEnvVars) > 0 { diff --git a/internal/route/rules/rules.go b/internal/route/rules/rules.go index 9e6d52cf..01b4640d 100644 --- a/internal/route/rules/rules.go +++ b/internal/route/rules/rules.go @@ -286,8 +286,7 @@ func logError(err error, r *http.Request) { var h2Err http2.StreamError if errors.As(err, &h2Err) { // ignore these errors - switch h2Err.Code { - case http2.ErrCodeStreamClosed: + if h2Err.Code == http2.ErrCodeStreamClosed { return } } diff --git a/internal/route/rules/vars.go b/internal/route/rules/vars.go index 4d29eab4..08de2d61 100644 --- a/internal/route/rules/vars.go +++ b/internal/route/rules/vars.go @@ -39,7 +39,7 @@ func NeedExpandVars(s string) bool { var ( voidResponseModifier = httputils.NewResponseModifier(httptest.NewRecorder()) dummyRequest = http.Request{ - Method: "GET", + Method: http.MethodGet, URL: &url.URL{Path: "/"}, Header: http.Header{}, } diff --git a/internal/route/test_route.go b/internal/route/test_route.go index 3d034ba8..7d9d4167 100644 --- a/internal/route/test_route.go +++ b/internal/route/test_route.go @@ -9,10 +9,10 @@ import ( "github.com/yusing/goutils/task" ) -func NewStartedTestRoute(t testing.TB, base *Route) (types.Route, error) { - t.Helper() +func NewStartedTestRoute(tb testing.TB, base *Route) (types.Route, error) { + tb.Helper() - task := task.GetTestTask(t) + task := task.GetTestTask(tb) if ep := epctx.FromCtx(task.Context()); ep == nil { ep = entrypoint.NewEntrypoint(task, nil) epctx.SetCtx(task, ep) diff --git a/internal/route/types/http_config.go b/internal/route/types/http_config.go index 334897a4..3ba5f769 100644 --- a/internal/route/types/http_config.go +++ b/internal/route/types/http_config.go @@ -27,7 +27,6 @@ type HTTPConfig struct { // BuildTLSConfig creates a TLS configuration based on the HTTP config options. func (cfg *HTTPConfig) BuildTLSConfig(targetURL *url.URL) (*tls.Config, error) { - //nolint:gosec tlsConfig := &tls.Config{} // Handle InsecureSkipVerify (legacy NoTLSVerify option) diff --git a/internal/route/types/scheme.go b/internal/route/types/scheme.go index dc6af468..7b85b58a 100644 --- a/internal/route/types/scheme.go +++ b/internal/route/types/scheme.go @@ -8,6 +8,7 @@ import ( gperr "github.com/yusing/goutils/errs" ) +//nolint:recvcheck type Scheme uint8 var ErrInvalidScheme = errors.New("invalid scheme") diff --git a/internal/serialization/validation.go b/internal/serialization/validation.go index 7582bdaa..550d507b 100644 --- a/internal/serialization/validation.go +++ b/internal/serialization/validation.go @@ -41,20 +41,18 @@ func ValidateWithCustomValidator(v reflect.Value) error { if elemType.Implements(validatorType) { return v.Elem().Interface().(CustomValidator).Validate() } - } else { - if vt.PkgPath() != "" { // not a builtin type - // prioritize pointer method - if v.CanAddr() { - vAddr := v.Addr() - if vAddr.Type().Implements(validatorType) { - return vAddr.Interface().(CustomValidator).Validate() - } - } - // fallback to value method - if vt.Implements(validatorType) { - return v.Interface().(CustomValidator).Validate() + } else if vt.PkgPath() != "" { // not a builtin type + // prioritize pointer method + if v.CanAddr() { + vAddr := v.Addr() + if vAddr.Type().Implements(validatorType) { + return vAddr.Interface().(CustomValidator).Validate() } } + // fallback to value method + if vt.Implements(validatorType) { + return v.Interface().(CustomValidator).Validate() + } } return nil } diff --git a/internal/types/health.go b/internal/types/health.go index 69c5f6a0..0560d73d 100644 --- a/internal/types/health.go +++ b/internal/types/health.go @@ -12,6 +12,7 @@ import ( ) type ( + //nolint:recvcheck HealthStatus uint8 // @name HealthStatus HealthStatusString string // @name HealthStatusString @@ -83,6 +84,7 @@ type ( HealthInfo struct { HealthInfoWithoutDetail + Detail string `json:"detail"` } // @name HealthInfo )