Fixed a few issues:

- Incorrect name being shown on dashboard "Proxies page"
- Apps being shown when homepage.show is false
- Load balanced routes are shown on homepage instead of the load balancer
- Route with idlewatcher will now be removed on container destroy
- Idlewatcher panic
- Performance improvement
- Idlewatcher infinitely loading
- Reload stucked / not working properly
- Streams stuck on shutdown / reload
- etc...
Added:
- support idlewatcher for loadbalanced routes
- partial implementation for stream type idlewatcher
Issues:
- graceful shutdown
This commit is contained in:
yusing
2024-10-18 16:47:01 +08:00
parent c0c61709ca
commit 53557e38b6
69 changed files with 2368 additions and 1654 deletions

View File

@@ -2,6 +2,7 @@ package api
import (
"fmt"
"net"
"net/http"
v1 "github.com/yusing/go-proxy/internal/api/v1"
@@ -21,34 +22,35 @@ func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc
mux.ServeMux.HandleFunc(fmt.Sprintf("%s %s", method, endpoint), checkHost(handler))
}
func NewHandler(cfg *config.Config) http.Handler {
func NewHandler() http.Handler {
mux := NewServeMux()
mux.HandleFunc("GET", "/v1", v1.Index)
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
mux.HandleFunc("GET", "/v1/checkhealth", wrap(cfg, v1.CheckHealth))
mux.HandleFunc("HEAD", "/v1/checkhealth", wrap(cfg, v1.CheckHealth))
mux.HandleFunc("POST", "/v1/reload", wrap(cfg, v1.Reload))
mux.HandleFunc("GET", "/v1/list", wrap(cfg, v1.List))
mux.HandleFunc("GET", "/v1/list/{what}", wrap(cfg, v1.List))
mux.HandleFunc("GET", "/v1/checkhealth", v1.CheckHealth)
mux.HandleFunc("HEAD", "/v1/checkhealth", v1.CheckHealth)
mux.HandleFunc("POST", "/v1/reload", v1.Reload)
mux.HandleFunc("GET", "/v1/list", v1.List)
mux.HandleFunc("GET", "/v1/list/{what}", v1.List)
mux.HandleFunc("GET", "/v1/file", v1.GetFileContent)
mux.HandleFunc("GET", "/v1/file/{filename...}", v1.GetFileContent)
mux.HandleFunc("POST", "/v1/file/{filename...}", v1.SetFileContent)
mux.HandleFunc("PUT", "/v1/file/{filename...}", v1.SetFileContent)
mux.HandleFunc("GET", "/v1/stats", wrap(cfg, v1.Stats))
mux.HandleFunc("GET", "/v1/stats/ws", wrap(cfg, v1.StatsWS))
mux.HandleFunc("GET", "/v1/stats", v1.Stats)
mux.HandleFunc("GET", "/v1/stats/ws", v1.StatsWS)
mux.HandleFunc("GET", "/v1/error_page", errorpage.GetHandleFunc())
return mux
}
// allow only requests to API server with host matching common.APIHTTPAddr.
// allow only requests to API server with localhost.
func checkHost(f http.HandlerFunc) http.HandlerFunc {
if common.IsDebug {
return f
}
return func(w http.ResponseWriter, r *http.Request) {
if r.Host != common.APIHTTPAddr {
Logger.Warnf("invalid request to API server with host: %s, expect %s", r.Host, common.APIHTTPAddr)
http.Error(w, "invalid request", http.StatusForbidden)
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if host != "127.0.0.1" && host != "localhost" && host != "[::1]" {
Logger.Warnf("blocked API request from %s", host)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
f(w, r)

View File

@@ -4,11 +4,10 @@ import (
"net/http"
. "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/watcher/health"
)
func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
func CheckHealth(w http.ResponseWriter, r *http.Request) {
target := r.FormValue("target")
if target == "" {
HandleErr(w, r, ErrMissingKey("target"), http.StatusBadRequest)

View File

@@ -11,7 +11,7 @@ import (
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/proxy/provider"
"github.com/yusing/go-proxy/internal/route/provider"
)
func GetFileContent(w http.ResponseWriter, r *http.Request) {

View File

@@ -9,19 +9,21 @@ import (
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/net/http/middleware"
"github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
)
const (
ListRoutes = "routes"
ListConfigFiles = "config_files"
ListMiddlewares = "middlewares"
ListMiddlewareTrace = "middleware_trace"
ListMatchDomains = "match_domains"
ListHomepageConfig = "homepage_config"
ListRoutes = "routes"
ListConfigFiles = "config_files"
ListMiddlewares = "middlewares"
ListMiddlewareTraces = "middleware_trace"
ListMatchDomains = "match_domains"
ListHomepageConfig = "homepage_config"
ListTasks = "tasks"
)
func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
func List(w http.ResponseWriter, r *http.Request) {
what := r.PathValue("what")
if what == "" {
what = ListRoutes
@@ -29,27 +31,24 @@ func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
switch what {
case ListRoutes:
listRoutes(cfg, w, r)
U.RespondJSON(w, r, config.RoutesByAlias(route.RouteType(r.FormValue("type"))))
case ListConfigFiles:
listConfigFiles(w, r)
case ListMiddlewares:
listMiddlewares(w, r)
case ListMiddlewareTrace:
listMiddlewareTrace(w, r)
U.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces:
U.RespondJSON(w, r, middleware.GetAllTrace())
case ListMatchDomains:
listMatchDomains(cfg, w, r)
U.RespondJSON(w, r, config.Value().MatchDomains)
case ListHomepageConfig:
listHomepageConfig(cfg, w, r)
U.RespondJSON(w, r, config.HomepageConfig())
case ListTasks:
U.RespondJSON(w, r, task.DebugTaskMap())
default:
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
}
}
func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
routes := cfg.RoutesByAlias(route.RouteType(r.FormValue("type")))
U.RespondJSON(w, r, routes)
}
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 1)
if err != nil {
@@ -61,19 +60,3 @@ func listConfigFiles(w http.ResponseWriter, r *http.Request) {
}
U.RespondJSON(w, r, files)
}
func listMiddlewareTrace(w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, middleware.GetAllTrace())
}
func listMiddlewares(w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, middleware.All())
}
func listMatchDomains(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, cfg.Value().MatchDomains)
}
func listHomepageConfig(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, cfg.HomepageConfig())
}

View File

@@ -34,36 +34,34 @@ func ReloadServer() E.NestedError {
return nil
}
func ListRoutes() (map[string]map[string]any, E.NestedError) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, v1.ListRoutes))
func List[T any](what string) (_ T, outErr E.NestedError) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what))
if err != nil {
return nil, E.From(err)
outErr = E.From(err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, E.Failure("list routes").Extraf("status code: %v", resp.StatusCode)
outErr = E.Failure("list "+what).Extraf("status code: %v", resp.StatusCode)
return
}
var routes map[string]map[string]any
err = json.NewDecoder(resp.Body).Decode(&routes)
var res T
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return nil, E.From(err)
outErr = E.From(err)
return
}
return routes, nil
return res, nil
}
func ListRoutes() (map[string]map[string]any, E.NestedError) {
return List[map[string]map[string]any](v1.ListRoutes)
}
func ListMiddlewareTraces() (middleware.Traces, E.NestedError) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, v1.ListMiddlewareTrace))
if err != nil {
return nil, E.From(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, E.Failure("list middleware trace").Extraf("status code: %v", resp.StatusCode)
}
var traces middleware.Traces
err = json.NewDecoder(resp.Body).Decode(&traces)
if err != nil {
return nil, E.From(err)
}
return traces, nil
return List[middleware.Traces](v1.ListMiddlewareTraces)
}
func DebugListTasks() (map[string]any, E.NestedError) {
return List[map[string]any](v1.ListTasks)
}

View File

@@ -7,8 +7,8 @@ import (
"github.com/yusing/go-proxy/internal/config"
)
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
if err := cfg.Reload(); err != nil {
func Reload(w http.ResponseWriter, r *http.Request) {
if err := config.Reload(); err != nil {
U.RespondJSON(w, r, err.JSONObject(), http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)

View File

@@ -14,19 +14,19 @@ import (
"github.com/yusing/go-proxy/internal/utils"
)
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, getStats(cfg))
func Stats(w http.ResponseWriter, r *http.Request) {
U.RespondJSON(w, r, getStats())
}
func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
func StatsWS(w http.ResponseWriter, r *http.Request) {
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
originPats := make([]string, len(cfg.Value().MatchDomains)+len(localAddresses))
originPats := make([]string, len(config.Value().MatchDomains)+len(localAddresses))
if len(originPats) == 0 {
U.Logger.Warnf("no match domains configured, accepting websocket request from all origins")
originPats = []string{"*"}
} else {
for i, domain := range cfg.Value().MatchDomains {
for i, domain := range config.Value().MatchDomains {
originPats[i] = "*." + domain
}
originPats = append(originPats, localAddresses...)
@@ -51,7 +51,7 @@ func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
defer ticker.Stop()
for range ticker.C {
stats := getStats(cfg)
stats := getStats()
if err := wsjson.Write(ctx, conn, stats); err != nil {
U.Logger.Errorf("/stats/ws failed to write JSON: %s", err)
return
@@ -59,9 +59,9 @@ func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
}
}
func getStats(cfg *config.Config) map[string]any {
func getStats() map[string]any {
return map[string]any{
"proxies": cfg.Statistics(),
"proxies": config.Statistics(),
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
}
}