refactor and organize code

This commit is contained in:
yusing 2025-02-15 05:44:47 +08:00
parent 1af6dd9cf8
commit 18d258aaa2
169 changed files with 1020 additions and 755 deletions

View file

@ -6,7 +6,7 @@ import (
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env" "github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/server" "github.com/yusing/go-proxy/agent/pkg/server"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg" "github.com/yusing/go-proxy/pkg"
@ -16,7 +16,7 @@ func main() {
args := os.Args args := os.Args
if len(args) > 1 && args[1] == "migrate" { if len(args) > 1 && args[1] == "migrate" {
if err := agent.MigrateFromOld(); err != nil { if err := agent.MigrateFromOld(); err != nil {
E.LogFatal("failed to migrate from old docker compose", err) gperr.LogFatal("failed to migrate from old docker compose", err)
} }
return return
} }
@ -24,21 +24,21 @@ func main() {
ca := &agent.PEMPair{} ca := &agent.PEMPair{}
err := ca.Load(env.AgentCACert) err := ca.Load(env.AgentCACert)
if err != nil { if err != nil {
E.LogFatal("init CA error", err) gperr.LogFatal("init CA error", err)
} }
caCert, err := ca.ToTLSCert() caCert, err := ca.ToTLSCert()
if err != nil { if err != nil {
E.LogFatal("init CA error", err) gperr.LogFatal("init CA error", err)
} }
srv := &agent.PEMPair{} srv := &agent.PEMPair{}
srv.Load(env.AgentSSLCert) srv.Load(env.AgentSSLCert)
if err != nil { if err != nil {
E.LogFatal("init SSL error", err) gperr.LogFatal("init SSL error", err)
} }
srvCert, err := srv.ToTLSCert() srvCert, err := srv.ToTLSCert()
if err != nil { if err != nil {
E.LogFatal("init SSL error", err) gperr.LogFatal("init SSL error", err)
} }
logging.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion()) logging.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())

View file

@ -12,9 +12,9 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yusing/go-proxy/agent/pkg/certs" "github.com/yusing/go-proxy/agent/pkg/certs"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg" "github.com/yusing/go-proxy/pkg"
@ -80,17 +80,17 @@ func checkVersion(a, b string) bool {
return withoutBuildTime(a) == withoutBuildTime(b) return withoutBuildTime(a) == withoutBuildTime(b)
} }
func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) E.Error { func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) error {
clientCert, err := tls.X509KeyPair(crt, key) clientCert, err := tls.X509KeyPair(crt, key)
if err != nil { if err != nil {
return E.Wrap(err) return err
} }
// create tls config // create tls config
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(ca) ok := caCertPool.AppendCertsFromPEM(ca)
if !ok { if !ok {
return E.New("invalid CA certificate") return gperr.New("invalid CA certificate")
} }
cfg.tlsConfig = &tls.Config{ cfg.tlsConfig = &tls.Config{
@ -108,17 +108,17 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
// check agent version // check agent version
version, _, err := cfg.Fetch(ctx, EndpointVersion) version, _, err := cfg.Fetch(ctx, EndpointVersion)
if err != nil { if err != nil {
return E.Wrap(err) return err
} }
if !checkVersion(string(version), pkg.GetVersion()) { if !checkVersion(string(version), pkg.GetVersion()) {
return E.Errorf("agent version mismatch: server: %s, agent: %s", pkg.GetVersion(), string(version)) return gperr.Errorf("agent version mismatch: server: %s, agent: %s", pkg.GetVersion(), string(version))
} }
// get agent name // get agent name
name, _, err := cfg.Fetch(ctx, EndpointName) name, _, err := cfg.Fetch(ctx, EndpointName)
if err != nil { if err != nil {
return E.Wrap(err) return err
} }
cfg.name = string(name) cfg.name = string(name)
@ -128,18 +128,18 @@ func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte)
return nil return nil
} }
func (cfg *AgentConfig) Start(parent task.Parent) E.Error { func (cfg *AgentConfig) Start(parent task.Parent) error {
certData, err := os.ReadFile(certs.AgentCertsFilename(cfg.Addr)) certData, err := os.ReadFile(certs.AgentCertsFilename(cfg.Addr))
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return E.Errorf("agents certs not found, did you run `godoxy new-agent %s ...`?", cfg.Addr) return gperr.Errorf("agents certs not found, did you run `godoxy new-agent %s ...`?", cfg.Addr)
} }
return E.Wrap(err) return gperr.Wrap(err)
} }
ca, crt, key, err := certs.ExtractCert(certData) ca, crt, key, err := certs.ExtractCert(certData)
if err != nil { if err != nil {
return E.Wrap(err) return gperr.Wrap(err)
} }
return cfg.StartWithCerts(parent, ca, crt, key) return cfg.StartWithCerts(parent, ca, crt, key)

View file

@ -9,7 +9,7 @@ import (
_ "embed" _ "embed"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -110,11 +110,11 @@ func MigrateFromOld() error {
composeConfig.SSLCert = agentCert.String() composeConfig.SSLCert = agentCert.String()
composeTemplate, err := composeConfig.Generate() composeTemplate, err := composeConfig.Generate()
if err != nil { if err != nil {
return E.Wrap(err, "failed to generate new docker compose") return gperr.Wrap(err, "failed to generate new docker compose")
} }
if err := os.WriteFile("/app/compose.yml", []byte(composeTemplate), 0600); err != nil { if err := os.WriteFile("/app/compose.yml", []byte(composeTemplate), 0600); err != nil {
return E.Wrap(err, "failed to write new docker compose") return gperr.Wrap(err, "failed to write new docker compose")
} }
logging.Info().Msg("Migrated from old docker compose:") logging.Info().Msg("Migrated from old docker compose:")

View file

@ -7,7 +7,7 @@ import (
"os" "os"
"strings" "strings"
apiUtils "github.com/yusing/go-proxy/internal/api/v1/utils" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health"
"github.com/yusing/go-proxy/internal/watcher/health/monitor" "github.com/yusing/go-proxy/internal/watcher/health/monitor"
@ -72,5 +72,5 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
return return
} }
apiUtils.RespondJSON(w, r, result) gphttp.RespondJSON(w, r, result)
} }

View file

@ -8,7 +8,7 @@ import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/reverseproxy" "github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
) )

View file

@ -8,10 +8,10 @@ import (
"time" "time"
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
agentproxy "github.com/yusing/go-proxy/agent/pkg/agentproxy" "github.com/yusing/go-proxy/agent/pkg/agentproxy"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/http" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/http/reverseproxy" "github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )

View file

@ -14,7 +14,7 @@ import (
"github.com/yusing/go-proxy/agent/pkg/env" "github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/handler" "github.com/yusing/go-proxy/agent/pkg/handler"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/server" "github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
) )

View file

@ -11,10 +11,10 @@ import (
"github.com/yusing/go-proxy/internal/api/v1/query" "github.com/yusing/go-proxy/internal/api/v1/query"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config" "github.com/yusing/go-proxy/internal/config"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/routes/routequery" "github.com/yusing/go-proxy/internal/route/routes/routequery"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg" "github.com/yusing/go-proxy/pkg"
@ -32,7 +32,7 @@ func main() {
return return
case common.CommandReload: case common.CommandReload:
if err := query.ReloadServer(); err != nil { if err := query.ReloadServer(); err != nil {
E.LogFatal("server reload error", err) gperr.LogFatal("server reload error", err)
} }
rawLogger.Println("ok") rawLogger.Println("ok")
return return
@ -88,9 +88,9 @@ func main() {
middleware.LoadComposeFiles() middleware.LoadComposeFiles()
var cfg *config.Config var cfg *config.Config
var err E.Error var err gperr.Error
if cfg, err = config.Load(); err != nil { if cfg, err = config.Load(); err != nil {
E.LogWarn("errors in config", err) gperr.LogWarn("errors in config", err)
} }
switch args.Command { switch args.Command {

View file

@ -13,7 +13,7 @@ import (
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger" "github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/uptime" "github.com/yusing/go-proxy/internal/metrics/uptime"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )

View file

@ -6,18 +6,19 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
) )
func ListAgents(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { func ListAgents(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) { if httpheaders.IsWebsocket(r.Header) {
U.PeriodicWS(w, r, 10*time.Second, func(conn *websocket.Conn) error { gpwebsocket.Periodic(w, r, 10*time.Second, func(conn *websocket.Conn) error {
wsjson.Write(r.Context(), conn, cfg.ListAgents()) wsjson.Write(r.Context(), conn, cfg.ListAgents())
return nil return nil
}) })
} else { } else {
U.RespondJSON(w, r, cfg.ListAgents()) gphttp.RespondJSON(w, r, cfg.ListAgents())
} }
} }

View file

@ -3,9 +3,9 @@ package auth
import ( import (
"net/http" "net/http"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp"
) )
var defaultAuth Provider var defaultAuth Provider
@ -44,7 +44,7 @@ func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
if IsEnabled() { if IsEnabled() {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if err := defaultAuth.CheckToken(r); err != nil { if err := defaultAuth.CheckToken(r); err != nil {
U.RespondError(w, err, http.StatusUnauthorized) gphttp.ClientError(w, err, http.StatusUnauthorized)
} else { } else {
next(w, r) next(w, r)
} }

View file

@ -12,9 +12,8 @@ import (
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/net/gphttp"
CE "github.com/yusing/go-proxy/internal/utils" CE "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -154,7 +153,7 @@ func generateState() (string, error) {
func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) { func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
state, err := generateState() state, err := generateState()
if err != nil { if err != nil {
U.HandleErr(w, r, err, http.StatusInternalServerError) gphttp.ServerError(w, r, err)
return return
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
@ -171,7 +170,7 @@ func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Reque
if auth.isMiddleware { if auth.isMiddleware {
u, err := r.URL.Parse(redirURL) u, err := r.URL.Parse(redirURL)
if err != nil { if err != nil {
U.HandleErr(w, r, err, http.StatusInternalServerError) gphttp.ServerError(w, r, err)
return return
} }
q := u.Query() q := u.Query()
@ -201,31 +200,31 @@ func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Re
state, err := r.Cookie(CookieOauthState) state, err := r.Cookie(CookieOauthState)
if err != nil { if err != nil {
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest) gphttp.BadRequest(w, "missing state cookie")
return return
} }
query := r.URL.Query() query := r.URL.Query()
if query.Get("state") != state.Value { if query.Get("state") != state.Value {
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest) gphttp.BadRequest(w, "invalid oauth state")
return return
} }
oauth2Token, err := auth.exchange(r) oauth2Token, err := auth.exchange(r)
if err != nil { if err != nil {
U.HandleErr(w, r, fmt.Errorf("failed to exchange token: %w", err), http.StatusInternalServerError) gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
return return
} }
rawIDToken, ok := oauth2Token.Extra("id_token").(string) rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok { if !ok {
U.HandleErr(w, r, E.New("missing id_token"), http.StatusInternalServerError) gphttp.BadRequest(w, "missing id_token")
return return
} }
idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken) idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken)
if err != nil { if err != nil {
U.HandleErr(w, r, fmt.Errorf("failed to verify ID token: %w", err), http.StatusInternalServerError) gphttp.ServerError(w, r, fmt.Errorf("failed to verify ID token: %w", err))
return return
} }
@ -243,7 +242,7 @@ func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.R
token, err := r.Cookie(auth.TokenCookieName()) token, err := r.Cookie(auth.TokenCookieName())
if err != nil { if err != nil {
U.HandleErr(w, r, E.New("missing token cookie"), http.StatusBadRequest) gphttp.BadRequest(w, "missing token cookie")
return return
} }
clearTokenCookie(w, r, auth.TokenCookieName()) clearTokenCookie(w, r, auth.TokenCookieName())
@ -258,12 +257,12 @@ func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.R
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) { func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
state, err := r.Cookie(CookieOauthState) state, err := r.Cookie(CookieOauthState)
if err != nil { if err != nil {
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest) gphttp.BadRequest(w, "missing state cookie")
return return
} }
if r.URL.Query().Get("state") != state.Value { if r.URL.Query().Get("state") != state.Value {
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest) gphttp.BadRequest(w, "invalid oauth state")
return return
} }

View file

@ -7,16 +7,16 @@ import (
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
var ( var (
ErrInvalidUsername = E.New("invalid username") ErrInvalidUsername = gperr.New("invalid username")
ErrInvalidPassword = E.New("invalid password") ErrInvalidPassword = gperr.New("invalid password")
) )
type ( type (
@ -94,7 +94,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
case claims.Username != auth.username: case claims.Username != auth.username:
return ErrUserNotAllowed.Subject(claims.Username) return ErrUserNotAllowed.Subject(claims.Username)
case claims.ExpiresAt.Before(time.Now()): case claims.ExpiresAt.Before(time.Now()):
return E.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time)) return gperr.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
} }
return nil return nil
@ -111,17 +111,16 @@ func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Re
} }
err := json.NewDecoder(r.Body).Decode(&creds) err := json.NewDecoder(r.Body).Decode(&creds)
if err != nil { if err != nil {
U.HandleErr(w, r, err, http.StatusBadRequest) gphttp.Unauthorized(w, "invalid credentials")
return return
} }
if err := auth.validatePassword(creds.User, creds.Pass); err != nil { if err := auth.validatePassword(creds.User, creds.Pass); err != nil {
U.LogError(r).Err(err).Msg("auth: invalid credentials") gphttp.Unauthorized(w, "invalid credentials")
U.RespondError(w, E.New("invalid credentials"), http.StatusUnauthorized)
return return
} }
token, err := auth.NewToken() token, err := auth.NewToken()
if err != nil { if err != nil {
U.HandleErr(w, r, err, http.StatusInternalServerError) gphttp.ServerError(w, r, err)
return return
} }
setTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL) setTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)

View file

@ -5,14 +5,14 @@ import (
"net/http" "net/http"
"time" "time"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
var ( var (
ErrMissingToken = E.New("missing token") ErrMissingToken = gperr.New("missing token")
ErrInvalidToken = E.New("invalid token") ErrInvalidToken = gperr.New("invalid token")
ErrUserNotAllowed = E.New("user not allowed") ErrUserNotAllowed = gperr.New("user not allowed")
) )
// cookieFQDN returns the fully qualified domain name of the request host // cookieFQDN returns the fully qualified domain name of the request host

View file

@ -13,10 +13,10 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/vincent-petithory/dataurl" "github.com/vincent-petithory/dataurl"
U "github.com/yusing/go-proxy/internal/api/v1/utils" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/route/routes" "github.com/yusing/go-proxy/internal/route/routes"
route "github.com/yusing/go-proxy/internal/route/types" route "github.com/yusing/go-proxy/internal/route/types"
) )
@ -53,11 +53,11 @@ func (res *fetchResult) ContentType() string {
func GetFavIcon(w http.ResponseWriter, req *http.Request) { func GetFavIcon(w http.ResponseWriter, req *http.Request) {
url, alias := req.FormValue("url"), req.FormValue("alias") url, alias := req.FormValue("url"), req.FormValue("alias")
if url == "" && alias == "" { if url == "" && alias == "" {
U.RespondError(w, U.ErrMissingKey("url or alias"), http.StatusBadRequest) gphttp.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest)
return return
} }
if url != "" && alias != "" { if url != "" && alias != "" {
U.RespondError(w, U.ErrInvalidKey("url and alias are mutually exclusive"), http.StatusBadRequest) gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest)
return return
} }
@ -65,7 +65,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
if url != "" { if url != "" {
var iconURL homepage.IconURL var iconURL homepage.IconURL
if err := iconURL.Parse(url); err != nil { if err := iconURL.Parse(url); err != nil {
U.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
fetchResult := getFavIconFromURL(&iconURL) fetchResult := getFavIconFromURL(&iconURL)
@ -74,14 +74,14 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
return return
} }
w.Header().Set("Content-Type", fetchResult.ContentType()) w.Header().Set("Content-Type", fetchResult.ContentType())
U.WriteBody(w, fetchResult.icon) gphttp.WriteBody(w, fetchResult.icon)
return return
} }
// try with route.Homepage.Icon // try with route.Homepage.Icon
r, ok := routes.GetHTTPRoute(alias) r, ok := routes.GetHTTPRoute(alias)
if !ok { if !ok {
U.RespondError(w, errors.New("no such route"), http.StatusNotFound) gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
return return
} }
@ -105,7 +105,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
return return
} }
w.Header().Set("Content-Type", result.ContentType()) w.Header().Set("Content-Type", result.ContentType())
U.WriteBody(w, result.icon) gphttp.WriteBody(w, result.icon)
} }
func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult { func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult {
@ -125,7 +125,7 @@ func fetchIconAbsolute(url string) *fetchResult {
return result return result
} }
resp, err := U.Get(url) resp, err := gphttp.Get(url)
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
if err == nil { if err == nil {
err = errors.New(resp.Status) err = errors.New(resp.Status)

View file

@ -7,11 +7,11 @@ import (
"path" "path"
"strings" "strings"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/provider" "github.com/yusing/go-proxy/internal/route/provider"
) )
@ -51,12 +51,12 @@ func (t FileType) GetPath(filename string) string {
func getArgs(r *http.Request) (fileType FileType, filename string, err error) { func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
fileType = FileType(r.PathValue("type")) fileType = FileType(r.PathValue("type"))
if !fileType.IsValid() { if !fileType.IsValid() {
err = U.ErrInvalidKey("type") err = gphttp.ErrInvalidKey("type")
return return
} }
filename = r.PathValue("filename") filename = r.PathValue("filename")
if filename == "" { if filename == "" {
err = U.ErrMissingKey("filename") err = gphttp.ErrMissingKey("filename")
} }
return return
} }
@ -64,23 +64,23 @@ func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
func GetFileContent(w http.ResponseWriter, r *http.Request) { func GetFileContent(w http.ResponseWriter, r *http.Request) {
fileType, filename, err := getArgs(r) fileType, filename, err := getArgs(r)
if err != nil { if err != nil {
U.RespondError(w, err, http.StatusBadRequest) gphttp.BadRequest(w, err.Error())
return return
} }
content, err := os.ReadFile(fileType.GetPath(filename)) content, err := os.ReadFile(fileType.GetPath(filename))
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
U.WriteBody(w, content) gphttp.WriteBody(w, content)
} }
func validateFile(fileType FileType, content []byte) error { func validateFile(fileType FileType, content []byte) gperr.Error {
switch fileType { switch fileType {
case FileTypeConfig: case FileTypeConfig:
return config.Validate(content) return config.Validate(content)
case FileTypeMiddleware: case FileTypeMiddleware:
errs := E.NewBuilder("middleware errors") errs := gperr.NewBuilder("middleware errors")
middleware.BuildMiddlewaresFromYAML("", content, errs) middleware.BuildMiddlewaresFromYAML("", content, errs)
return errs.Error() return errs.Error()
} }
@ -90,18 +90,17 @@ func validateFile(fileType FileType, content []byte) error {
func ValidateFile(w http.ResponseWriter, r *http.Request) { func ValidateFile(w http.ResponseWriter, r *http.Request) {
fileType := FileType(r.PathValue("type")) fileType := FileType(r.PathValue("type"))
if !fileType.IsValid() { if !fileType.IsValid() {
U.RespondError(w, U.ErrInvalidKey("type"), http.StatusBadRequest) gphttp.BadRequest(w, "invalid file type")
return return
} }
content, err := io.ReadAll(r.Body) content, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
r.Body.Close() r.Body.Close()
err = validateFile(fileType, content) if valErr := validateFile(fileType, content); valErr != nil {
if err != nil { gphttp.JSONError(w, valErr, http.StatusBadRequest)
U.RespondError(w, err, http.StatusBadRequest)
return return
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -110,23 +109,23 @@ func ValidateFile(w http.ResponseWriter, r *http.Request) {
func SetFileContent(w http.ResponseWriter, r *http.Request) { func SetFileContent(w http.ResponseWriter, r *http.Request) {
fileType, filename, err := getArgs(r) fileType, filename, err := getArgs(r)
if err != nil { if err != nil {
U.RespondError(w, err, http.StatusBadRequest) gphttp.BadRequest(w, err.Error())
return return
} }
content, err := io.ReadAll(r.Body) content, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
if valErr := validateFile(fileType, content); valErr != nil { if valErr := validateFile(fileType, content); valErr != nil {
U.RespondError(w, valErr, http.StatusBadRequest) gphttp.JSONError(w, valErr, http.StatusBadRequest)
return return
} }
err = os.WriteFile(fileType.GetPath(filename), content, 0o644) err = os.WriteFile(fileType.GetPath(filename), content, 0o644)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View file

@ -6,17 +6,18 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/route/routes/routequery" "github.com/yusing/go-proxy/internal/route/routes/routequery"
) )
func Health(w http.ResponseWriter, r *http.Request) { func Health(w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) { if httpheaders.IsWebsocket(r.Header) {
U.PeriodicWS(w, r, 1*time.Second, func(conn *websocket.Conn) error { gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
return wsjson.Write(r.Context(), conn, routequery.HealthMap()) return wsjson.Write(r.Context(), conn, routequery.HealthMap())
}) })
} else { } else {
U.RespondJSON(w, r, routequery.HealthMap()) gphttp.RespondJSON(w, r, routequery.HealthMap())
} }
} }

View file

@ -5,8 +5,8 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/net/gphttp"
) )
const ( const (
@ -37,13 +37,13 @@ type (
func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) { func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
what := r.FormValue("what") what := r.FormValue("what")
if what == "" { if what == "" {
http.Error(w, "missing what or which", http.StatusBadRequest) gphttp.BadRequest(w, "missing what or which")
return return
} }
data, err := io.ReadAll(r.Body) data, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
utils.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
r.Body.Close() r.Body.Close()
@ -53,21 +53,21 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
case HomepageOverrideItem: case HomepageOverrideItem:
var params HomepageOverrideItemParams var params HomepageOverrideItemParams
if err := json.Unmarshal(data, &params); err != nil { if err := json.Unmarshal(data, &params); err != nil {
utils.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
overrides.OverrideItem(params.Which, &params.Value) overrides.OverrideItem(params.Which, &params.Value)
case HomepageOverrideItemsBatch: case HomepageOverrideItemsBatch:
var params HomepageOverrideItemsBatchParams var params HomepageOverrideItemsBatchParams
if err := json.Unmarshal(data, &params); err != nil { if err := json.Unmarshal(data, &params); err != nil {
utils.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
overrides.OverrideItems(params.Value) overrides.OverrideItems(params.Value)
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
var params HomepageOverrideItemVisibleParams var params HomepageOverrideItemVisibleParams
if err := json.Unmarshal(data, &params); err != nil { if err := json.Unmarshal(data, &params); err != nil {
utils.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
if params.Value { if params.Value {
@ -78,7 +78,7 @@ func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
case HomepageOverrideCategoryOrder: case HomepageOverrideCategoryOrder:
var params HomepageOverrideCategoryOrderParams var params HomepageOverrideCategoryOrderParams
if err := json.Unmarshal(data, &params); err != nil { if err := json.Unmarshal(data, &params); err != nil {
utils.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
overrides.SetCategoryOrder(params.Which, params.Value) overrides.SetCategoryOrder(params.Which, params.Value)

View file

@ -3,9 +3,9 @@ package v1
import ( import (
"net/http" "net/http"
. "github.com/yusing/go-proxy/internal/api/v1/utils" "github.com/yusing/go-proxy/internal/net/gphttp"
) )
func Index(w http.ResponseWriter, r *http.Request) { func Index(w http.ResponseWriter, r *http.Request) {
WriteBody(w, []byte("API ready")) gphttp.WriteBody(w, []byte("API ready"))
} }

View file

@ -1,15 +1,16 @@
package v1 package v1
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/route/routes/routequery" "github.com/yusing/go-proxy/internal/route/routes/routequery"
route "github.com/yusing/go-proxy/internal/route/types" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
@ -41,26 +42,25 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
case ListRoute: case ListRoute:
if route := listRoute(which); route == nil { if route := listRoute(which); route == nil {
http.NotFound(w, r) http.NotFound(w, r)
return
} else { } else {
U.RespondJSON(w, r, route) gphttp.RespondJSON(w, r, route)
} }
case ListRoutes: case ListRoutes:
U.RespondJSON(w, r, routequery.RoutesByAlias(route.RouteType(r.FormValue("type")))) gphttp.RespondJSON(w, r, routequery.RoutesByAlias(route.RouteType(r.FormValue("type"))))
case ListFiles: case ListFiles:
listFiles(w, r) listFiles(w, r)
case ListMiddlewares: case ListMiddlewares:
U.RespondJSON(w, r, middleware.All()) gphttp.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces: case ListMiddlewareTraces:
U.RespondJSON(w, r, middleware.GetAllTrace()) gphttp.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains: case ListMatchDomains:
U.RespondJSON(w, r, cfg.Value().MatchDomains) gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
case ListHomepageConfig: case ListHomepageConfig:
U.RespondJSON(w, r, routequery.HomepageConfig(cfg.Value().Homepage.UseDefaultCategories, r.FormValue("category"), r.FormValue("provider"))) gphttp.RespondJSON(w, r, routequery.HomepageConfig(cfg.Value().Homepage.UseDefaultCategories, r.FormValue("category"), r.FormValue("provider")))
case ListRouteProviders: case ListRouteProviders:
U.RespondJSON(w, r, cfg.RouteProviderList()) gphttp.RespondJSON(w, r, cfg.RouteProviderList())
case ListHomepageCategories: case ListHomepageCategories:
U.RespondJSON(w, r, routequery.HomepageCategories()) gphttp.RespondJSON(w, r, routequery.HomepageCategories())
case ListIcons: case ListIcons:
limit, err := strconv.Atoi(r.FormValue("limit")) limit, err := strconv.Atoi(r.FormValue("limit"))
if err != nil { if err != nil {
@ -68,17 +68,17 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
} }
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit) icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
if err != nil { if err != nil {
U.RespondError(w, err) gphttp.ClientError(w, err)
return return
} }
if icons == nil { if icons == nil {
icons = []string{} icons = []string{}
} }
U.RespondJSON(w, r, icons) gphttp.RespondJSON(w, r, icons)
case ListTasks: case ListTasks:
U.RespondJSON(w, r, task.DebugTaskList()) gphttp.RespondJSON(w, r, task.DebugTaskList())
default: default:
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest) gphttp.BadRequest(w, fmt.Sprintf("invalid what: %s", what))
} }
} }
@ -99,7 +99,7 @@ func listRoute(which string) any {
func listFiles(w http.ResponseWriter, r *http.Request) { func listFiles(w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 0, true) files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
resp := map[FileType][]string{ resp := map[FileType][]string{
@ -116,12 +116,12 @@ func listFiles(w http.ResponseWriter, r *http.Request) {
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true) mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
for _, mid := range mids { for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/") mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid) resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
} }
U.RespondJSON(w, r, resp) gphttp.RespondJSON(w, r, resp)
} }

View file

@ -12,8 +12,9 @@ import (
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/certs" "github.com/yusing/go-proxy/agent/pkg/certs"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
@ -21,27 +22,27 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query() q := r.URL.Query()
name := q.Get("name") name := q.Get("name")
if name == "" { if name == "" {
U.RespondError(w, U.ErrMissingKey("name")) gphttp.ClientError(w, gphttp.ErrMissingKey("name"))
return return
} }
host := q.Get("host") host := q.Get("host")
if host == "" { if host == "" {
U.RespondError(w, U.ErrMissingKey("host")) gphttp.ClientError(w, gphttp.ErrMissingKey("host"))
return return
} }
portStr := q.Get("port") portStr := q.Get("port")
if portStr == "" { if portStr == "" {
U.RespondError(w, U.ErrMissingKey("port")) gphttp.ClientError(w, gphttp.ErrMissingKey("port"))
return return
} }
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 { if err != nil || port < 1 || port > 65535 {
U.RespondError(w, U.ErrInvalidKey("port")) gphttp.ClientError(w, gphttp.ErrInvalidKey("port"))
return return
} }
hostport := fmt.Sprintf("%s:%d", host, port) hostport := fmt.Sprintf("%s:%d", host, port)
if _, ok := config.GetInstance().GetAgent(hostport); ok { if _, ok := config.GetInstance().GetAgent(hostport); ok {
U.RespondError(w, U.ErrAlreadyExists("agent", hostport), http.StatusConflict) gphttp.ClientError(w, gphttp.ErrAlreadyExists("agent", hostport), http.StatusConflict)
return return
} }
t := q.Get("type") t := q.Get("type")
@ -49,13 +50,13 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
case "docker": case "docker":
break break
case "system": case "system":
U.RespondError(w, U.Errorf("system agent is not supported yet"), http.StatusNotImplemented) gphttp.ClientError(w, gperr.Errorf("system agent is not supported yet"), http.StatusNotImplemented)
return return
case "": case "":
U.RespondError(w, U.ErrMissingKey("type")) gphttp.ClientError(w, gphttp.ErrMissingKey("type"))
return return
default: default:
U.RespondError(w, U.ErrInvalidKey("type")) gphttp.ClientError(w, gphttp.ErrInvalidKey("type"))
return return
} }
@ -69,7 +70,7 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
ca, srv, client, err := agent.NewAgent() ca, srv, client, err := agent.NewAgent()
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
@ -83,11 +84,11 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
template, err := cfg.Generate() template, err := cfg.Generate()
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
U.RespondJSON(w, r, map[string]any{ gphttp.RespondJSON(w, r, map[string]any{
"compose": template, "compose": template,
"ca": ca, "ca": ca,
"client": client, "client": client,
@ -98,7 +99,7 @@ func AddAgent(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
clientPEMData, err := io.ReadAll(r.Body) clientPEMData, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
@ -109,24 +110,24 @@ func AddAgent(w http.ResponseWriter, r *http.Request) {
} }
if err := json.Unmarshal(clientPEMData, &data); err != nil { if err := json.Unmarshal(clientPEMData, &data); err != nil {
U.RespondError(w, err, http.StatusBadRequest) gphttp.ClientError(w, err, http.StatusBadRequest)
return return
} }
nRoutesAdded, err := config.GetInstance().AddAgent(data.Host, data.CA, data.Client) nRoutesAdded, err := config.GetInstance().AddAgent(data.Host, data.CA, data.Client)
if err != nil { if err != nil {
U.RespondError(w, err) gphttp.ClientError(w, err)
return return
} }
zip, err := certs.ZipCert(data.CA.Cert, data.Client.Cert, data.Client.Key) zip, err := certs.ZipCert(data.CA.Cert, data.Client.Cert, data.Client.Key)
if err != nil { if err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
if err := os.WriteFile(certs.AgentCertsFilename(data.Host), zip, 0600); err != nil { if err := os.WriteFile(certs.AgentCertsFilename(data.Host), zip, 0600); err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }

View file

@ -7,20 +7,20 @@ import (
"net/http" "net/http"
v1 "github.com/yusing/go-proxy/internal/api/v1" v1 "github.com/yusing/go-proxy/internal/api/v1"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
) )
func ReloadServer() E.Error { func ReloadServer() gperr.Error {
resp, err := U.Post(common.APIHTTPURL+"/v1/reload", "", nil) resp, err := gphttp.Post(common.APIHTTPURL+"/v1/reload", "", nil)
if err != nil { if err != nil {
return E.From(err) return gperr.Wrap(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
failure := E.Errorf("server reload status %v", resp.StatusCode) failure := gperr.Errorf("server reload status %v", resp.StatusCode)
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return failure.With(err) return failure.With(err)
@ -31,34 +31,34 @@ func ReloadServer() E.Error {
return nil return nil
} }
func List[T any](what string) (_ T, outErr E.Error) { func List[T any](what string) (_ T, outErr gperr.Error) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what)) resp, err := gphttp.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
if err != nil { if err != nil {
outErr = E.From(err) outErr = gperr.Wrap(err)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
outErr = E.Errorf("list %s: failed, status %v", what, resp.StatusCode) outErr = gperr.Errorf("list %s: failed, status %v", what, resp.StatusCode)
return return
} }
var res T var res T
err = json.NewDecoder(resp.Body).Decode(&res) err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil { if err != nil {
outErr = E.From(err) outErr = gperr.Wrap(err)
return return
} }
return res, nil return res, nil
} }
func ListRoutes() (map[string]map[string]any, E.Error) { func ListRoutes() (map[string]map[string]any, gperr.Error) {
return List[map[string]map[string]any](v1.ListRoutes) return List[map[string]map[string]any](v1.ListRoutes)
} }
func ListMiddlewareTraces() (middleware.Traces, E.Error) { func ListMiddlewareTraces() (middleware.Traces, gperr.Error) {
return List[middleware.Traces](v1.ListMiddlewareTraces) return List[middleware.Traces](v1.ListMiddlewareTraces)
} }
func DebugListTasks() (map[string]any, E.Error) { func DebugListTasks() (map[string]any, gperr.Error) {
return List[map[string]any](v1.ListTasks) return List[map[string]any](v1.ListTasks)
} }

View file

@ -3,14 +3,14 @@ package v1
import ( import (
"net/http" "net/http"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/gphttp"
) )
func Reload(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { func Reload(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
if err := cfg.Reload(); err != nil { if err := cfg.Reload(); err != nil {
U.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
U.WriteBody(w, []byte("OK")) gphttp.WriteBody(w, []byte("OK"))
} }

View file

@ -6,19 +6,20 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
if httpheaders.IsWebsocket(r.Header) { if httpheaders.IsWebsocket(r.Header) {
U.PeriodicWS(w, r, 1*time.Second, func(conn *websocket.Conn) error { gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
return wsjson.Write(r.Context(), conn, getStats(cfg)) return wsjson.Write(r.Context(), conn, getStats(cfg))
}) })
} else { } else {
U.RespondJSON(w, r, getStats(cfg)) gphttp.RespondJSON(w, r, getStats(cfg))
} }
} }

View file

@ -4,12 +4,12 @@ import (
"net/http" "net/http"
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent" agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/metrics/systeminfo" "github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/http/reverseproxy" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
) )
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
@ -23,7 +23,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
agent, ok := cfg.GetAgent(agentAddr) agent, ok := cfg.GetAgent(agentAddr)
if !ok { if !ok {
U.HandleErr(w, r, U.ErrInvalidKey("agent_addr"), http.StatusNotFound) gphttp.NotFound(w, "agent_addr")
return return
} }
@ -31,20 +31,20 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
if !isWS { if !isWS {
respData, status, err := agent.Forward(r, agentPkg.EndpointSystemInfo) respData, status, err := agent.Forward(r, agentPkg.EndpointSystemInfo)
if err != nil { if err != nil {
U.HandleErr(w, r, E.Wrap(err, "failed to forward request to agent")) gphttp.ServerError(w, r, gperr.Wrap(err, "failed to forward request to agent"))
return return
} }
if status != http.StatusOK { if status != http.StatusOK {
http.Error(w, string(respData), status) http.Error(w, string(respData), status)
return return
} }
U.WriteBody(w, respData) gphttp.WriteBody(w, respData)
} else { } else {
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport()) rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
header := r.Header.Clone() header := r.Header.Clone()
r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil) r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
if err != nil { if err != nil {
U.HandleErr(w, r, E.Wrap(err, "failed to create request")) gphttp.ServerError(w, r, gperr.Wrap(err, "failed to create request"))
return return
} }
r.Header = header r.Header = header

View file

@ -1,66 +0,0 @@
package utils
import (
"context"
"errors"
"net/http"
"syscall"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/net/http/httpheaders"
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
)
// HandleErr logs the error and returns an error code to the client.
// If code is specified, it will be used as the HTTP status code; otherwise,
// http.StatusInternalServerError is used.
//
// The error is only logged but not returned to the client.
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
switch {
case err == nil,
errors.Is(err, context.Canceled),
errors.Is(err, syscall.EPIPE),
errors.Is(err, syscall.ECONNRESET):
return
}
LogError(r).Msg(err.Error())
if httpheaders.IsWebsocket(r.Header) {
return
}
if len(code) == 0 {
code = []int{http.StatusInternalServerError}
}
http.Error(w, http.StatusText(code[0]), code[0])
}
// RespondError returns error details to the client.
// If code is specified, it will be used as the HTTP status code; otherwise,
// http.StatusBadRequest is used.
func RespondError(w http.ResponseWriter, err error, code ...int) {
if len(code) == 0 {
code = []int{http.StatusBadRequest}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.Error(w, ansi.StripANSI(err.Error()), code[0])
}
func Errorf(format string, args ...any) error {
return E.Errorf(format, args...)
}
func ErrMissingKey(k string) error {
return E.New(k + " is required")
}
func ErrInvalidKey(k string) error {
return E.New(k + " is invalid")
}
func ErrAlreadyExists(k, v string) error {
return E.Errorf("%s %q already exists", k, v)
}
func ErrNotFound(k, v string) error {
return E.Errorf("%s %q not found", k, v)
}

View file

@ -3,10 +3,10 @@ package v1
import ( import (
"net/http" "net/http"
. "github.com/yusing/go-proxy/internal/api/v1/utils" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/pkg" "github.com/yusing/go-proxy/pkg"
) )
func GetVersion(w http.ResponseWriter, r *http.Request) { func GetVersion(w http.ResponseWriter, r *http.Request) {
WriteBody(w, []byte(pkg.GetVersion())) gphttp.WriteBody(w, []byte(pkg.GetVersion()))
} }

View file

@ -10,7 +10,7 @@ import (
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
@ -30,17 +30,17 @@ type (
) )
var ( var (
ErrMissingDomain = E.New("missing field 'domains'") ErrMissingDomain = gperr.New("missing field 'domains'")
ErrMissingEmail = E.New("missing field 'email'") ErrMissingEmail = gperr.New("missing field 'email'")
ErrMissingProvider = E.New("missing field 'provider'") ErrMissingProvider = gperr.New("missing field 'provider'")
ErrInvalidDomain = E.New("invalid domain") ErrInvalidDomain = gperr.New("invalid domain")
ErrUnknownProvider = E.New("unknown provider") ErrUnknownProvider = gperr.New("unknown provider")
) )
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`) var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
// Validate implements the utils.CustomValidator interface. // Validate implements the utils.CustomValidator interface.
func (cfg *AutocertConfig) Validate() E.Error { func (cfg *AutocertConfig) Validate() gperr.Error {
if cfg == nil { if cfg == nil {
return nil return nil
} }
@ -50,7 +50,7 @@ func (cfg *AutocertConfig) Validate() E.Error {
return nil return nil
} }
b := E.NewBuilder("autocert errors") b := gperr.NewBuilder("autocert errors")
if cfg.Provider != ProviderLocal { if cfg.Provider != ProviderLocal {
if len(cfg.Domains) == 0 { if len(cfg.Domains) == 0 {
b.Add(ErrMissingDomain) b.Add(ErrMissingDomain)
@ -79,7 +79,7 @@ func (cfg *AutocertConfig) Validate() E.Error {
return b.Error() return b.Error()
} }
func (cfg *AutocertConfig) GetProvider() (*Provider, E.Error) { func (cfg *AutocertConfig) GetProvider() (*Provider, gperr.Error) {
if cfg == nil { if cfg == nil {
cfg = new(AutocertConfig) cfg = new(AutocertConfig)
} }
@ -107,10 +107,10 @@ func (cfg *AutocertConfig) GetProvider() (*Provider, E.Error) {
logging.Info().Msg("generate new ACME private key") logging.Info().Msg("generate new ACME private key")
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return nil, E.New("generate ACME private key").With(err) return nil, gperr.New("generate ACME private key").With(err)
} }
if err = cfg.saveACMEKey(privKey); err != nil { if err = cfg.saveACMEKey(privKey); err != nil {
return nil, E.New("save ACME private key").With(err) return nil, gperr.New("save ACME private key").With(err)
} }
} }
} }

View file

@ -4,6 +4,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt"
"os" "os"
"path" "path"
"reflect" "reflect"
@ -14,7 +15,7 @@ import (
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
@ -32,7 +33,7 @@ type (
tlsCert *tls.Certificate tlsCert *tls.Certificate
certExpiries CertExpiries certExpiries CertExpiries
} }
ProviderGenerator func(ProviderOpt) (challenge.Provider, E.Error) ProviderGenerator func(ProviderOpt) (challenge.Provider, gperr.Error)
CertExpiries map[string]time.Time CertExpiries map[string]time.Time
) )
@ -62,7 +63,7 @@ func (p *Provider) GetExpiries() CertExpiries {
return p.certExpiries return p.certExpiries
} }
func (p *Provider) ObtainCert() E.Error { func (p *Provider) ObtainCert() error {
if p.cfg.Provider == ProviderLocal { if p.cfg.Provider == ProviderLocal {
return nil return nil
} }
@ -75,7 +76,7 @@ func (p *Provider) ObtainCert() E.Error {
if p.user.Registration == nil { if p.user.Registration == nil {
if err := p.registerACME(); err != nil { if err := p.registerACME(); err != nil {
return E.From(err) return err
} }
} }
@ -100,22 +101,22 @@ func (p *Provider) ObtainCert() E.Error {
Bundle: true, Bundle: true,
}) })
if err != nil { if err != nil {
return E.From(err) return err
} }
} }
if err = p.saveCert(cert); err != nil { if err = p.saveCert(cert); err != nil {
return E.From(err) return err
} }
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey) tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.PrivateKey)
if err != nil { if err != nil {
return E.From(err) return err
} }
expiries, err := getCertExpiries(&tlsCert) expiries, err := getCertExpiries(&tlsCert)
if err != nil { if err != nil {
return E.From(err) return err
} }
p.tlsCert = &tlsCert p.tlsCert = &tlsCert
p.certExpiries = expiries p.certExpiries = expiries
@ -123,14 +124,14 @@ func (p *Provider) ObtainCert() E.Error {
return nil return nil
} }
func (p *Provider) LoadCert() E.Error { func (p *Provider) LoadCert() error {
cert, err := tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath) cert, err := tls.LoadX509KeyPair(p.cfg.CertPath, p.cfg.KeyPath)
if err != nil { if err != nil {
return E.Errorf("load SSL certificate: %w", err) return fmt.Errorf("load SSL certificate: %w", err)
} }
expiries, err := getCertExpiries(&cert) expiries, err := getCertExpiries(&cert)
if err != nil { if err != nil {
return E.Errorf("parse SSL certificate: %w", err) return fmt.Errorf("parse SSL certificate: %w", err)
} }
p.tlsCert = &cert p.tlsCert = &cert
p.certExpiries = expiries p.certExpiries = expiries
@ -171,7 +172,7 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
continue continue
} }
if err := p.renewIfNeeded(); err != nil { if err := p.renewIfNeeded(); err != nil {
E.LogWarn("cert renew failed", err) gperr.LogWarn("cert renew failed", err)
lastErrOn = time.Now() lastErrOn = time.Now()
continue continue
} }
@ -184,10 +185,10 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
}() }()
} }
func (p *Provider) initClient() E.Error { func (p *Provider) initClient() error {
legoClient, err := lego.NewClient(p.legoCfg) legoClient, err := lego.NewClient(p.legoCfg)
if err != nil { if err != nil {
return E.From(err) return err
} }
generator := providersGenMap[p.cfg.Provider] generator := providersGenMap[p.cfg.Provider]
@ -198,7 +199,7 @@ func (p *Provider) initClient() E.Error {
err = legoClient.Challenge.SetDNS01Provider(legoProvider) err = legoClient.Challenge.SetDNS01Provider(legoProvider)
if err != nil { if err != nil {
return E.From(err) return err
} }
p.client = legoClient p.client = legoClient
@ -273,7 +274,7 @@ func (p *Provider) certState() CertState {
return CertStateValid return CertStateValid
} }
func (p *Provider) renewIfNeeded() E.Error { func (p *Provider) renewIfNeeded() error {
if p.cfg.Provider == ProviderLocal { if p.cfg.Provider == ProviderLocal {
return nil return nil
} }
@ -312,13 +313,13 @@ func providerGenerator[CT any, PT challenge.Provider](
defaultCfg func() *CT, defaultCfg func() *CT,
newProvider func(*CT) (PT, error), newProvider func(*CT) (PT, error),
) ProviderGenerator { ) ProviderGenerator {
return func(opt ProviderOpt) (challenge.Provider, E.Error) { return func(opt ProviderOpt) (challenge.Provider, gperr.Error) {
cfg := defaultCfg() cfg := defaultCfg()
err := U.Deserialize(opt, &cfg) err := U.Deserialize(opt, &cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
p, pErr := newProvider(cfg) p, pErr := newProvider(cfg)
return p, E.From(pErr) return p, gperr.Wrap(pErr)
} }
} }

View file

@ -1,16 +1,16 @@
package autocert package autocert
import ( import (
"errors"
"os" "os"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
func (p *Provider) Setup() (err E.Error) { func (p *Provider) Setup() (err error) {
if err = p.LoadCert(); err != nil { if err = p.LoadCert(); err != nil {
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist if !errors.Is(err, os.ErrNotExist) { // ignore if cert doesn't exist
return err return err
} }
logging.Debug().Msg("obtaining cert due to error loading cert") logging.Debug().Msg("obtaining cert due to error loading cert")

View file

@ -4,12 +4,6 @@ import (
"time" "time"
) )
const (
ConnectionTimeout = 5 * time.Second
DialTimeout = 3 * time.Second
KeepAlive = 60 * time.Second
)
// file, folder structure // file, folder structure
const ( const (
@ -50,5 +44,3 @@ const (
StopTimeoutDefault = "30s" StopTimeoutDefault = "30s"
StopMethodDefault = "stop" StopMethodDefault = "stop"
) )
const HeaderCheckRedirect = "X-Goproxy-Check-Redirect"

View file

@ -2,7 +2,7 @@ package config
import ( import (
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/route/provider" "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/utils/functional" "github.com/yusing/go-proxy/internal/utils/functional"
) )
@ -29,12 +29,12 @@ func (cfg *Config) GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, b
return GetAgent(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost)) return GetAgent(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
} }
func (cfg *Config) AddAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, E.Error) { func (cfg *Config) AddAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) {
var agentCfg agent.AgentConfig var agentCfg agent.AgentConfig
agentCfg.Addr = host agentCfg.Addr = host
err := agentCfg.StartWithCerts(cfg.Task(), ca.Cert, client.Cert, client.Key) err := agentCfg.StartWithCerts(cfg.Task(), ca.Cert, client.Cert, client.Key)
if err != nil { if err != nil {
return 0, err return 0, gperr.Wrap(err, "failed to start agent")
} }
provider := provider.NewAgentProvider(&agentCfg) provider := provider.NewAgentProvider(&agentCfg)

View file

@ -14,9 +14,9 @@ import (
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types" config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/entrypoint" "github.com/yusing/go-proxy/internal/entrypoint"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/server" "github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/notif" "github.com/yusing/go-proxy/internal/notif"
proxy "github.com/yusing/go-proxy/internal/route/provider" proxy "github.com/yusing/go-proxy/internal/route/provider"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
@ -64,7 +64,7 @@ func newConfig() *Config {
} }
} }
func Load() (*Config, E.Error) { func Load() (*Config, gperr.Error) {
if config.HasInstance() { if config.HasInstance() {
panic(errors.New("config already loaded")) panic(errors.New("config already loaded"))
} }
@ -84,8 +84,8 @@ func WatchChanges() {
t, t,
configEventFlushInterval, configEventFlushInterval,
OnConfigChange, OnConfigChange,
func(err E.Error) { func(err gperr.Error) {
E.LogError("config reload error", err) gperr.LogError("config reload error", err)
}, },
) )
eventQueue.Start(cfgWatcher.Events(t.Context())) eventQueue.Start(cfgWatcher.Events(t.Context()))
@ -109,7 +109,7 @@ func OnConfigChange(ev []events.Event) {
} }
} }
func Reload() E.Error { func Reload() gperr.Error {
// avoid race between config change and API reload request // avoid race between config change and API reload request
reloadMu.Lock() reloadMu.Lock()
defer reloadMu.Unlock() defer reloadMu.Unlock()
@ -118,7 +118,7 @@ func Reload() E.Error {
err := newCfg.load() err := newCfg.load()
if err != nil { if err != nil {
newCfg.task.Finish(err) newCfg.task.Finish(err)
return E.New("using last config").With(err) return gperr.New("using last config").With(err)
} }
// cancel all current subtasks -> wait // cancel all current subtasks -> wait
@ -133,10 +133,13 @@ func (cfg *Config) Value() *config.Config {
return cfg.value return cfg.value
} }
func (cfg *Config) Reload() E.Error { func (cfg *Config) Reload() gperr.Error {
return Reload() return Reload()
} }
// AutoCertProvider returns the autocert provider.
//
// If the autocert provider is not configured, it returns nil.
func (cfg *Config) AutoCertProvider() *autocert.Provider { func (cfg *Config) AutoCertProvider() *autocert.Provider {
return cfg.autocertProvider return cfg.autocertProvider
} }
@ -163,7 +166,7 @@ func (cfg *Config) StartAutoCert() {
} }
if err := autocert.Setup(); err != nil { if err := autocert.Setup(); err != nil {
E.LogFatal("autocert setup error", err) gperr.LogFatal("autocert setup error", err)
} else { } else {
autocert.ScheduleRenewal(cfg.task) autocert.ScheduleRenewal(cfg.task)
} }
@ -175,8 +178,8 @@ func (cfg *Config) StartProxyProviders() {
return p.Start(cfg.task) return p.Start(cfg.task)
}) })
if err := E.Join(errs...); err != nil { if err := gperr.Join(errs...); err != nil {
E.LogError("route provider errors", err) gperr.LogError("route provider errors", err)
} }
} }
@ -210,21 +213,21 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
} }
} }
func (cfg *Config) load() E.Error { func (cfg *Config) load() gperr.Error {
const errMsg = "config load error" const errMsg = "config load error"
data, err := os.ReadFile(common.ConfigPath) data, err := os.ReadFile(common.ConfigPath)
if err != nil { if err != nil {
E.LogFatal(errMsg, err) gperr.LogFatal(errMsg, err)
} }
model := config.DefaultConfig() model := config.DefaultConfig()
if err := utils.DeserializeYAML(data, model); err != nil { if err := utils.DeserializeYAML(data, model); err != nil {
E.LogFatal(errMsg, err) gperr.LogFatal(errMsg, err)
} }
// errors are non fatal below // errors are non fatal below
errs := E.NewBuilder(errMsg) errs := gperr.NewBuilder(errMsg)
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares)) errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog)) errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
cfg.initNotification(model.Providers.Notification) cfg.initNotification(model.Providers.Notification)
@ -252,7 +255,7 @@ func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
} }
} }
func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err E.Error) { func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err gperr.Error) {
if cfg.autocertProvider != nil { if cfg.autocertProvider != nil {
return return
} }
@ -261,9 +264,9 @@ func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err E.Err
return return
} }
func (cfg *Config) errIfExists(p *proxy.Provider) E.Error { func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error {
if _, ok := cfg.providers.Load(p.String()); ok { if _, ok := cfg.providers.Load(p.String()); ok {
return E.Errorf("provider %s already exists", p.String()) return gperr.Errorf("provider %s already exists", p.String())
} }
return nil return nil
} }
@ -272,9 +275,9 @@ func (cfg *Config) storeProvider(p *proxy.Provider) {
cfg.providers.Store(p.String(), p) cfg.providers.Store(p.String(), p)
} }
func (cfg *Config) loadRouteProviders(providers *config.Providers) E.Error { func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
errs := E.NewBuilder("route provider errors") errs := gperr.NewBuilder("route provider errors")
results := E.NewBuilder("loaded route providers") results := gperr.NewBuilder("loaded route providers")
removeAllAgents() removeAllAgents()
@ -297,7 +300,7 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) E.Error {
err = cfg.errIfExists(p) err = cfg.errIfExists(p)
} }
if err != nil { if err != nil {
errs.Add(E.PrependSubject(filename, err)) errs.Add(gperr.PrependSubject(filename, err))
continue continue
} }
cfg.storeProvider(p) cfg.storeProvider(p)

View file

@ -8,11 +8,10 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/yusing/go-proxy/agent/pkg/agent" "github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/autocert" "github.com/yusing/go-proxy/internal/autocert"
"github.com/yusing/go-proxy/internal/net/http/accesslog" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
"github.com/yusing/go-proxy/internal/notif" "github.com/yusing/go-proxy/internal/notif"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
E "github.com/yusing/go-proxy/internal/error"
) )
type ( type (
@ -37,12 +36,12 @@ type (
ConfigInstance interface { ConfigInstance interface {
Value() *Config Value() *Config
Reload() E.Error Reload() gperr.Error
Statistics() map[string]any Statistics() map[string]any
RouteProviderList() []string RouteProviderList() []string
Context() context.Context Context() context.Context
GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool) GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool)
AddAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, E.Error) AddAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
ListAgents() []*agent.AgentConfig ListAgents() []*agent.AgentConfig
} }
) )
@ -79,7 +78,7 @@ func HasInstance() bool {
return instance != nil return instance != nil
} }
func Validate(data []byte) E.Error { func Validate(data []byte) gperr.Error {
var model Config var model Config
return utils.DeserializeYAML(data, &model) return utils.DeserializeYAML(data, &model)
} }

View file

@ -6,7 +6,7 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
) )
type templateData struct { type templateData struct {
@ -23,11 +23,11 @@ func (w *Watcher) makeLoadingPageBody() []byte {
msg := w.ContainerName + " is starting..." msg := w.ContainerName + " is starting..."
data := new(templateData) data := new(templateData)
data.CheckRedirectHeader = common.HeaderCheckRedirect data.CheckRedirectHeader = httpheaders.HeaderGoDoxyCheckRedirect
data.Title = w.ContainerName data.Title = w.ContainerName
data.Message = strings.ReplaceAll(msg, " ", "&ensp;") data.Message = strings.ReplaceAll(msg, " ", "&ensp;")
buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(common.HeaderCheckRedirect))) buf := bytes.NewBuffer(make([]byte, len(loadingPage)+len(data.Title)+len(data.Message)+len(httpheaders.HeaderGoDoxyCheckRedirect)))
err := loadingPageTmpl.Execute(buf, data) err := loadingPageTmpl.Execute(buf, data)
if err != nil { // should never happen in production if err != nil { // should never happen in production
panic(err) panic(err)

View file

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
) )
type ( type (
@ -40,7 +40,7 @@ var validSignals = map[string]struct{}{
"INT": {}, "TERM": {}, "HUP": {}, "QUIT": {}, "INT": {}, "TERM": {}, "HUP": {}, "QUIT": {},
} }
func ValidateConfig(cont *docker.Container) (*Config, E.Error) { func ValidateConfig(cont *docker.Container) (*Config, gperr.Error) {
if cont == nil { if cont == nil {
return nil, nil return nil, nil
} }
@ -54,14 +54,14 @@ func ValidateConfig(cont *docker.Container) (*Config, E.Error) {
}, nil }, nil
} }
errs := E.NewBuilder("invalid idlewatcher config") errs := gperr.NewBuilder("invalid idlewatcher config")
idleTimeout := E.Collect(errs, validateDurationPostitive, cont.IdleTimeout) idleTimeout := gperr.Collect(errs, validateDurationPostitive, cont.IdleTimeout)
wakeTimeout := E.Collect(errs, validateDurationPostitive, cont.WakeTimeout) wakeTimeout := gperr.Collect(errs, validateDurationPostitive, cont.WakeTimeout)
stopTimeout := E.Collect(errs, validateDurationPostitive, cont.StopTimeout) stopTimeout := gperr.Collect(errs, validateDurationPostitive, cont.StopTimeout)
stopMethod := E.Collect(errs, validateStopMethod, cont.StopMethod) stopMethod := gperr.Collect(errs, validateStopMethod, cont.StopMethod)
signal := E.Collect(errs, validateSignal, cont.StopSignal) signal := gperr.Collect(errs, validateSignal, cont.StopSignal)
startEndpoint := E.Collect(errs, validateStartEndpoint, cont.StartEndpoint) startEndpoint := gperr.Collect(errs, validateStartEndpoint, cont.StartEndpoint)
if errs.HasError() { if errs.HasError() {
return nil, errs.Error() return nil, errs.Error()

View file

@ -5,9 +5,9 @@ import (
"time" "time"
"github.com/yusing/go-proxy/internal/docker/idlewatcher/types" "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/metrics" "github.com/yusing/go-proxy/internal/metrics"
"github.com/yusing/go-proxy/internal/net/http/reverseproxy" "github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
net "github.com/yusing/go-proxy/internal/net/types" net "github.com/yusing/go-proxy/internal/net/types"
route "github.com/yusing/go-proxy/internal/route/types" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
@ -37,7 +37,7 @@ const (
// TODO: support stream // TODO: support stream
func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy, stream net.Stream) (Waker, E.Error) { func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy, stream net.Stream) (Waker, gperr.Error) {
hcCfg := route.HealthCheckConfig() hcCfg := route.HealthCheckConfig()
hcCfg.Timeout = idleWakerCheckTimeout hcCfg.Timeout = idleWakerCheckTimeout
@ -48,7 +48,7 @@ func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReversePro
task := parent.Subtask("idlewatcher." + route.TargetName()) task := parent.Subtask("idlewatcher." + route.TargetName())
watcher, err := registerWatcher(task, route, waker) watcher, err := registerWatcher(task, route, waker)
if err != nil { if err != nil {
return nil, E.Errorf("register watcher: %w", err) return nil, gperr.Errorf("register watcher: %w", err)
} }
switch { switch {
@ -66,16 +66,16 @@ func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReversePro
} }
// lifetime should follow route provider. // lifetime should follow route provider.
func NewHTTPWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy) (Waker, E.Error) { func NewHTTPWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy) (Waker, gperr.Error) {
return newWaker(parent, route, rp, nil) return newWaker(parent, route, rp, nil)
} }
func NewStreamWaker(parent task.Parent, route route.Route, stream net.Stream) (Waker, E.Error) { func NewStreamWaker(parent task.Parent, route route.Route, stream net.Stream) (Waker, gperr.Error) {
return newWaker(parent, route, nil, stream) return newWaker(parent, route, nil, stream)
} }
// Start implements health.HealthMonitor. // Start implements health.HealthMonitor.
func (w *Watcher) Start(parent task.Parent) E.Error { func (w *Watcher) Start(parent task.Parent) gperr.Error {
w.task.OnCancel("route_cleanup", func() { w.task.OnCancel("route_cleanup", func() {
parent.Finish(w.task.FinishCause()) parent.Finish(w.task.FinishCause())
if w.metric != nil { if w.metric != nil {

View file

@ -7,8 +7,8 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/yusing/go-proxy/internal/common" gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
gphttp "github.com/yusing/go-proxy/internal/net/http" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health"
) )
@ -63,7 +63,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
accept := gphttp.GetAccept(r.Header) accept := gphttp.GetAccept(r.Header)
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty()) acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
isCheckRedirect := r.Header.Get(common.HeaderCheckRedirect) != "" isCheckRedirect := r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != ""
if !isCheckRedirect && acceptHTML { if !isCheckRedirect && acceptHTML {
// Send a loading response to the client // Send a loading response to the client
body := w.makeLoadingPageBody() body := w.makeLoadingPageBody()

View file

@ -10,7 +10,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
D "github.com/yusing/go-proxy/internal/docker" D "github.com/yusing/go-proxy/internal/docker"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types" idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
route "github.com/yusing/go-proxy/internal/route/types" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
@ -180,7 +180,7 @@ func (w *Watcher) wakeIfStopped() error {
case "running": case "running":
return nil return nil
default: default:
return E.Errorf("unexpected container status: %s", status) return gperr.Errorf("unexpected container status: %s", status)
} }
} }
@ -213,7 +213,7 @@ func (w *Watcher) expires() time.Time {
return w.lastReset.Add(w.IdleTimeout) return w.lastReset.Add(w.IdleTimeout)
} }
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan E.Error) { func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan gperr.Error) {
eventCh, errCh = dockerWatcher.EventsWithOptions(w.Task().Context(), watcher.DockerListOptions{ eventCh, errCh = dockerWatcher.EventsWithOptions(w.Task().Context(), watcher.DockerListOptions{
Filters: watcher.NewDockerFilter( Filters: watcher.NewDockerFilter(
watcher.DockerFilterContainer, watcher.DockerFilterContainer,
@ -251,7 +251,7 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
return w.task.FinishCause() return w.task.FinishCause()
case err := <-dockerEventErrCh: case err := <-dockerEventErrCh:
if !err.Is(context.Canceled) { if !err.Is(context.Canceled) {
E.LogError("idlewatcher error", err, &w.Logger) gperr.LogError("idlewatcher error", err, &w.Logger)
} }
return err return err
case e := <-dockerEventCh: case e := <-dockerEventCh:

View file

@ -1,17 +1,17 @@
package docker package docker
import ( import (
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
type LabelMap = map[string]any type LabelMap = map[string]any
var ErrInvalidLabel = E.New("invalid label") var ErrInvalidLabel = gperr.New("invalid label")
func ParseLabels(labels map[string]string) (LabelMap, E.Error) { func ParseLabels(labels map[string]string) (LabelMap, gperr.Error) {
nestedMap := make(LabelMap) nestedMap := make(LabelMap)
errs := E.NewBuilder("labels error") errs := gperr.NewBuilder("labels error")
for lbl, value := range labels { for lbl, value := range labels {
parts := strutils.SplitRune(lbl, '.') parts := strutils.SplitRune(lbl, '.')
@ -37,7 +37,7 @@ func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
// Move deeper into the nested map // Move deeper into the nested map
m, ok := currentMap[k].(LabelMap) m, ok := currentMap[k].(LabelMap)
if !ok && currentMap[k] != "" { if !ok && currentMap[k] != "" {
errs.Add(E.Errorf("expect mapping, got %T", currentMap[k]).Subject(lbl)) errs.Add(gperr.Errorf("expect mapping, got %T", currentMap[k]).Subject(lbl))
continue continue
} else if !ok { } else if !ok {
m = make(LabelMap) m = make(LabelMap)

View file

@ -7,10 +7,10 @@ import (
"strings" "strings"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/http/accesslog" "github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/net/http/middleware/errorpage" "github.com/yusing/go-proxy/internal/net/gphttp/middleware/errorpage"
"github.com/yusing/go-proxy/internal/route/routes" "github.com/yusing/go-proxy/internal/route/routes"
route "github.com/yusing/go-proxy/internal/route/types" route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"

106
internal/gperr/README.md Normal file
View file

@ -0,0 +1,106 @@
# gperr
gperr is an error interface that supports nested structure and subject highlighting.
## Usage
### gperr.Error
The error interface.
### gperr.New
Like `errors.New`, but returns a `gperr.Error`.
### gperr.Wrap
Like `fmt.Errorf("%s: %w", message, err)`, but returns a `gperr.Error`.
### gperr.Error.Subject
Returns a new error with the subject prepended to the error message. The main subject is highlighted.
```go
err := gperr.New("error message")
err = err.Subject("bar")
err = err.Subject("foo")
```
Output:
<code>foo > <span style="color: red;">bar</span>: error message</code>
### gperr.Error.Subjectf
Like `gperr.Error.Subject`, but formats the subject with `fmt.Sprintf`.
### gperr.PrependSubject
Prepends the subject to the error message like `gperr.Error.Subject`.
```go
err := gperr.New("error message")
err = gperr.PrependSubject(err, "foo")
err = gperr.PrependSubject(err, "bar")
```
Output:
<code>bar > <span style="color: red;">foo</span>: error message</code>
### gperr.Error.With
Adds a new error to the error chain.
```go
err := gperr.New("error message")
err = err.With(gperr.New("inner error"))
err = err.With(gperr.New("inner error 2").With(gperr.New("inner inner error")))
```
Output:
```
error message:
• inner error
• inner error 2
• inner inner error
```
### gperr.Error.Withf
Like `gperr.Error.With`, but formats the error with `fmt.Errorf`.
### gperr.Error.Is
Returns true if the error is equal to the given error.
### gperr.Builder
A builder for `gperr.Error`.
```go
builder := gperr.NewBuilder("foo")
builder.Add(gperr.New("error message"))
builder.Addf("error message: %s", "foo")
builder.AddRange(gperr.New("error message 1"), gperr.New("error message 2"))
```
Output:
```
foo:
• error message
• error message: foo
• error message 1
• error message 2
```
### gperr.Builder.Build
Builds a `gperr.Error` from the builder.
## When to return gperr.Error
- When you want to return multiple errors
- When the error has a subject

View file

@ -1,4 +1,4 @@
package err package gperr
import ( import (
"encoding/json" "encoding/json"

View file

@ -1,4 +1,4 @@
package err package gperr
import ( import (
"fmt" "fmt"
@ -36,7 +36,7 @@ func (b *Builder) error() Error {
func (b *Builder) Error() Error { func (b *Builder) Error() Error {
if len(b.errs) == 1 { if len(b.errs) == 1 {
return From(b.errs[0]) return wrap(b.errs[0])
} }
return b.error() return b.error()
} }
@ -60,7 +60,7 @@ func (b *Builder) Add(err error) *Builder {
b.Lock() b.Lock()
defer b.Unlock() defer b.Unlock()
switch err := From(err).(type) { switch err := wrap(err).(type) {
case *baseError: case *baseError:
b.errs = append(b.errs, err.Err) b.errs = append(b.errs, err.Err)
case *nestedError: case *nestedError:

View file

@ -1,4 +1,4 @@
package err_test package gperr_test
import ( import (
"context" "context"
@ -6,7 +6,7 @@ import (
"io" "io"
"testing" "testing"
. "github.com/yusing/go-proxy/internal/error" . "github.com/yusing/go-proxy/internal/gperr"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -1,4 +1,4 @@
package err package gperr
type Error interface { type Error interface {
error error

View file

@ -1,4 +1,4 @@
package err package gperr
import ( import (
"errors" "errors"
@ -44,7 +44,7 @@ func TestBaseWithExtra(t *testing.T) {
func TestBaseUnwrap(t *testing.T) { func TestBaseUnwrap(t *testing.T) {
err := errors.New("err") err := errors.New("err")
wrapped := From(err) wrapped := Wrap(err)
ExpectError(t, err, errors.Unwrap(wrapped)) ExpectError(t, err, errors.Unwrap(wrapped))
} }
@ -52,7 +52,7 @@ func TestBaseUnwrap(t *testing.T) {
func TestNestedUnwrap(t *testing.T) { func TestNestedUnwrap(t *testing.T) {
err := errors.New("err") err := errors.New("err")
err2 := New("err2") err2 := New("err2")
wrapped := From(err).Subject("foo").With(err2.Subject("bar")) wrapped := Wrap(err).Subject("foo").With(err2.Subject("bar"))
unwrapper, ok := wrapped.(interface{ Unwrap() []error }) unwrapper, ok := wrapped.(interface{ Unwrap() []error })
ExpectTrue(t, ok) ExpectTrue(t, ok)
@ -64,7 +64,7 @@ func TestNestedUnwrap(t *testing.T) {
func TestErrorIs(t *testing.T) { func TestErrorIs(t *testing.T) {
from := errors.New("error") from := errors.New("error")
err := From(from) err := Wrap(from)
ExpectError(t, from, err) ExpectError(t, from, err)
ExpectTrue(t, err.Is(from)) ExpectTrue(t, err.Is(from))

View file

@ -1,4 +1,4 @@
package err package gperr
import ( import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -6,42 +6,35 @@ import (
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
) )
func getLogger(logger ...*zerolog.Logger) *zerolog.Logger { func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger) {
var l *zerolog.Logger
if len(logger) > 0 { if len(logger) > 0 {
return logger[0] l = logger[0]
} else {
l = logging.GetLogger()
} }
return logging.GetLogger() l.WithLevel(level).Msg(msg + ": " + err.Error())
} }
//go:inline
func LogFatal(msg string, err error, logger ...*zerolog.Logger) { func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
if common.IsDebug { if common.IsDebug {
LogPanic(msg, err, logger...) LogPanic(msg, err, logger...)
} }
getLogger(logger...).Fatal().Msg(err.Error()) log(msg, err, zerolog.FatalLevel, logger...)
} }
//go:inline
func LogError(msg string, err error, logger ...*zerolog.Logger) { func LogError(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Error().Msg(err.Error()) log(msg, err, zerolog.ErrorLevel, logger...)
} }
//go:inline
func LogWarn(msg string, err error, logger ...*zerolog.Logger) { func LogWarn(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Warn().Msg(err.Error()) log(msg, err, zerolog.WarnLevel, logger...)
} }
//go:inline
func LogPanic(msg string, err error, logger ...*zerolog.Logger) { func LogPanic(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Panic().Msg(err.Error()) log(msg, err, zerolog.PanicLevel, logger...)
} }
//go:inline
func LogInfo(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Info().Msg(err.Error())
}
//go:inline
func LogDebug(msg string, err error, logger ...*zerolog.Logger) { func LogDebug(msg string, err error, logger ...*zerolog.Logger) {
getLogger(logger...).Debug().Msg(err.Error()) log(msg, err, zerolog.DebugLevel, logger...)
} }

View file

@ -1,4 +1,4 @@
package err package gperr
import ( import (
"errors" "errors"
@ -99,7 +99,7 @@ func makeLines(errs []error, level int) []string {
} }
lines := make([]string, 0, len(errs)) lines := make([]string, 0, len(errs))
for _, err := range errs { for _, err := range errs {
switch err := From(err).(type) { switch err := wrap(err).(type) {
case *nestedError: case *nestedError:
if err.Err != nil { if err.Err != nil {
lines = append(lines, makeLine(err.Err.Error(), level)) lines = append(lines, makeLine(err.Err.Error(), level))

View file

@ -1,4 +1,4 @@
package err package gperr
import ( import (
"encoding/json" "encoding/json"

View file

@ -1,6 +1,8 @@
package err package gperr
import ( import (
"encoding/json"
"errors"
"fmt" "fmt"
) )
@ -19,27 +21,50 @@ func Errorf(format string, args ...any) Error {
return &baseError{fmt.Errorf(format, args...)} return &baseError{fmt.Errorf(format, args...)}
} }
// Wrap wraps message in front of the error message.
func Wrap(err error, message ...string) Error { func Wrap(err error, message ...string) Error {
if len(message) == 0 || message[0] == "" { if err == nil {
return From(err) return nil
} }
return Errorf("%s: %w", message[0], err) if len(message) == 0 || message[0] == "" {
return wrap(err)
}
//nolint:errorlint
switch err := err.(type) {
case *baseError:
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
return err
case *nestedError:
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
return err
}
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
} }
func From(err error) Error { func wrap(err error) Error {
if err == nil { if err == nil {
return nil return nil
} }
//nolint:errorlint //nolint:errorlint
switch err := err.(type) { switch err := err.(type) {
case *baseError: case Error:
return err
case *nestedError:
return err return err
} }
return &baseError{err} return &baseError{err}
} }
func IsJSONMarshallable(err error) bool {
switch err := err.(type) {
case *nestedError, *withSubject:
return true
case *baseError:
return IsJSONMarshallable(err.Err)
default:
var v json.Marshaler
return errors.As(err, &v)
}
}
func Join(errors ...error) Error { func Join(errors ...error) Error {
n := 0 n := 0
for _, err := range errors { for _, err := range errors {

View file

@ -0,0 +1,58 @@
package gperr
import (
"errors"
"testing"
)
type testErr struct{}
func (e *testErr) Error() string {
return "test error"
}
func (e *testErr) MarshalJSON() ([]byte, error) {
return nil, nil
}
func TestIsJSONMarshallable(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{
name: "testErr",
err: &testErr{},
want: true,
},
{
name: "baseError",
err: &baseError{},
want: true,
},
{
name: "nestedError",
err: &nestedError{},
want: true,
},
{
name: "withSubject",
err: &withSubject{},
want: true,
},
{
name: "standard error",
err: errors.New("test error"),
want: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := IsJSONMarshallable(test.err); got != test.want {
t.Errorf("IsJSONMarshallable(%v) = %v, want %v", test.err, got, test.want)
}
})
}
}

View file

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
) )
type ( type (
@ -31,7 +31,7 @@ const (
IconSourceSelfhSt IconSourceSelfhSt
) )
var ErrInvalidIconURL = E.New("invalid icon url") var ErrInvalidIconURL = gperr.New("invalid icon url")
func NewSelfhStIconURL(reference, format string) *IconURL { func NewSelfhStIconURL(reference, format string) *IconURL {
return &IconURL{ return &IconURL{

View file

@ -11,9 +11,10 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
F "github.com/yusing/go-proxy/internal/utils/functional" F "github.com/yusing/go-proxy/internal/utils/functional"
) )
@ -27,8 +28,9 @@ type memLogger struct {
sync.RWMutex sync.RWMutex
notifyLock sync.RWMutex notifyLock sync.RWMutex
connChans F.Map[chan *logEntryRange, struct{}] connChans F.Map[chan *logEntryRange, struct{}]
listeners F.Map[chan []byte, struct{}]
bufPool sync.Pool // used in hook mode bufPool sync.Pool
} }
type MemLogger io.Writer type MemLogger io.Writer
@ -41,15 +43,16 @@ const (
maxMemLogSize = 16 * 1024 maxMemLogSize = 16 * 1024
truncateSize = maxMemLogSize / 2 truncateSize = maxMemLogSize / 2
initialWriteChunkSize = 4 * 1024 initialWriteChunkSize = 4 * 1024
hookModeBufSize = 256 bufPoolSize = 256
) )
var memLoggerInstance = &memLogger{ var memLoggerInstance = &memLogger{
connChans: F.NewMapOf[chan *logEntryRange, struct{}](), connChans: F.NewMapOf[chan *logEntryRange, struct{}](),
listeners: F.NewMapOf[chan []byte, struct{}](),
bufPool: sync.Pool{ bufPool: sync.Pool{
New: func() any { New: func() any {
return &buffer{ return &buffer{
data: make([]byte, 0, hookModeBufSize), data: make([]byte, 0, bufPoolSize),
} }
}, },
}, },
@ -92,6 +95,10 @@ func HandlerFunc() http.HandlerFunc {
return memLoggerInstance.ServeHTTP return memLoggerInstance.ServeHTTP
} }
func Events() (<-chan []byte, func()) {
return memLoggerInstance.events()
}
func (m *memLogger) truncateIfNeeded(n int) { func (m *memLogger) truncateIfNeeded(n int) {
m.RLock() m.RLock()
needTruncate := m.Len()+n > maxMemLogSize needTruncate := m.Len()+n > maxMemLogSize
@ -111,7 +118,7 @@ func (m *memLogger) truncateIfNeeded(n int) {
func (m *memLogger) notifyWS(pos, n int) { func (m *memLogger) notifyWS(pos, n int) {
if m.connChans.Size() > 0 { if m.connChans.Size() > 0 {
timeout := time.NewTimer(1 * time.Second) timeout := time.NewTimer(2 * time.Second)
defer timeout.Stop() defer timeout.Stop()
m.notifyLock.RLock() m.notifyLock.RLock()
@ -125,6 +132,19 @@ func (m *memLogger) notifyWS(pos, n int) {
return false return false
} }
}) })
if m.listeners.Size() > 0 {
msg := make([]byte, n)
copy(msg, m.Buffer.Bytes()[pos:pos+n])
m.listeners.Range(func(ch chan []byte, _ struct{}) bool {
select {
case <-timeout.C:
logging.Warn().Msg("mem logger: timeout logging to channel")
return false
case ch <- msg:
return true
}
})
}
return return
} }
} }
@ -153,9 +173,9 @@ func (m *memLogger) Write(p []byte) (n int, err error) {
} }
func (m *memLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *memLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := utils.InitiateWS(w, r) conn, err := gpwebsocket.Initiate(w, r)
if err != nil { if err != nil {
utils.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
@ -172,13 +192,27 @@ func (m *memLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}() }()
if err := m.wsInitial(r.Context(), conn); err != nil { if err := m.wsInitial(r.Context(), conn); err != nil {
utils.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
m.wsStreamLog(r.Context(), conn, logCh) m.wsStreamLog(r.Context(), conn, logCh)
} }
func (m *memLogger) events() (<-chan []byte, func()) {
ch := make(chan []byte, 10)
m.notifyLock.Lock()
defer m.notifyLock.Unlock()
m.listeners.Store(ch, struct{}{})
return ch, func() {
m.notifyLock.Lock()
defer m.notifyLock.Unlock()
m.listeners.Delete(ch)
close(ch)
}
}
func (m *memLogger) writeBytes(ctx context.Context, conn *websocket.Conn, b []byte) error { func (m *memLogger) writeBytes(ctx context.Context, conn *websocket.Conn, b []byte) error {
return conn.Write(ctx, websocket.MessageText, b) return conn.Write(ctx, websocket.MessageText, b)
} }

View file

@ -7,9 +7,10 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
"github.com/yusing/go-proxy/internal/api/v1/utils"
metricsutils "github.com/yusing/go-proxy/internal/metrics/utils" metricsutils "github.com/yusing/go-proxy/internal/metrics/utils"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
) )
// ServeHTTP serves the data for the given period. // ServeHTTP serves the data for the given period.
@ -36,7 +37,7 @@ func (p *Poller[T, AggregateT]) ServeHTTP(w http.ResponseWriter, r *http.Request
if interval < minInterval { if interval < minInterval {
interval = minInterval interval = minInterval
} }
utils.PeriodicWS(w, r, interval, func(conn *websocket.Conn) error { gpwebsocket.Periodic(w, r, interval, func(conn *websocket.Conn) error {
data, err := p.getRespData(r) data, err := p.getRespData(r)
if err != nil { if err != nil {
return err return err
@ -49,14 +50,14 @@ func (p *Poller[T, AggregateT]) ServeHTTP(w http.ResponseWriter, r *http.Request
} else { } else {
data, err := p.getRespData(r) data, err := p.getRespData(r)
if err != nil { if err != nil {
utils.HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
if data == nil { if data == nil {
http.Error(w, "no data", http.StatusNoContent) http.Error(w, "no data", http.StatusNoContent)
return return
} }
utils.RespondJSON(w, r, data) gphttp.RespondJSON(w, r, data)
} }
} }

View file

@ -6,7 +6,7 @@ import (
"net/url" "net/url"
"time" "time"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
) )
@ -84,7 +84,7 @@ func (p *Poller[T, AggregateT]) gatherErrs() (string, bool) {
if len(p.errs) == 0 { if len(p.errs) == 0 {
return "", false return "", false
} }
errs := E.NewBuilder(fmt.Sprintf("poller %s has encountered %d errors in the last %s:", p.name, len(p.errs), gatherErrsInterval)) errs := gperr.NewBuilder(fmt.Sprintf("poller %s has encountered %d errors in the last %s:", p.name, len(p.errs), gatherErrsInterval))
for _, e := range p.errs { for _, e := range p.errs {
errs.Addf("%w: %d times", e.err, e.count) errs.Addf("%w: %d times", e.err, e.count)
} }

View file

@ -12,7 +12,7 @@ import (
"github.com/shirou/gopsutil/v4/net" "github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/sensors" "github.com/shirou/gopsutil/v4/sensors"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/metrics/period" "github.com/yusing/go-proxy/internal/metrics/period"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
@ -40,7 +40,7 @@ func _() { // check if this behavior is not changed
} }
func getSystemInfo(ctx context.Context, lastResult *SystemInfo) (*SystemInfo, error) { func getSystemInfo(ctx context.Context, lastResult *SystemInfo) (*SystemInfo, error) {
errs := E.NewBuilder("failed to get system info") errs := gperr.NewBuilder("failed to get system info")
var systemInfo SystemInfo var systemInfo SystemInfo
if !common.MetricsDisableCPU { if !common.MetricsDisableCPU {
@ -95,8 +95,8 @@ func getSystemInfo(ctx context.Context, lastResult *SystemInfo) (*SystemInfo, er
} }
if errs.HasError() { if errs.HasError() {
allWarnings := E.NewBuilder("") allWarnings := gperr.NewBuilder("")
allErrors := E.NewBuilder("failed to get system info") allErrors := gperr.NewBuilder("failed to get system info")
errs.ForEach(func(err error) { errs.ForEach(func(err error) {
// disk.Warnings has the same type // disk.Warnings has the same type
// all Warnings are alias of common.Warnings from "github.com/shirou/gopsutil/v4/internal/common" // all Warnings are alias of common.Warnings from "github.com/shirou/gopsutil/v4/internal/common"

View file

@ -7,7 +7,7 @@ import (
"sync" "sync"
"time" "time"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
) )
@ -131,7 +131,7 @@ func (l *AccessLogger) Flush(force bool) {
} }
func (l *AccessLogger) handleErr(err error) { func (l *AccessLogger) handleErr(err error) {
E.LogError("failed to write access log", err) gperr.LogError("failed to write access log", err)
} }
func (l *AccessLogger) start() { func (l *AccessLogger) start() {

View file

@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
. "github.com/yusing/go-proxy/internal/net/http/accesslog" . "github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/yusing/go-proxy/internal/docker" "github.com/yusing/go-proxy/internal/docker"
. "github.com/yusing/go-proxy/internal/net/http/accesslog" . "github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -3,7 +3,7 @@ package accesslog_test
import ( import (
"testing" "testing"
. "github.com/yusing/go-proxy/internal/net/http/accesslog" . "github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -5,7 +5,7 @@ import (
"net/http" "net/http"
"strings" "strings"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
@ -27,7 +27,7 @@ type (
CIDR struct{ types.CIDR } CIDR struct{ types.CIDR }
) )
var ErrInvalidHTTPHeaderFilter = E.New("invalid http header filter") var ErrInvalidHTTPHeaderFilter = gperr.New("invalid http header filter")
func (f *LogFilter[T]) CheckKeep(req *http.Request, res *http.Response) bool { func (f *LogFilter[T]) CheckKeep(req *http.Request, res *http.Response) bool {
if len(f.Values) == 0 { if len(f.Values) == 0 {

View file

@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"testing" "testing"
. "github.com/yusing/go-proxy/internal/net/http/accesslog" . "github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -7,7 +7,7 @@ import (
"strconv" "strconv"
"time" "time"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
@ -19,8 +19,8 @@ type Retention struct {
const chunkSizeMax int64 = 128 * 1024 // 128KB const chunkSizeMax int64 = 128 * 1024 // 128KB
var ( var (
ErrInvalidSyntax = E.New("invalid syntax") ErrInvalidSyntax = gperr.New("invalid syntax")
ErrZeroValue = E.New("zero value") ErrZeroValue = gperr.New("zero value")
) )
// Syntax: // Syntax:

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"time" "time"
. "github.com/yusing/go-proxy/internal/net/http/accesslog" . "github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"

View file

@ -3,7 +3,7 @@ package accesslog
import ( import (
"strconv" "strconv"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
) )
@ -12,7 +12,7 @@ type StatusCodeRange struct {
End int End int
} }
var ErrInvalidStatusCodeRange = E.New("invalid status code range") var ErrInvalidStatusCodeRange = gperr.New("invalid status code range")
func (r *StatusCodeRange) Includes(code int) bool { func (r *StatusCodeRange) Includes(code int) bool {
return r.Start <= code && code <= r.End return r.Start <= code && code <= r.End
@ -25,7 +25,7 @@ func (r *StatusCodeRange) Parse(v string) error {
case 1: case 1:
start, err := strconv.Atoi(split[0]) start, err := strconv.Atoi(split[0])
if err != nil { if err != nil {
return E.From(err) return gperr.Wrap(err)
} }
r.Start = start r.Start = start
r.End = start r.End = start
@ -33,7 +33,7 @@ func (r *StatusCodeRange) Parse(v string) error {
case 2: case 2:
start, errStart := strconv.Atoi(split[0]) start, errStart := strconv.Atoi(split[0])
end, errEnd := strconv.Atoi(split[1]) end, errEnd := strconv.Atoi(split[1])
if err := E.Join(errStart, errEnd); err != nil { if err := gperr.Join(errStart, errEnd); err != nil {
return err return err
} }
r.Start = start r.Start = start

View file

@ -1,7 +1,9 @@
package utils package gphttp
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
@ -10,8 +12,14 @@ import (
func WriteBody(w http.ResponseWriter, body []byte) { func WriteBody(w http.ResponseWriter, body []byte) {
if _, err := w.Write(body); err != nil { if _, err := w.Write(body); err != nil {
switch {
case errors.Is(err, http.ErrHandlerTimeout),
errors.Is(err, context.DeadlineExceeded):
logging.Err(err).Msg("timeout writing body")
default:
logging.Err(err).Msg("failed to write body") logging.Err(err).Msg("failed to write body")
} }
}
} }
func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int) (canProceed bool) { func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int) (canProceed bool) {
@ -25,13 +33,13 @@ func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int)
case string: case string:
_, err = w.Write([]byte(fmt.Sprintf("%q", data))) _, err = w.Write([]byte(fmt.Sprintf("%q", data)))
case []byte: case []byte:
_, err = w.Write(data) panic("use WriteBody instead")
default: default:
err = json.NewEncoder(w).Encode(data) err = json.NewEncoder(w).Encode(data)
} }
if err != nil { if err != nil {
HandleErr(w, r, err) LogError(r).Err(err).Msg("failed to encode json")
return false return false
} }
return true return true

View file

@ -1,4 +1,4 @@
package http package gphttp
import ( import (
"mime" "mime"

View file

@ -1,4 +1,4 @@
package http package gphttp
import ( import (
"net/http" "net/http"

View file

@ -1,22 +1,21 @@
package utils package gphttp
import ( import (
"crypto/tls" "crypto/tls"
"net" "net"
"net/http" "net/http"
"time"
"github.com/yusing/go-proxy/internal/common"
) )
var ( var (
httpClient = &http.Client{ httpClient = &http.Client{
Timeout: common.ConnectionTimeout, Timeout: 5 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
DisableKeepAlives: true, DisableKeepAlives: true,
ForceAttemptHTTP2: false, ForceAttemptHTTP2: false,
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
Timeout: common.DialTimeout, Timeout: 3 * time.Second,
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives KeepAlive: 60 * time.Second, // this is different from DisableKeepAlives
}).DialContext, }).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}, },

View file

@ -0,0 +1,95 @@
package gphttp
import (
"context"
"encoding/json"
"errors"
"net/http"
"syscall"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
)
// ServerError is for handling server errors.
//
// It logs the error and returns http.StatusInternalServerError to the client.
// Status code can be specified as an argument.
func ServerError(w http.ResponseWriter, r *http.Request, err error, code ...int) {
switch {
case err == nil,
errors.Is(err, context.Canceled),
errors.Is(err, syscall.EPIPE),
errors.Is(err, syscall.ECONNRESET):
return
}
LogError(r).Msg(err.Error())
if httpheaders.IsWebsocket(r.Header) {
return
}
if len(code) == 0 {
code = []int{http.StatusInternalServerError}
}
http.Error(w, http.StatusText(code[0]), code[0])
}
// ClientError is for responding to client errors.
//
// It returns http.StatusBadRequest with reason to the client.
// Status code can be specified as an argument.
//
// For JSON marshallable errors (e.g. gperr.Error), it returns the error details as JSON.
// Otherwise, it returns the error details as plain text.
func ClientError(w http.ResponseWriter, err error, code ...int) {
if len(code) == 0 {
code = []int{http.StatusBadRequest}
}
if gperr.IsJSONMarshallable(err) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(err)
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
}
http.Error(w, err.Error(), code[0])
}
// JSONError returns a JSON response of gperr.Error with the given status code.
func JSONError(w http.ResponseWriter, err gperr.Error, code int) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(err)
http.Error(w, err.Error(), code)
}
// BadRequest returns a Bad Request response with the given error message.
func BadRequest(w http.ResponseWriter, err string, code ...int) {
if len(code) == 0 {
code = []int{http.StatusBadRequest}
}
http.Error(w, err, code[0])
}
// Unauthorized returns an Unauthorized response with the given error message.
func Unauthorized(w http.ResponseWriter, err string) {
BadRequest(w, err, http.StatusUnauthorized)
}
// NotFound returns a Not Found response with the given error message.
func NotFound(w http.ResponseWriter, err string) {
BadRequest(w, err, http.StatusNotFound)
}
func ErrMissingKey(k string) error {
return gperr.New(k + " is required")
}
func ErrInvalidKey(k string) error {
return gperr.New(k + " is invalid")
}
func ErrAlreadyExists(k, v string) error {
return gperr.Errorf("%s %q already exists", k, v)
}
func ErrNotFound(k, v string) error {
return gperr.Errorf("%s %q not found", k, v)
}

View file

@ -1,4 +1,4 @@
package utils package gpwebsocket
import ( import (
"net/http" "net/http"
@ -7,8 +7,10 @@ import (
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
) )
func warnNoMatchDomains() { func warnNoMatchDomains() {
@ -17,7 +19,7 @@ func warnNoMatchDomains() {
var warnNoMatchDomainOnce sync.Once var warnNoMatchDomainOnce sync.Once
func InitiateWS(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) { func Initiate(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
var originPats []string var originPats []string
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"} localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
@ -42,17 +44,17 @@ func InitiateWS(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error)
}) })
} }
func PeriodicWS(w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) { func Periodic(w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
conn, err := InitiateWS(w, r) conn, err := Initiate(w, r)
if err != nil { if err != nil {
HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
//nolint:errcheck //nolint:errcheck
defer conn.CloseNow() defer conn.CloseNow()
if err := do(conn); err != nil { if err := do(conn); err != nil {
HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
@ -65,9 +67,20 @@ func PeriodicWS(w http.ResponseWriter, r *http.Request, interval time.Duration,
return return
case <-ticker.C: case <-ticker.C:
if err := do(conn); err != nil { if err := do(conn); err != nil {
HandleErr(w, r, err) gphttp.ServerError(w, r, err)
return return
} }
} }
} }
} }
// WriteText writes a text message to the websocket connection.
// It returns true if the message was written successfully, false otherwise.
// It logs an error if the message is not written successfully.
func WriteText(r *http.Request, conn *websocket.Conn, msg string) bool {
if err := conn.Write(r.Context(), websocket.MessageText, []byte(msg)); err != nil {
gperr.LogError("failed to write text message", err)
return false
}
return true
}

View file

@ -17,13 +17,15 @@ const (
HeaderXForwardedURI = "X-Forwarded-Uri" HeaderXForwardedURI = "X-Forwarded-Uri"
HeaderXRealIP = "X-Real-IP" HeaderXRealIP = "X-Real-IP"
HeaderUpstreamName = "X-GoDoxy-Upstream-Name"
HeaderUpstreamScheme = "X-GoDoxy-Upstream-Scheme"
HeaderUpstreamHost = "X-GoDoxy-Upstream-Host"
HeaderUpstreamPort = "X-GoDoxy-Upstream-Port"
HeaderContentType = "Content-Type" HeaderContentType = "Content-Type"
HeaderContentLength = "Content-Length" HeaderContentLength = "Content-Length"
HeaderUpstreamName = "X-Godoxy-Upstream-Name"
HeaderUpstreamScheme = "X-Godoxy-Upstream-Scheme"
HeaderUpstreamHost = "X-Godoxy-Upstream-Host"
HeaderUpstreamPort = "X-Godoxy-Upstream-Port"
HeaderGoDoxyCheckRedirect = "X-Godoxy-Check-Redirect"
) )
// Hop-by-hop headers. These are removed when sent to the backend. // Hop-by-hop headers. These are removed when sent to the backend.

View file

@ -6,8 +6,8 @@ import (
"net/http" "net/http"
"sync" "sync"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/gphttp/middleware"
) )
type ipHash struct { type ipHash struct {
@ -23,10 +23,10 @@ func (lb *LoadBalancer) newIPHash() impl {
if len(lb.Options) == 0 { if len(lb.Options) == 0 {
return impl return impl
} }
var err E.Error var err gperr.Error
impl.realIP, err = middleware.RealIP.New(lb.Options) impl.realIP, err = middleware.RealIP.New(lb.Options)
if err != nil { if err != nil {
E.LogError("invalid real_ip options, ignoring", err, &impl.l) gperr.LogError("invalid real_ip options, ignoring", err, &impl.l)
} }
return impl return impl
} }

View file

@ -6,10 +6,10 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/gperr"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
"github.com/yusing/go-proxy/internal/route/routes" "github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/watcher/health" "github.com/yusing/go-proxy/internal/watcher/health"
@ -54,7 +54,7 @@ func New(cfg *Config) *LoadBalancer {
} }
// Start implements task.TaskStarter. // Start implements task.TaskStarter.
func (lb *LoadBalancer) Start(parent task.Parent) E.Error { func (lb *LoadBalancer) Start(parent task.Parent) gperr.Error {
lb.startTime = time.Now() lb.startTime = time.Now()
lb.task = parent.Subtask("loadbalancer."+lb.Link, false) lb.task = parent.Subtask("loadbalancer."+lb.Link, false)
parent.OnCancel("lb_remove_route", func() { parent.OnCancel("lb_remove_route", func() {
@ -227,7 +227,7 @@ func (lb *LoadBalancer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable) http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
return return
} }
if r.Header.Get(common.HeaderCheckRedirect) != "" { if r.Header.Get(httpheaders.HeaderGoDoxyCheckRedirect) != "" {
// wake all servers // wake all servers
for _, srv := range srvs { for _, srv := range srvs {
if err := srv.TryWake(); err != nil { if err := srv.TryWake(); err != nil {

View file

@ -3,7 +3,7 @@ package loadbalancer
import ( import (
"testing" "testing"
"github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" "github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )

View file

@ -1,7 +1,7 @@
package loadbalancer package loadbalancer
import ( import (
"github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" "github.com/yusing/go-proxy/internal/net/gphttp/loadbalancer/types"
) )
type ( type (

View file

@ -1,4 +1,4 @@
package utils package gphttp
import ( import (
"net/http" "net/http"

View file

@ -1,4 +1,4 @@
package http package gphttp
import "net/http" import "net/http"

View file

@ -5,7 +5,7 @@ import (
"net/http" "net/http"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/types" "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
F "github.com/yusing/go-proxy/internal/utils/functional" F "github.com/yusing/go-proxy/internal/utils/functional"

View file

@ -7,7 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )
@ -61,7 +61,7 @@ func TestCIDRWhitelistValidation(t *testing.T) {
} }
func TestCIDRWhitelist(t *testing.T) { func TestCIDRWhitelist(t *testing.T) {
errs := E.NewBuilder("") errs := gperr.NewBuilder("")
mids := BuildMiddlewaresFromYAML("", testCIDRWhitelistCompose, errs) mids := BuildMiddlewaresFromYAML("", testCIDRWhitelistCompose, errs)
ExpectNoError(t, errs.Error()) ExpectNoError(t, errs.Error())
deny = mids["deny@file"] deny = mids["deny@file"]

View file

@ -9,9 +9,9 @@ import (
"strings" "strings"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
gphttp "github.com/yusing/go-proxy/internal/net/http" gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/http/httpheaders" "github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/http/middleware/errorpage" "github.com/yusing/go-proxy/internal/net/gphttp/middleware/errorpage"
) )
type customErrorPage struct{} type customErrorPage struct{}

View file

@ -7,7 +7,7 @@ import (
"sync" "sync"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
U "github.com/yusing/go-proxy/internal/utils" U "github.com/yusing/go-proxy/internal/utils"
@ -90,7 +90,7 @@ func watchDir() {
loadContent() loadContent()
} }
case err := <-errCh: case err := <-errCh:
E.LogError("error watching error page directory", err) gperr.LogError("error watching error page directory", err)
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show more