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
)