preparing for v0.5

This commit is contained in:
default
2024-08-01 10:06:42 +08:00
parent 24778d1093
commit 93359110a2
115 changed files with 5153 additions and 4395 deletions

29
src/api/handler.go Normal file
View File

@@ -0,0 +1,29 @@
package api
import (
"net/http"
v1 "github.com/yusing/go-proxy/api/v1"
"github.com/yusing/go-proxy/config"
)
func NewHandler(cfg *config.Config) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1", v1.Index)
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/file", v1.GetFileContent)
mux.HandleFunc("GET /v1/file/{filename}", v1.GetFileContent)
mux.HandleFunc("PUT /v1/file/{filename}", v1.SetFileContent)
mux.HandleFunc("GET /v1/stats", wrap(cfg, v1.Stats))
return mux
}
func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
f(cfg, w, r)
}
}

49
src/api/v1/checkhealth.go Normal file
View File

@@ -0,0 +1,49 @@
package v1
import (
"fmt"
"net/http"
U "github.com/yusing/go-proxy/api/v1/utils"
"github.com/yusing/go-proxy/config"
R "github.com/yusing/go-proxy/route"
)
func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
target := r.FormValue("target")
if target == "" {
U.HandleErr(w, r, U.ErrMissingKey("target"), http.StatusBadRequest)
return
}
var ok bool
switch route := cfg.FindRoute(target).(type) {
case nil:
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
return
case *R.HTTPRoute:
path := r.FormValue("path")
if path == "" {
U.HandleErr(w, r, U.ErrMissingKey("path"), http.StatusBadRequest)
return
}
sr, hasSr := route.GetSubroute(path)
if !hasSr {
U.HandleErr(w, r, U.ErrNotFound("path", path), http.StatusNotFound)
return
}
ok = U.IsSiteHealthy(sr.TargetURL.String())
case *R.StreamRoute:
ok = U.IsStreamHealthy(
route.Scheme.ProxyScheme.String(),
fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort),
)
}
if ok {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusRequestTimeout)
}
}

58
src/api/v1/file.go Normal file
View File

@@ -0,0 +1,58 @@
package v1
import (
"net/http"
"os"
"path"
U "github.com/yusing/go-proxy/api/v1/utils"
"github.com/yusing/go-proxy/common"
"github.com/yusing/go-proxy/config"
E "github.com/yusing/go-proxy/error"
"github.com/yusing/go-proxy/proxy/provider"
)
func GetFileContent(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
filename = common.ConfigFileName
}
content, err := os.ReadFile(path.Join(common.ConfigBasePath, filename))
if err != nil {
U.HandleErr(w, r, err)
return
}
w.Write(content)
}
func SetFileContent(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
U.HandleErr(w, r, U.ErrMissingKey("filename"), http.StatusBadRequest)
return
}
content := make([]byte, r.ContentLength)
_, err := E.Check(r.Body.Read(content))
if err.IsNotNil() {
U.HandleErr(w, r, err)
return
}
if filename == common.ConfigFileName {
err = config.Validate(content)
} else {
err = provider.Validate(content)
}
if err.IsNotNil() {
U.HandleErr(w, r, err)
return
}
err = E.From(os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0644))
if err.IsNotNil() {
U.HandleErr(w, r, err)
return
}
w.WriteHeader(http.StatusOK)
}

7
src/api/v1/index.go Normal file
View File

@@ -0,0 +1,7 @@
package v1
import "net/http"
func Index(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("API ready"))
}

62
src/api/v1/list.go Normal file
View File

@@ -0,0 +1,62 @@
package v1
import (
"encoding/json"
"net/http"
"os"
"github.com/yusing/go-proxy/common"
"github.com/yusing/go-proxy/config"
U "github.com/yusing/go-proxy/api/v1/utils"
)
func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
what := r.PathValue("what")
if what == "" {
what = "routes"
}
switch what {
case "routes":
listRoutes(cfg, w, r)
case "config_files":
listConfigFiles(w, r)
default:
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
}
}
func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
routes := cfg.RoutesByAlias()
type_filter := r.FormValue("type")
if type_filter != "" {
for k, v := range routes {
if v["type"] != type_filter {
delete(routes, k)
}
}
}
if err := U.RespondJson(routes, w); err != nil {
U.HandleErr(w, r, err)
}
}
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
files, err := os.ReadDir(common.ConfigBasePath)
if err != nil {
U.HandleErr(w, r, err)
return
}
filenames := make([]string, len(files))
for i, f := range files {
filenames[i] = f.Name()
}
resp, err := json.Marshal(filenames)
if err != nil {
U.HandleErr(w, r, err)
return
}
w.Write(resp)
}

16
src/api/v1/reload.go Normal file
View File

@@ -0,0 +1,16 @@
package v1
import (
"net/http"
U "github.com/yusing/go-proxy/api/v1/utils"
"github.com/yusing/go-proxy/config"
)
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
if err := cfg.Reload(); err.IsNotNil() {
U.HandleErr(w, r, err)
return
}
w.WriteHeader(http.StatusOK)
}

20
src/api/v1/stats.go Normal file
View File

@@ -0,0 +1,20 @@
package v1
import (
"net/http"
U "github.com/yusing/go-proxy/api/v1/utils"
"github.com/yusing/go-proxy/config"
"github.com/yusing/go-proxy/server"
"github.com/yusing/go-proxy/utils"
)
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
stats := map[string]interface{}{
"proxies": cfg.Statistics(),
"uptime": utils.FormatDuration(server.GetProxyServer().Uptime()),
}
if err := U.RespondJson(stats, w); err != nil {
U.HandleErr(w, r, err)
}
}

32
src/api/v1/utils/error.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import (
"errors"
"fmt"
"net/http"
"github.com/sirupsen/logrus"
E "github.com/yusing/go-proxy/error"
)
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
err = E.From(err).Subjectf("%s %s", r.Method, r.URL)
logrus.WithField("?", "api").Error(err)
if len(code) > 0 {
http.Error(w, err.Error(), code[0])
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func ErrMissingKey(k string) error {
return errors.New("missing key '" + k + "' in query or request body")
}
func ErrInvalidKey(k string) error {
return errors.New("invalid key '" + k + "' in query or request body")
}
func ErrNotFound(k, v string) error {
return fmt.Errorf("key %q with value %q not found", k, v)
}

62
src/api/v1/utils/net.go Normal file
View File

@@ -0,0 +1,62 @@
package utils
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"github.com/yusing/go-proxy/common"
E "github.com/yusing/go-proxy/error"
)
func IsSiteHealthy(url string) bool {
// try HEAD first
// if HEAD is not allowed, try GET
resp, err := HttpClient.Head(url)
if resp != nil {
resp.Body.Close()
}
if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed {
_, err = HttpClient.Get(url)
}
if resp != nil {
resp.Body.Close()
}
return err == nil
}
func IsStreamHealthy(scheme, address string) bool {
conn, err := net.DialTimeout(scheme, address, common.DialTimeout)
if err != nil {
return false
}
conn.Close()
return true
}
func ReloadServer() E.NestedError {
resp, err := HttpClient.Post(fmt.Sprintf("http://localhost%v/reload", common.APIHTTPPort), "", nil)
if err != nil {
return E.From(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return E.Failure("server reload").Subjectf("status code: %v", resp.StatusCode)
}
return E.Nil()
}
var HttpClient = &http.Client{
Timeout: common.ConnectionTimeout,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DisableKeepAlives: true,
ForceAttemptHTTP2: true,
DialContext: (&net.Dialer{
Timeout: common.DialTimeout,
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
}).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}

17
src/api/v1/utils/utils.go Normal file
View File

@@ -0,0 +1,17 @@
package utils
import (
"encoding/json"
"net/http"
)
func RespondJson(data any, w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
j, err := json.Marshal(data)
if err != nil {
return err
} else {
w.Write(j)
}
return nil
}