mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 08:48:32 +02:00
refactor: fix lint errors; improve error handling
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/yusing/godoxy/internal/api"
|
"github.com/yusing/godoxy/internal/api"
|
||||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||||
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
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) {
|
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "image/svg+xml")
|
w.Header().Set("Content-Type", "image/svg+xml")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
|
fmt.Fprint(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`)
|
||||||
})
|
})
|
||||||
|
|
||||||
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||||
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||||
apiHandler := newApiHandler(mux)
|
apiHandler := newAPIHandler(mux)
|
||||||
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||||
|
|
||||||
mux.Finalize()
|
mux.Finalize()
|
||||||
|
|
||||||
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
go func() {
|
||||||
w.Header().Set("Pragma", "no-cache")
|
//nolint:gosec
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
err := http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Expires", "0")
|
w.Header().Set("Pragma", "no-cache")
|
||||||
mux.mux.ServeHTTP(w, r)
|
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 := gin.New()
|
||||||
r.Use(api.ErrorHandler())
|
r.Use(api.ErrorHandler())
|
||||||
r.Use(api.ErrorLoggingMiddleware())
|
r.Use(api.ErrorLoggingMiddleware())
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "context"
|
|||||||
|
|
||||||
type ContextKey struct{}
|
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)
|
ctx.SetValue(ContextKey{}, acl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ func newAgent(cfg *agent.AgentConfig) *Agent {
|
|||||||
if addr != agent.AgentHost+":443" {
|
if addr != agent.AgentHost+":443" {
|
||||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
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(),
|
TLSConfig: cfg.TLSConfig(),
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
|
|||||||
@@ -10,24 +10,24 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/valyala/fasthttp"
|
"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"
|
"github.com/yusing/goutils/http/reverseproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
func (agent *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
|
req, err := http.NewRequestWithContext(ctx, method, agentPkg.APIBaseURL+endpoint, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (agent *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||||
req.URL.Host = agent.AgentHost
|
req.URL.Host = agentPkg.AgentHost
|
||||||
req.URL.Scheme = "https"
|
req.URL.Scheme = "https"
|
||||||
req.URL.Path = agent.APIEndpointBase + endpoint
|
req.URL.Path = agentPkg.APIEndpointBase + endpoint
|
||||||
req.RequestURI = ""
|
req.RequestURI = ""
|
||||||
resp, err := cfg.httpClient.Do(req)
|
resp, err := agent.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -40,20 +40,20 @@ type HealthCheckResponse struct {
|
|||||||
Latency time.Duration `json:"latency"`
|
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()
|
req := fasthttp.AcquireRequest()
|
||||||
defer fasthttp.ReleaseRequest(req)
|
defer fasthttp.ReleaseRequest(req)
|
||||||
|
|
||||||
resp := fasthttp.AcquireResponse()
|
resp := fasthttp.AcquireResponse()
|
||||||
defer fasthttp.ReleaseResponse(resp)
|
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.SetMethod(fasthttp.MethodGet)
|
||||||
req.Header.Set("Accept-Encoding", "identity")
|
req.Header.Set("Accept-Encoding", "identity")
|
||||||
req.SetConnectionClose()
|
req.SetConnectionClose()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
err = agent.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
||||||
ret.Latency = time.Since(start)
|
ret.Latency = time.Since(start)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ret, err
|
return ret, err
|
||||||
@@ -71,14 +71,14 @@ func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret Health
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
func (agent *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||||
transport := cfg.Transport()
|
transport := agent.Transport()
|
||||||
dialer := websocket.Dialer{
|
dialer := websocket.Dialer{
|
||||||
NetDialContext: transport.DialContext,
|
NetDialContext: transport.DialContext,
|
||||||
NetDialTLSContext: transport.DialTLSContext,
|
NetDialTLSContext: transport.DialTLSContext,
|
||||||
}
|
}
|
||||||
return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
|
return dialer.DialContext(ctx, agentPkg.APIBaseURL+endpoint, http.Header{
|
||||||
"Host": {agent.AgentHost},
|
"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
|
// 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
|
// 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) {
|
func (agent *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||||
rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
|
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
|
||||||
req.URL.Host = agent.AgentHost
|
req.URL.Host = agentPkg.AgentHost
|
||||||
req.URL.Scheme = "https"
|
req.URL.Scheme = "https"
|
||||||
req.URL.Path = endpoint
|
req.URL.Path = endpoint
|
||||||
req.RequestURI = ""
|
req.RequestURI = ""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
type ContextKey struct{}
|
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)
|
ctx.SetValue(ContextKey{}, ep)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ var pinger = &fasthttp.Client{
|
|||||||
DisableHeaderNamesNormalizing: true,
|
DisableHeaderNamesNormalizing: true,
|
||||||
DisablePathNormalizing: true,
|
DisablePathNormalizing: true,
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true, //nolint:gosec
|
||||||
},
|
},
|
||||||
MaxConnsPerHost: 1000,
|
MaxConnsPerHost: 1000,
|
||||||
NoDefaultUserAgentHeader: true,
|
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)
|
respErr := pinger.DoTimeout(req, resp, timeout)
|
||||||
lat := time.Since(start)
|
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) {
|
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()
|
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()
|
var userAgent = "GoDoxy/" + version.Get().String()
|
||||||
@@ -101,20 +101,20 @@ func setCommonHeaders(setHeader func(key, value string)) {
|
|||||||
setHeader("Pragma", "no-cache")
|
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 {
|
if err != nil {
|
||||||
var tlsErr *tls.CertificateVerificationError
|
var tlsErr *tls.CertificateVerificationError
|
||||||
if ok := errors.As(err, &tlsErr); !ok {
|
if ok := errors.As(err, &tlsErr); !ok {
|
||||||
return types.HealthCheckResult{
|
return types.HealthCheckResult{
|
||||||
Latency: lat,
|
Latency: lat,
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
return types.HealthCheckResult{
|
return types.HealthCheckResult{
|
||||||
Latency: lat,
|
Latency: lat,
|
||||||
Healthy: true,
|
Healthy: true,
|
||||||
Detail: tlsErr.Error(),
|
Detail: tlsErr.Error(),
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusCode := getStatusCode()
|
statusCode := getStatusCode()
|
||||||
@@ -122,11 +122,11 @@ func processHealthResponse(lat time.Duration, err error, getStatusCode func() in
|
|||||||
return types.HealthCheckResult{
|
return types.HealthCheckResult{
|
||||||
Latency: lat,
|
Latency: lat,
|
||||||
Detail: http.StatusText(statusCode),
|
Detail: http.StatusText(statusCode),
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.HealthCheckResult{
|
return types.HealthCheckResult{
|
||||||
Latency: lat,
|
Latency: lat,
|
||||||
Healthy: true,
|
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:
|
format:
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,21 +106,21 @@ func (u *URL) Parse(v string) error {
|
|||||||
|
|
||||||
func (u *URL) parse(v string, checkExists bool) error {
|
func (u *URL) parse(v string, checkExists bool) error {
|
||||||
if v == "" {
|
if v == "" {
|
||||||
return ErrInvalidIconURL
|
return gperr.PrependSubject(ErrInvalidIconURL, "empty url")
|
||||||
}
|
}
|
||||||
slashIndex := strings.Index(v, "/")
|
slashIndex := strings.Index(v, "/")
|
||||||
if slashIndex == -1 {
|
if slashIndex == -1 {
|
||||||
return ErrInvalidIconURL
|
return gperr.PrependSubject(ErrInvalidIconURL, v)
|
||||||
}
|
}
|
||||||
beforeSlash := v[:slashIndex]
|
beforeSlash := v[:slashIndex]
|
||||||
switch beforeSlash {
|
switch beforeSlash {
|
||||||
case "http:", "https:":
|
case "http:", "https:":
|
||||||
u.FullURL = &v
|
u.FullURL = &v
|
||||||
u.Source = SourceAbsolute
|
u.Source = SourceAbsolute
|
||||||
case "@target", "": // @target/favicon.ico, /favicon.ico
|
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||||
url := v[slashIndex:]
|
url := v[slashIndex:]
|
||||||
if url == "/" {
|
if url == "/" {
|
||||||
return fmt.Errorf("%w: empty path", ErrInvalidIconURL)
|
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("%s", "empty path")
|
||||||
}
|
}
|
||||||
u.FullURL = &url
|
u.FullURL = &url
|
||||||
u.Source = SourceRelative
|
u.Source = SourceRelative
|
||||||
@@ -132,16 +132,16 @@ func (u *URL) parse(v string, checkExists bool) error {
|
|||||||
}
|
}
|
||||||
parts := strings.Split(v[slashIndex+1:], ".")
|
parts := strings.Split(v[slashIndex+1:], ".")
|
||||||
if len(parts) != 2 {
|
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])
|
reference, format := parts[0], strings.ToLower(parts[1])
|
||||||
if reference == "" || format == "" {
|
if reference == "" || format == "" {
|
||||||
return ErrInvalidIconURL
|
return gperr.PrependSubject(ErrInvalidIconURL, v).Withf("empty reference or format")
|
||||||
}
|
}
|
||||||
switch format {
|
switch format {
|
||||||
case "svg", "png", "webp":
|
case "svg", "png", "webp":
|
||||||
default:
|
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
|
isLight, isDark := false, false
|
||||||
if strings.HasSuffix(reference, "-light") {
|
if strings.HasSuffix(reference, "-light") {
|
||||||
@@ -159,7 +159,7 @@ func (u *URL) parse(v string, checkExists bool) error {
|
|||||||
IsDark: isDark,
|
IsDark: isDark,
|
||||||
}
|
}
|
||||||
if checkExists && !u.HasIcon() {
|
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:
|
default:
|
||||||
return gperr.PrependSubject(ErrInvalidIconURL, v)
|
return gperr.PrependSubject(ErrInvalidIconURL, v)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package qbittorrent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,18 +10,29 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||||
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
URL string
|
URL string
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password strutils.Redacted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
|
func (c *Client) Initialize(ctx context.Context, url string, cfg map[string]any) error {
|
||||||
c.URL = url
|
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)
|
_, err := c.Version(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37,7 +49,7 @@ func (c *Client) doRequest(ctx context.Context, method, endpoint string, query u
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Username != "" && c.Password != "" {
|
if c.Username != "" && c.Password != "" {
|
||||||
req.SetBasicAuth(c.Username, c.Password)
|
req.SetBasicAuth(c.Username, c.Password.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := widgets.HTTPClient.Do(req)
|
resp, err := widgets.HTTPClient.Do(req)
|
||||||
|
|||||||
@@ -62,6 +62,6 @@ func DebugHandler(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
w.writeLoadingPage(rw)
|
_ = w.writeLoadingPage(rw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
Config = types.IdlewatcherConfig
|
||||||
|
|
||||||
routeHelper struct {
|
routeHelper struct {
|
||||||
route types.Route
|
route types.Route
|
||||||
rp *reverseproxy.ReverseProxy
|
rp *reverseproxy.ReverseProxy
|
||||||
@@ -52,7 +54,7 @@ type (
|
|||||||
|
|
||||||
l zerolog.Logger
|
l zerolog.Logger
|
||||||
|
|
||||||
cfg *types.IdlewatcherConfig
|
cfg *Config
|
||||||
|
|
||||||
provider synk.Value[idlewatcher.Provider]
|
provider synk.Value[idlewatcher.Provider]
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ const reqTimeout = 3 * time.Second
|
|||||||
// prevents dependencies from being stopped automatically.
|
// prevents dependencies from being stopped automatically.
|
||||||
const neverTick = time.Duration(1<<63 - 1)
|
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()
|
key := cfg.Key()
|
||||||
|
|
||||||
watcherMapMu.RLock()
|
watcherMapMu.RLock()
|
||||||
@@ -193,7 +195,7 @@ func NewWatcher(parent task.Parent, r types.Route, cfg *types.IdlewatcherConfig)
|
|||||||
|
|
||||||
depCfg := depRoute.IdlewatcherConfig()
|
depCfg := depRoute.IdlewatcherConfig()
|
||||||
if depCfg == nil {
|
if depCfg == nil {
|
||||||
depCfg = new(types.IdlewatcherConfig)
|
depCfg = new(Config)
|
||||||
depCfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase
|
depCfg.IdlewatcherConfigBase = cfg.IdlewatcherConfigBase
|
||||||
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
|
depCfg.IdleTimeout = neverTick // disable auto sleep for dependencies
|
||||||
} else if depCfg.IdleTimeout > 0 && depCfg.IdleTimeout != neverTick {
|
} else if depCfg.IdleTimeout > 0 && depCfg.IdleTimeout != neverTick {
|
||||||
|
|||||||
@@ -17,16 +17,19 @@ type (
|
|||||||
} // @name AccessLoggerConfigBase
|
} // @name AccessLoggerConfigBase
|
||||||
ACLLoggerConfig struct {
|
ACLLoggerConfig struct {
|
||||||
ConfigBase
|
ConfigBase
|
||||||
|
|
||||||
LogAllowed bool `json:"log_allowed"`
|
LogAllowed bool `json:"log_allowed"`
|
||||||
} // @name ACLLoggerConfig
|
} // @name ACLLoggerConfig
|
||||||
RequestLoggerConfig struct {
|
RequestLoggerConfig struct {
|
||||||
ConfigBase
|
ConfigBase
|
||||||
|
|
||||||
Format Format `json:"format" validate:"oneof=common combined json"`
|
Format Format `json:"format" validate:"oneof=common combined json"`
|
||||||
Filters Filters `json:"filters"`
|
Filters Filters `json:"filters"`
|
||||||
Fields Fields `json:"fields"`
|
Fields Fields `json:"fields"`
|
||||||
} // @name RequestLoggerConfig
|
} // @name RequestLoggerConfig
|
||||||
Config struct {
|
Config struct {
|
||||||
ConfigBase
|
ConfigBase
|
||||||
|
|
||||||
acl *ACLLoggerConfig
|
acl *ACLLoggerConfig
|
||||||
req *RequestLoggerConfig
|
req *RequestLoggerConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileAccessLogger struct {
|
fileAccessLogger struct {
|
||||||
|
RequestFormatter
|
||||||
|
ACLLogFormatter
|
||||||
|
|
||||||
task *task.Task
|
task *task.Task
|
||||||
cfg *Config
|
cfg *Config
|
||||||
|
|
||||||
@@ -41,9 +44,6 @@ type (
|
|||||||
errRateLimiter *rate.Limiter
|
errRateLimiter *rate.Limiter
|
||||||
|
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
|
|
||||||
RequestFormatter
|
|
||||||
ACLLogFormatter
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ func (m *MockFile) Len() int64 {
|
|||||||
|
|
||||||
func (m *MockFile) Content() []byte {
|
func (m *MockFile) Content() []byte {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
m.Seek(0, io.SeekStart)
|
_, _ = m.Seek(0, io.SeekStart)
|
||||||
_, _ = buf.ReadFrom(m.File)
|
_, _ = buf.ReadFrom(m.File)
|
||||||
m.Seek(0, io.SeekStart)
|
_, _ = m.Seek(0, io.SeekStart)
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Retention struct {
|
type Retention struct {
|
||||||
Days uint64 `json:"days,omitempty"`
|
Days int64 `json:"days,omitempty" validate:"min=0"`
|
||||||
Last uint64 `json:"last,omitempty"`
|
Last int64 `json:"last,omitempty" validate:"min=0"`
|
||||||
KeepSize uint64 `json:"keep_size,omitempty"`
|
KeepSize int64 `json:"keep_size,omitempty" validate:"min=0"`
|
||||||
} // @name LogRetention
|
} // @name LogRetention
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -39,9 +39,9 @@ func (r *Retention) Parse(v string) (err error) {
|
|||||||
}
|
}
|
||||||
switch split[0] {
|
switch split[0] {
|
||||||
case "last":
|
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
|
default: // <N> days|weeks|months
|
||||||
n, err := strconv.ParseUint(split[0], 10, 64)
|
n, err := strconv.ParseInt(split[0], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention, result *Rotate
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case config.Last > 0:
|
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
|
// not needed to parse time for last N lines
|
||||||
case config.Days > 0:
|
case config.Days > 0:
|
||||||
cutoff := mockable.TimeNow().AddDate(0, 0, -int(config.Days)+1)
|
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
|
result.OriginalSize = fileSize
|
||||||
|
|
||||||
keepSize := int64(config.KeepSize)
|
keepSize := config.KeepSize
|
||||||
if keepSize >= fileSize {
|
if keepSize >= fileSize {
|
||||||
result.NumBytesKeep = fileSize
|
result.NumBytesKeep = fileSize
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -99,7 +100,7 @@ func (cfg *MaxMind) LoadMaxMindDB(parent task.Parent) error {
|
|||||||
|
|
||||||
if !valid {
|
if !valid {
|
||||||
cfg.Logger().Info().Msg("MaxMind DB not found/invalid, downloading...")
|
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)
|
return fmt.Errorf("%w: %w", ErrDownloadFailure, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -128,7 +129,7 @@ func (cfg *MaxMind) scheduleUpdate(parent task.Parent) {
|
|||||||
ticker := time.NewTicker(updateInterval)
|
ticker := time.NewTicker(updateInterval)
|
||||||
|
|
||||||
cfg.loadLastUpdate()
|
cfg.loadLastUpdate()
|
||||||
cfg.update()
|
cfg.update(task.Context())
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
@@ -143,15 +144,18 @@ func (cfg *MaxMind) scheduleUpdate(parent task.Parent) {
|
|||||||
case <-task.Context().Done():
|
case <-task.Context().Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
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
|
// check for update
|
||||||
cfg.Logger().Info().Msg("checking for MaxMind DB update...")
|
cfg.Logger().Info().Msg("checking for MaxMind DB update...")
|
||||||
remoteLastModified, err := cfg.checkLastest()
|
remoteLastModified, err := cfg.checkLastest(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg.Logger().Err(err).Msg("failed to check MaxMind DB update")
|
cfg.Logger().Err(err).Msg("failed to check MaxMind DB update")
|
||||||
return
|
return
|
||||||
@@ -165,15 +169,15 @@ func (cfg *MaxMind) update() {
|
|||||||
Time("latest", remoteLastModified.Local()).
|
Time("latest", remoteLastModified.Local()).
|
||||||
Time("current", cfg.lastUpdate).
|
Time("current", cfg.lastUpdate).
|
||||||
Msg("MaxMind DB update available")
|
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")
|
cfg.Logger().Err(err).Msg("failed to update MaxMind DB")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg.Logger().Info().Msg("MaxMind DB updated")
|
cfg.Logger().Info().Msg("MaxMind DB updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *MaxMind) doReq(method string) (*http.Response, error) {
|
func (cfg *MaxMind) doReq(ctx context.Context, method string) (*http.Response, error) {
|
||||||
req, err := http.NewRequest(method, cfg.dbURL(), nil)
|
req, err := http.NewRequestWithContext(ctx, method, cfg.dbURL(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -185,34 +189,36 @@ func (cfg *MaxMind) doReq(method string) (*http.Response, error) {
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *MaxMind) checkLastest() (lastModifiedT *time.Time, err error) {
|
func (cfg *MaxMind) checkLastest(ctx context.Context) (lastModifiedT time.Time, err error) {
|
||||||
resp, err := cfg.doReq(http.MethodHead)
|
resp, err := cfg.doReq(ctx, http.MethodHead)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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")
|
lastModified := resp.Header.Get("Last-Modified")
|
||||||
if lastModified == "" {
|
if lastModified == "" {
|
||||||
cfg.Logger().Warn().Msg("MaxMind responded no last modified time, update skipped")
|
return time.Time{}, nil
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
|
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg.Logger().Warn().Err(err).Msg("MaxMind responded invalid last modified time, update skipped")
|
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 {
|
func (cfg *MaxMind) download(ctx context.Context) error {
|
||||||
resp, err := cfg.doReq(http.MethodGet)
|
ctx, cancel := context.WithTimeout(ctx, updateTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resp, err := cfg.doReq(ctx, http.MethodGet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func mockMaxMindDBOpen(t *testing.T) {
|
|||||||
func Test_MaxMindConfig_doReq(t *testing.T) {
|
func Test_MaxMindConfig_doReq(t *testing.T) {
|
||||||
cfg := testCfg()
|
cfg := testCfg()
|
||||||
mockDoReq(t, cfg)
|
mockDoReq(t, cfg)
|
||||||
resp, err := cfg.doReq(http.MethodGet)
|
resp, err := cfg.doReq(t.Context(), http.MethodGet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("newReq() error = %v", err)
|
t.Fatalf("newReq() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func Test_MaxMindConfig_checkLatest(t *testing.T) {
|
|||||||
cfg := testCfg()
|
cfg := testCfg()
|
||||||
mockDoReq(t, cfg)
|
mockDoReq(t, cfg)
|
||||||
|
|
||||||
latest, err := cfg.checkLastest()
|
latest, err := cfg.checkLastest(t.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("checkLatest() error = %v", err)
|
t.Fatalf("checkLatest() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ func Test_MaxMindConfig_download(t *testing.T) {
|
|||||||
mockMaxMindDBOpen(t)
|
mockMaxMindDBOpen(t)
|
||||||
mockDoReq(t, cfg)
|
mockDoReq(t, cfg)
|
||||||
|
|
||||||
err := cfg.download()
|
err := cfg.download(t.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("download() error = %v", err)
|
t.Fatalf("download() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,19 +51,6 @@ const (
|
|||||||
SystemInfoAggregateModeSensorTemperature SystemInfoAggregateMode = "sensor_temperature" // @name SystemInfoAggregateModeSensorTemperature
|
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)
|
var Poller = period.NewPoller("system_info", getSystemInfo, aggregate)
|
||||||
|
|
||||||
func isNoDataAvailable(err error) bool {
|
func isNoDataAvailable(err error) bool {
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ func TestSerialize(t *testing.T) {
|
|||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
entries[i] = testInfo
|
entries[i] = testInfo
|
||||||
}
|
}
|
||||||
|
var allQueries = []SystemInfoAggregateMode{
|
||||||
|
SystemInfoAggregateModeCPUAverage,
|
||||||
|
SystemInfoAggregateModeMemoryUsage,
|
||||||
|
SystemInfoAggregateModeMemoryUsagePercent,
|
||||||
|
SystemInfoAggregateModeDisksReadSpeed,
|
||||||
|
SystemInfoAggregateModeDisksWriteSpeed,
|
||||||
|
SystemInfoAggregateModeDisksIOPS,
|
||||||
|
SystemInfoAggregateModeDiskUsage,
|
||||||
|
SystemInfoAggregateModeNetworkSpeed,
|
||||||
|
SystemInfoAggregateModeNetworkTransfer,
|
||||||
|
SystemInfoAggregateModeSensorTemperature,
|
||||||
|
}
|
||||||
for _, query := range allQueries {
|
for _, query := range allQueries {
|
||||||
t.Run(string(query), func(t *testing.T) {
|
t.Run(string(query), func(t *testing.T) {
|
||||||
_, result := aggregate(entries, url.Values{"aggregate": []string{string(query)}})
|
_, result := aggregate(entries, url.Values{"aggregate": []string{string(query)}})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package uptime
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
@@ -70,7 +71,7 @@ func aggregateStatuses(entries []StatusByAlias, query url.Values) (int, Aggregat
|
|||||||
for alias, status := range entry.Map {
|
for alias, status := range entry.Map {
|
||||||
statuses[alias] = append(statuses[alias], Status{
|
statuses[alias] = append(statuses[alias], Status{
|
||||||
Status: status.Status,
|
Status: status.Status,
|
||||||
Latency: int32(status.Latency.Milliseconds()),
|
Latency: int32(min(math.MaxInt32, status.Latency.Milliseconds())), //nolint:gosec
|
||||||
Timestamp: entry.Timestamp,
|
Timestamp: entry.Timestamp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -134,7 +135,6 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
|||||||
|
|
||||||
status := types.StatusUnknown
|
status := types.StatusUnknown
|
||||||
if state := config.ActiveState.Load(); state != nil {
|
if state := config.ActiveState.Load(); state != nil {
|
||||||
// FIXME: pass ctx to getRoute
|
|
||||||
r, ok := entrypoint.FromCtx(state.Context()).GetRoute(alias)
|
r, ok := entrypoint.FromCtx(state.Context()).GetRoute(alias)
|
||||||
if ok {
|
if ok {
|
||||||
mon := r.HealthMonitor()
|
mon := r.HealthMonitor()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type roundRobin struct {
|
type roundRobin struct {
|
||||||
index atomic.Uint32
|
index atomic.Uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ impl = (*roundRobin)(nil)
|
var _ impl = (*roundRobin)(nil)
|
||||||
@@ -21,6 +21,6 @@ func (lb *roundRobin) ChooseServer(srvs types.LoadBalancerServers, r *http.Reque
|
|||||||
if len(srvs) == 0 {
|
if len(srvs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
index := (lb.index.Add(1) - 1) % uint32(len(srvs))
|
index := (lb.index.Add(1) - 1) % uint64(len(srvs))
|
||||||
return srvs[index]
|
return srvs[index]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ type HcaptchaProvider struct {
|
|||||||
Secret string `json:"secret" validate:"required"`
|
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 {
|
func (p *HcaptchaProvider) CSPDirectives() []string {
|
||||||
return []string{"script-src", "frame-src", "style-src", "connect-src"}
|
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 {
|
func (p *HcaptchaProvider) CSPSources() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"https://hcaptcha.com",
|
"https://hcaptcha.com",
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Middleware struct {
|
Middleware struct {
|
||||||
|
commonOptions
|
||||||
|
|
||||||
name string
|
name string
|
||||||
construct ImplNewFunc
|
construct ImplNewFunc
|
||||||
impl any
|
impl any
|
||||||
|
|
||||||
commonOptions
|
|
||||||
}
|
}
|
||||||
ByPriority []*Middleware
|
ByPriority []*Middleware
|
||||||
|
|
||||||
@@ -196,7 +196,12 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
|
|||||||
|
|
||||||
if exec, ok := m.impl.(ResponseModifier); ok {
|
if exec, ok := m.impl.(ResponseModifier); ok {
|
||||||
lrm := httputils.NewLazyResponseModifier(w, needsBuffering)
|
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)
|
next(lrm, r)
|
||||||
|
|
||||||
// Skip modification if response wasn't buffered (non-HTML content)
|
// 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
|
// override the content length and body if changed
|
||||||
if currentResp.Body != currentBody {
|
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 {
|
} else {
|
||||||
next(w, r)
|
next(w, r)
|
||||||
@@ -239,12 +246,14 @@ func needsBuffering(header http.Header) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {
|
func (m *Middleware) LogWarn(req *http.Request) *zerolog.Event {
|
||||||
|
//nolint:zerologlint
|
||||||
return log.Warn().Str("middleware", m.name).
|
return log.Warn().Str("middleware", m.name).
|
||||||
Str("host", req.Host).
|
Str("host", req.Host).
|
||||||
Str("path", req.URL.Path)
|
Str("path", req.URL.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Middleware) LogError(req *http.Request) *zerolog.Event {
|
func (m *Middleware) LogError(req *http.Request) *zerolog.Event {
|
||||||
|
//nolint:zerologlint
|
||||||
return log.Error().Str("middleware", m.name).
|
return log.Error().Str("middleware", m.name).
|
||||||
Str("host", req.Host).
|
Str("host", req.Host).
|
||||||
Str("path", req.URL.Path)
|
Str("path", req.URL.Path)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
//nolint:recvcheck
|
||||||
FieldsBody []LogField
|
FieldsBody []LogField
|
||||||
ListBody []string
|
ListBody []string
|
||||||
MessageBody string
|
MessageBody string
|
||||||
@@ -106,6 +107,7 @@ func (m MessageBodyBytes) Format(format LogFormat) ([]byte, error) {
|
|||||||
switch format {
|
switch format {
|
||||||
case LogFormatRawJSON:
|
case LogFormatRawJSON:
|
||||||
return sonic.Marshal(string(m))
|
return sonic.Marshal(string(m))
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Ntfy is a provider for ntfy.
|
||||||
|
//
|
||||||
// See https://docs.ntfy.sh/publish
|
// See https://docs.ntfy.sh/publish
|
||||||
type Ntfy struct {
|
type Ntfy struct {
|
||||||
ProviderBase
|
ProviderBase
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package proxmox
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
"github.com/luthermonson/go-proxmox"
|
"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.
|
// closeTransportConnections forces close idle HTTP connections to prevent goroutine leaks.
|
||||||
// This is needed because the go-proxmox library's TermWebSocket closer doesn't close
|
// This is needed because the go-proxmox library's TermWebSocket closer doesn't close
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package route
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
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)
|
ep := entrypoint.FromCtx(ctx)
|
||||||
if ep == nil {
|
if ep == nil {
|
||||||
return fmt.Errorf("entrypoint not found in context")
|
return errors.New("entrypoint not found in context")
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
existing types.Route
|
existing types.Route
|
||||||
|
|||||||
@@ -372,6 +372,9 @@ func (r *Route) validateRules() error {
|
|||||||
r.Rules = rules
|
r.Rules = rules
|
||||||
}
|
}
|
||||||
case "file", "":
|
case "file", "":
|
||||||
|
if !strutils.IsValidFilename(src.Path) {
|
||||||
|
return fmt.Errorf("invalid rule file path %q", src.Path)
|
||||||
|
}
|
||||||
content, err := os.ReadFile(src.Path)
|
content, err := os.ReadFile(src.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read rule file %q: %w", src.Path, err)
|
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
|
// we don't want to copy the request object every fucking requests
|
||||||
// return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
|
// return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
|
||||||
ctxFieldPtr := (*context.Context)(unsafe.Add(unsafe.Pointer(r), ctxFieldOffset))
|
ctxFieldPtr := (*context.Context)(unsafe.Add(unsafe.Pointer(r), ctxFieldOffset))
|
||||||
|
//nolint:fatcontext
|
||||||
*ctxFieldPtr = &RouteContext{
|
*ctxFieldPtr = &RouteContext{
|
||||||
Context: r.Context(),
|
Context: r.Context(),
|
||||||
Route: route,
|
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>
|
rewrite <from> <to>
|
||||||
from: the path to rewrite, must start with /
|
from: the path to rewrite, must start with /
|
||||||
|
|||||||
@@ -158,13 +158,14 @@ func parse(v string) (subject string, args []string, err error) {
|
|||||||
buf.WriteRune('$')
|
buf.WriteRune('$')
|
||||||
}
|
}
|
||||||
|
|
||||||
if quote != 0 {
|
switch {
|
||||||
|
case quote != 0:
|
||||||
err = ErrUnterminatedQuotes
|
err = ErrUnterminatedQuotes
|
||||||
} else if brackets != 0 {
|
case brackets != 0:
|
||||||
err = ErrUnterminatedBrackets
|
err = ErrUnterminatedBrackets
|
||||||
} else if inEnvVar {
|
case inEnvVar:
|
||||||
err = ErrUnterminatedEnvVar
|
err = ErrUnterminatedEnvVar
|
||||||
} else {
|
default:
|
||||||
flush(false)
|
flush(false)
|
||||||
}
|
}
|
||||||
if len(missingEnvVars) > 0 {
|
if len(missingEnvVars) > 0 {
|
||||||
|
|||||||
@@ -286,8 +286,7 @@ func logError(err error, r *http.Request) {
|
|||||||
var h2Err http2.StreamError
|
var h2Err http2.StreamError
|
||||||
if errors.As(err, &h2Err) {
|
if errors.As(err, &h2Err) {
|
||||||
// ignore these errors
|
// ignore these errors
|
||||||
switch h2Err.Code {
|
if h2Err.Code == http2.ErrCodeStreamClosed {
|
||||||
case http2.ErrCodeStreamClosed:
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func NeedExpandVars(s string) bool {
|
|||||||
var (
|
var (
|
||||||
voidResponseModifier = httputils.NewResponseModifier(httptest.NewRecorder())
|
voidResponseModifier = httputils.NewResponseModifier(httptest.NewRecorder())
|
||||||
dummyRequest = http.Request{
|
dummyRequest = http.Request{
|
||||||
Method: "GET",
|
Method: http.MethodGet,
|
||||||
URL: &url.URL{Path: "/"},
|
URL: &url.URL{Path: "/"},
|
||||||
Header: http.Header{},
|
Header: http.Header{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"github.com/yusing/goutils/task"
|
"github.com/yusing/goutils/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewStartedTestRoute(t testing.TB, base *Route) (types.Route, error) {
|
func NewStartedTestRoute(tb testing.TB, base *Route) (types.Route, error) {
|
||||||
t.Helper()
|
tb.Helper()
|
||||||
|
|
||||||
task := task.GetTestTask(t)
|
task := task.GetTestTask(tb)
|
||||||
if ep := epctx.FromCtx(task.Context()); ep == nil {
|
if ep := epctx.FromCtx(task.Context()); ep == nil {
|
||||||
ep = entrypoint.NewEntrypoint(task, nil)
|
ep = entrypoint.NewEntrypoint(task, nil)
|
||||||
epctx.SetCtx(task, ep)
|
epctx.SetCtx(task, ep)
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ type HTTPConfig struct {
|
|||||||
|
|
||||||
// BuildTLSConfig creates a TLS configuration based on the HTTP config options.
|
// BuildTLSConfig creates a TLS configuration based on the HTTP config options.
|
||||||
func (cfg *HTTPConfig) BuildTLSConfig(targetURL *url.URL) (*tls.Config, error) {
|
func (cfg *HTTPConfig) BuildTLSConfig(targetURL *url.URL) (*tls.Config, error) {
|
||||||
//nolint:gosec
|
|
||||||
tlsConfig := &tls.Config{}
|
tlsConfig := &tls.Config{}
|
||||||
|
|
||||||
// Handle InsecureSkipVerify (legacy NoTLSVerify option)
|
// Handle InsecureSkipVerify (legacy NoTLSVerify option)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
gperr "github.com/yusing/goutils/errs"
|
gperr "github.com/yusing/goutils/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint:recvcheck
|
||||||
type Scheme uint8
|
type Scheme uint8
|
||||||
|
|
||||||
var ErrInvalidScheme = errors.New("invalid scheme")
|
var ErrInvalidScheme = errors.New("invalid scheme")
|
||||||
|
|||||||
@@ -41,20 +41,18 @@ func ValidateWithCustomValidator(v reflect.Value) error {
|
|||||||
if elemType.Implements(validatorType) {
|
if elemType.Implements(validatorType) {
|
||||||
return v.Elem().Interface().(CustomValidator).Validate()
|
return v.Elem().Interface().(CustomValidator).Validate()
|
||||||
}
|
}
|
||||||
} else {
|
} else if vt.PkgPath() != "" { // not a builtin type
|
||||||
if vt.PkgPath() != "" { // not a builtin type
|
// prioritize pointer method
|
||||||
// prioritize pointer method
|
if v.CanAddr() {
|
||||||
if v.CanAddr() {
|
vAddr := v.Addr()
|
||||||
vAddr := v.Addr()
|
if vAddr.Type().Implements(validatorType) {
|
||||||
if vAddr.Type().Implements(validatorType) {
|
return vAddr.Interface().(CustomValidator).Validate()
|
||||||
return vAddr.Interface().(CustomValidator).Validate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// fallback to value method
|
|
||||||
if vt.Implements(validatorType) {
|
|
||||||
return v.Interface().(CustomValidator).Validate()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// fallback to value method
|
||||||
|
if vt.Implements(validatorType) {
|
||||||
|
return v.Interface().(CustomValidator).Validate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
//nolint:recvcheck
|
||||||
HealthStatus uint8 // @name HealthStatus
|
HealthStatus uint8 // @name HealthStatus
|
||||||
HealthStatusString string // @name HealthStatusString
|
HealthStatusString string // @name HealthStatusString
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ type (
|
|||||||
|
|
||||||
HealthInfo struct {
|
HealthInfo struct {
|
||||||
HealthInfoWithoutDetail
|
HealthInfoWithoutDetail
|
||||||
|
|
||||||
Detail string `json:"detail"`
|
Detail string `json:"detail"`
|
||||||
} // @name HealthInfo
|
} // @name HealthInfo
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user