mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-17 05:59:42 +02:00
refactor: fix lint errors; improve error handling
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,8 @@ func httpGetImpl(url string) ([]byte, func([]byte), error) {
|
||||
}
|
||||
|
||||
/*
|
||||
UpdateWalkxCodeIcons updates the icon map with the icons from walkxcode.
|
||||
|
||||
format:
|
||||
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -62,6 +62,6 @@ func DebugHandler(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
default:
|
||||
w.writeLoadingPage(rw)
|
||||
_ = w.writeLoadingPage(rw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
)
|
||||
|
||||
//nolint:recvcheck
|
||||
type Scheme uint8
|
||||
|
||||
var ErrInvalidScheme = errors.New("invalid scheme")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user