refactor: fix lint errors; improve error handling

This commit is contained in:
yusing
2026-02-22 16:04:25 +08:00
parent 3a7d1f8b18
commit 0f78158c64
40 changed files with 191 additions and 136 deletions

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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 = ""

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -263,6 +263,8 @@ func httpGetImpl(url string) ([]byte, func([]byte), error) {
}
/*
UpdateWalkxCodeIcons updates the icon map with the icons from walkxcode.
format:
{

View File

@@ -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/<reference>.<format>, e.g. %s/adguard-home.webp", ErrInvalidIconURL, beforeSlash, beforeSlash)
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("expect %s/<reference>.<format>, 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)

View File

@@ -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)

View File

@@ -62,6 +62,6 @@ func DebugHandler(rw http.ResponseWriter, r *http.Request) {
}
}
default:
w.writeLoadingPage(rw)
_ = w.writeLoadingPage(rw)
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}
)

View File

@@ -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()
}

View File

@@ -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: // <N> days|weeks|months
n, err := strconv.ParseUint(split[0], 10, 64)
n, err := strconv.ParseInt(split[0], 10, 64)
if err != nil {
return err
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)}})

View File

@@ -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()

View File

@@ -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]
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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> <to>
from: the path to rewrite, must start with /

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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{},
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -8,6 +8,7 @@ import (
gperr "github.com/yusing/goutils/errs"
)
//nolint:recvcheck
type Scheme uint8
var ErrInvalidScheme = errors.New("invalid scheme")

View File

@@ -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
}

View File

@@ -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
)