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

@@ -13,7 +13,7 @@ import (
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"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"
)

View File

@@ -6,18 +6,19 @@ import (
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
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) {
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())
return nil
})
} else {
U.RespondJSON(w, r, cfg.ListAgents())
gphttp.RespondJSON(w, r, cfg.ListAgents())
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,10 +13,10 @@ import (
"github.com/PuerkitoBio/goquery"
"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/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"
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) {
url, alias := req.FormValue("url"), req.FormValue("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
}
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
}
@@ -65,7 +65,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
if url != "" {
var iconURL homepage.IconURL
if err := iconURL.Parse(url); err != nil {
U.RespondError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, err, http.StatusBadRequest)
return
}
fetchResult := getFavIconFromURL(&iconURL)
@@ -74,14 +74,14 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
return
}
w.Header().Set("Content-Type", fetchResult.ContentType())
U.WriteBody(w, fetchResult.icon)
gphttp.WriteBody(w, fetchResult.icon)
return
}
// try with route.Homepage.Icon
r, ok := routes.GetHTTPRoute(alias)
if !ok {
U.RespondError(w, errors.New("no such route"), http.StatusNotFound)
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
return
}
@@ -105,7 +105,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
return
}
w.Header().Set("Content-Type", result.ContentType())
U.WriteBody(w, result.icon)
gphttp.WriteBody(w, result.icon)
}
func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult {
@@ -125,7 +125,7 @@ func fetchIconAbsolute(url string) *fetchResult {
return result
}
resp, err := U.Get(url)
resp, err := gphttp.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
if err == nil {
err = errors.New(resp.Status)

View File

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

View File

@@ -6,17 +6,18 @@ import (
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/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"
"github.com/yusing/go-proxy/internal/route/routes/routequery"
)
func Health(w http.ResponseWriter, r *http.Request) {
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())
})
} else {
U.RespondJSON(w, r, routequery.HealthMap())
gphttp.RespondJSON(w, r, routequery.HealthMap())
}
}

View File

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

View File

@@ -3,9 +3,9 @@ package v1
import (
"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) {
WriteBody(w, []byte("API ready"))
gphttp.WriteBody(w, []byte("API ready"))
}

View File

@@ -1,15 +1,16 @@
package v1
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/yusing/go-proxy/internal"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
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"
route "github.com/yusing/go-proxy/internal/route/types"
"github.com/yusing/go-proxy/internal/task"
@@ -41,26 +42,25 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
case ListRoute:
if route := listRoute(which); route == nil {
http.NotFound(w, r)
return
} else {
U.RespondJSON(w, r, route)
gphttp.RespondJSON(w, r, route)
}
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:
listFiles(w, r)
case ListMiddlewares:
U.RespondJSON(w, r, middleware.All())
gphttp.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces:
U.RespondJSON(w, r, middleware.GetAllTrace())
gphttp.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains:
U.RespondJSON(w, r, cfg.Value().MatchDomains)
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
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:
U.RespondJSON(w, r, cfg.RouteProviderList())
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
case ListHomepageCategories:
U.RespondJSON(w, r, routequery.HomepageCategories())
gphttp.RespondJSON(w, r, routequery.HomepageCategories())
case ListIcons:
limit, err := strconv.Atoi(r.FormValue("limit"))
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)
if err != nil {
U.RespondError(w, err)
gphttp.ClientError(w, err)
return
}
if icons == nil {
icons = []string{}
}
U.RespondJSON(w, r, icons)
gphttp.RespondJSON(w, r, icons)
case ListTasks:
U.RespondJSON(w, r, task.DebugTaskList())
gphttp.RespondJSON(w, r, task.DebugTaskList())
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) {
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
if err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}
resp := map[FileType][]string{
@@ -116,12 +116,12 @@ func listFiles(w http.ResponseWriter, r *http.Request) {
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
if err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}
for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
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/certs"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
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"
)
@@ -21,27 +22,27 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
name := q.Get("name")
if name == "" {
U.RespondError(w, U.ErrMissingKey("name"))
gphttp.ClientError(w, gphttp.ErrMissingKey("name"))
return
}
host := q.Get("host")
if host == "" {
U.RespondError(w, U.ErrMissingKey("host"))
gphttp.ClientError(w, gphttp.ErrMissingKey("host"))
return
}
portStr := q.Get("port")
if portStr == "" {
U.RespondError(w, U.ErrMissingKey("port"))
gphttp.ClientError(w, gphttp.ErrMissingKey("port"))
return
}
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
U.RespondError(w, U.ErrInvalidKey("port"))
gphttp.ClientError(w, gphttp.ErrInvalidKey("port"))
return
}
hostport := fmt.Sprintf("%s:%d", host, port)
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
}
t := q.Get("type")
@@ -49,13 +50,13 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
case "docker":
break
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
case "":
U.RespondError(w, U.ErrMissingKey("type"))
gphttp.ClientError(w, gphttp.ErrMissingKey("type"))
return
default:
U.RespondError(w, U.ErrInvalidKey("type"))
gphttp.ClientError(w, gphttp.ErrInvalidKey("type"))
return
}
@@ -69,7 +70,7 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
ca, srv, client, err := agent.NewAgent()
if err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}
@@ -83,11 +84,11 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
template, err := cfg.Generate()
if err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}
U.RespondJSON(w, r, map[string]any{
gphttp.RespondJSON(w, r, map[string]any{
"compose": template,
"ca": ca,
"client": client,
@@ -98,7 +99,7 @@ func AddAgent(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
clientPEMData, err := io.ReadAll(r.Body)
if err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}
@@ -109,24 +110,24 @@ func AddAgent(w http.ResponseWriter, r *http.Request) {
}
if err := json.Unmarshal(clientPEMData, &data); err != nil {
U.RespondError(w, err, http.StatusBadRequest)
gphttp.ClientError(w, err, http.StatusBadRequest)
return
}
nRoutesAdded, err := config.GetInstance().AddAgent(data.Host, data.CA, data.Client)
if err != nil {
U.RespondError(w, err)
gphttp.ClientError(w, err)
return
}
zip, err := certs.ZipCert(data.CA.Cert, data.Client.Cert, data.Client.Key)
if err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}
if err := os.WriteFile(certs.AgentCertsFilename(data.Host), zip, 0600); err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
return
}

View File

@@ -7,20 +7,20 @@ import (
"net/http"
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"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/net/http/middleware"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/net/gphttp"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
)
func ReloadServer() E.Error {
resp, err := U.Post(common.APIHTTPURL+"/v1/reload", "", nil)
func ReloadServer() gperr.Error {
resp, err := gphttp.Post(common.APIHTTPURL+"/v1/reload", "", nil)
if err != nil {
return E.From(err)
return gperr.Wrap(err)
}
defer resp.Body.Close()
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)
if err != nil {
return failure.With(err)
@@ -31,34 +31,34 @@ func ReloadServer() E.Error {
return nil
}
func List[T any](what string) (_ T, outErr E.Error) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
func List[T any](what string) (_ T, outErr gperr.Error) {
resp, err := gphttp.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
if err != nil {
outErr = E.From(err)
outErr = gperr.Wrap(err)
return
}
defer resp.Body.Close()
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
}
var res T
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
outErr = E.From(err)
outErr = gperr.Wrap(err)
return
}
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)
}
func ListMiddlewareTraces() (middleware.Traces, E.Error) {
func ListMiddlewareTraces() (middleware.Traces, gperr.Error) {
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)
}

View File

@@ -3,14 +3,14 @@ package v1
import (
"net/http"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
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) {
if err := cfg.Reload(); err != nil {
U.HandleErr(w, r, err)
gphttp.ServerError(w, r, err)
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/wsjson"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
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"
)
func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
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))
})
} else {
U.RespondJSON(w, r, getStats(cfg))
gphttp.RespondJSON(w, r, getStats(cfg))
}
}

View File

@@ -4,12 +4,12 @@ import (
"net/http"
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"
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/net/http/httpheaders"
"github.com/yusing/go-proxy/internal/net/http/reverseproxy"
"github.com/yusing/go-proxy/internal/net/gphttp"
"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) {
@@ -23,7 +23,7 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
agent, ok := cfg.GetAgent(agentAddr)
if !ok {
U.HandleErr(w, r, U.ErrInvalidKey("agent_addr"), http.StatusNotFound)
gphttp.NotFound(w, "agent_addr")
return
}
@@ -31,20 +31,20 @@ func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
if !isWS {
respData, status, err := agent.Forward(r, agentPkg.EndpointSystemInfo)
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
}
if status != http.StatusOK {
http.Error(w, string(respData), status)
return
}
U.WriteBody(w, respData)
gphttp.WriteBody(w, respData)
} else {
rp := reverseproxy.NewReverseProxy("agent", agentPkg.AgentURL, agent.Transport())
header := r.Header.Clone()
r, err := http.NewRequestWithContext(r.Context(), r.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), 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
}
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

@@ -1,28 +0,0 @@
package utils
import (
"crypto/tls"
"net"
"net/http"
"github.com/yusing/go-proxy/internal/common"
)
var (
httpClient = &http.Client{
Timeout: common.ConnectionTimeout,
Transport: &http.Transport{
DisableKeepAlives: true,
ForceAttemptHTTP2: false,
DialContext: (&net.Dialer{
Timeout: common.DialTimeout,
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
}).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
Get = httpClient.Get
Post = httpClient.Post
Head = httpClient.Head
)

View File

@@ -1,20 +0,0 @@
package utils
import (
"net/http"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/logging"
)
func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
return logging.WithLevel(level).
Str("remote", r.RemoteAddr).
Str("host", r.Host).
Str("uri", r.Method+" "+r.RequestURI)
}
func LogError(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.ErrorLevel) }
func LogWarn(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.WarnLevel) }
func LogInfo(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.InfoLevel) }
func LogDebug(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.DebugLevel) }

View File

@@ -1,38 +0,0 @@
package utils
import (
"encoding/json"
"fmt"
"net/http"
"github.com/yusing/go-proxy/internal/logging"
)
func WriteBody(w http.ResponseWriter, body []byte) {
if _, err := w.Write(body); err != nil {
logging.Err(err).Msg("failed to write body")
}
}
func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int) (canProceed bool) {
if len(code) > 0 {
w.WriteHeader(code[0])
}
w.Header().Set("Content-Type", "application/json")
var err error
switch data := data.(type) {
case string:
_, err = w.Write([]byte(fmt.Sprintf("%q", data)))
case []byte:
_, err = w.Write(data)
default:
err = json.NewEncoder(w).Encode(data)
}
if err != nil {
HandleErr(w, r, err)
return false
}
return true
}

View File

@@ -1,73 +0,0 @@
package utils
import (
"net/http"
"sync"
"time"
"github.com/coder/websocket"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/httpheaders"
)
func warnNoMatchDomains() {
logging.Warn().Msg("no match domains configured, accepting websocket API request from all origins")
}
var warnNoMatchDomainOnce sync.Once
func InitiateWS(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
var originPats []string
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
allowedDomains := httpheaders.WebsocketAllowedDomains(r.Header)
if len(allowedDomains) == 0 || common.IsDebug {
warnNoMatchDomainOnce.Do(warnNoMatchDomains)
originPats = []string{"*"}
} else {
originPats = make([]string, len(allowedDomains))
for i, domain := range allowedDomains {
if domain[0] != '.' {
originPats[i] = "*." + domain
} else {
originPats[i] = "*" + domain
}
}
originPats = append(originPats, localAddresses...)
}
return websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: originPats,
})
}
func PeriodicWS(w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
conn, err := InitiateWS(w, r)
if err != nil {
HandleErr(w, r, err)
return
}
//nolint:errcheck
defer conn.CloseNow()
if err := do(conn); err != nil {
HandleErr(w, r, err)
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
if err := do(conn); err != nil {
HandleErr(w, r, err)
return
}
}
}
}

View File

@@ -3,10 +3,10 @@ package v1
import (
"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"
)
func GetVersion(w http.ResponseWriter, r *http.Request) {
WriteBody(w, []byte(pkg.GetVersion()))
gphttp.WriteBody(w, []byte(pkg.GetVersion()))
}