mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 12:42:34 +02:00
replace all schema check with go-playground/validator/v10
This commit is contained in:
parent
00f60a6e78
commit
6aefe4d5d9
23 changed files with 149 additions and 250 deletions
|
@ -44,9 +44,6 @@ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
# copy binary
|
# copy binary
|
||||||
COPY --from=builder /app /app
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
# copy schema directory
|
|
||||||
COPY schema/ /app/schema/
|
|
||||||
|
|
||||||
# copy example config
|
# copy example config
|
||||||
COPY config.example.yml /app/config/config.yml
|
COPY config.example.yml /app/config/config.yml
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -14,7 +14,6 @@ require (
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.20.5
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
|
||||||
golang.org/x/net v0.32.0
|
golang.org/x/net v0.32.0
|
||||||
golang.org/x/text v0.21.0
|
golang.org/x/text v0.21.0
|
||||||
golang.org/x/time v0.8.0
|
golang.org/x/time v0.8.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -128,8 +128,6 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|
|
@ -27,6 +27,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConfig(cfg *types.AutoCertConfig) *Config {
|
func NewConfig(cfg *types.AutoCertConfig) *Config {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = new(types.AutoCertConfig)
|
||||||
|
}
|
||||||
if cfg.CertPath == "" {
|
if cfg.CertPath == "" {
|
||||||
cfg.CertPath = CertFileDefault
|
cfg.CertPath = CertFileDefault
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,11 @@ import (
|
||||||
var (
|
var (
|
||||||
prefixes = []string{"GODOXY_", "GOPROXY_", ""}
|
prefixes = []string{"GODOXY_", "GOPROXY_", ""}
|
||||||
|
|
||||||
NoSchemaValidation = GetEnvBool("NO_SCHEMA_VALIDATION", true)
|
IsTest = GetEnvBool("TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
||||||
IsTest = GetEnvBool("TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
IsDebug = GetEnvBool("DEBUG", IsTest)
|
||||||
IsDebug = GetEnvBool("DEBUG", IsTest)
|
IsDebugSkipAuth = GetEnvBool("DEBUG_SKIP_AUTH", false)
|
||||||
IsDebugSkipAuth = GetEnvBool("DEBUG_SKIP_AUTH", false)
|
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
||||||
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
IsProduction = !IsTest && !IsDebug
|
||||||
IsProduction = !IsTest && !IsDebug
|
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
ProxyHTTPHost,
|
ProxyHTTPHost,
|
||||||
|
|
|
@ -16,11 +16,10 @@ import (
|
||||||
"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"
|
||||||
U "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"
|
||||||
"github.com/yusing/go-proxy/internal/watcher"
|
"github.com/yusing/go-proxy/internal/watcher"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -68,7 +67,8 @@ func Load() (*Config, E.Error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Validate(data []byte) E.Error {
|
func Validate(data []byte) E.Error {
|
||||||
return U.ValidateYaml(U.GetSchema(common.ConfigSchemaPath), data)
|
var model *types.Config
|
||||||
|
return utils.DeserializeYAML(data, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MatchDomains() []string {
|
func MatchDomains() []string {
|
||||||
|
@ -160,14 +160,8 @@ func (cfg *Config) load() E.Error {
|
||||||
E.LogFatal(errMsg, err, &logger)
|
E.LogFatal(errMsg, err, &logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !common.NoSchemaValidation {
|
|
||||||
if err := Validate(data); err != nil {
|
|
||||||
E.LogFatal(errMsg, err, &logger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model := types.DefaultConfig()
|
model := types.DefaultConfig()
|
||||||
if err := E.From(yaml.Unmarshal(data, model)); err != nil {
|
if err := utils.DeserializeYAML(data, model); err != nil {
|
||||||
E.LogFatal(errMsg, err, &logger)
|
E.LogFatal(errMsg, err, &logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +170,7 @@ func (cfg *Config) load() E.Error {
|
||||||
errs.Add(entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
errs.Add(entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
||||||
errs.Add(entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
errs.Add(entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
||||||
errs.Add(cfg.initNotification(model.Providers.Notification))
|
errs.Add(cfg.initNotification(model.Providers.Notification))
|
||||||
errs.Add(cfg.initAutoCert(&model.AutoCert))
|
errs.Add(cfg.initAutoCert(model.AutoCert))
|
||||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||||
|
|
||||||
cfg.value = model
|
cfg.value = model
|
||||||
|
|
|
@ -2,13 +2,13 @@ package types
|
||||||
|
|
||||||
type (
|
type (
|
||||||
AutoCertConfig struct {
|
AutoCertConfig struct {
|
||||||
Email string `json:"email,omitempty" yaml:"email"`
|
Email string `json:"email,omitempty" validate:"email"`
|
||||||
Domains []string `json:"domains,omitempty" yaml:",flow"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
CertPath string `json:"cert_path,omitempty" yaml:"cert_path"`
|
CertPath string `json:"cert_path,omitempty" validate:"omitempty,filepath"`
|
||||||
KeyPath string `json:"key_path,omitempty" yaml:"key_path"`
|
KeyPath string `json:"key_path,omitempty" validate:"omitempty,filepath"`
|
||||||
ACMEKeyPath string `json:"acme_key_path,omitempty" yaml:"acme_key_path"`
|
ACMEKeyPath string `json:"acme_key_path,omitempty" validate:"omitempty,filepath"`
|
||||||
Provider string `json:"provider,omitempty" yaml:"provider"`
|
Provider string `json:"provider,omitempty"`
|
||||||
Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"`
|
Options AutocertProviderOpt `json:"options,omitempty"`
|
||||||
}
|
}
|
||||||
AutocertProviderOpt map[string]any
|
AutocertProviderOpt map[string]any
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,21 +4,21 @@ import "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
AutoCert *AutoCertConfig `json:"autocert"`
|
||||||
Entrypoint Entrypoint `json:"entrypoint" yaml:",flow"`
|
Entrypoint Entrypoint `json:"entrypoint"`
|
||||||
Providers Providers `json:"providers" yaml:",flow"`
|
Providers Providers `json:"providers"`
|
||||||
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
|
MatchDomains []string `json:"match_domains" validate:"dive,fqdn"`
|
||||||
Homepage HomepageConfig `json:"homepage" yaml:"homepage"`
|
Homepage HomepageConfig `json:"homepage"`
|
||||||
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
|
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
||||||
}
|
}
|
||||||
Providers struct {
|
Providers struct {
|
||||||
Files []string `json:"include" yaml:"include"`
|
Files []string `json:"include" validate:"dive,filepath"`
|
||||||
Docker map[string]string `json:"docker" yaml:"docker"`
|
Docker map[string]string `json:"docker" validate:"dive,unix_addr|url"`
|
||||||
Notification []NotificationConfig `json:"notification" yaml:"notification"`
|
Notification []NotificationConfig `json:"notification"`
|
||||||
}
|
}
|
||||||
Entrypoint struct {
|
Entrypoint struct {
|
||||||
Middlewares []map[string]any `json:"middlewares" yaml:"middlewares"`
|
Middlewares []map[string]any `json:"middlewares"`
|
||||||
AccessLog *accesslog.Config `json:"access_log" yaml:"access_log"`
|
AccessLog *accesslog.Config `json:"access_log"`
|
||||||
}
|
}
|
||||||
NotificationConfig map[string]any
|
NotificationConfig map[string]any
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
type HomepageConfig struct {
|
type HomepageConfig struct {
|
||||||
UseDefaultCategories bool `json:"use_default_categories" yaml:"use_default_categories"`
|
UseDefaultCategories bool `json:"use_default_categories"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,29 +15,29 @@ type (
|
||||||
Container struct {
|
Container struct {
|
||||||
_ U.NoCopy
|
_ U.NoCopy
|
||||||
|
|
||||||
DockerHost string `json:"docker_host" yaml:"-"`
|
DockerHost string `json:"docker_host"`
|
||||||
ContainerName string `json:"container_name" yaml:"-"`
|
ContainerName string `json:"container_name"`
|
||||||
ContainerID string `json:"container_id" yaml:"-"`
|
ContainerID string `json:"container_id"`
|
||||||
ImageName string `json:"image_name" yaml:"-"`
|
ImageName string `json:"image_name"`
|
||||||
|
|
||||||
Labels map[string]string `json:"-" yaml:"-"`
|
Labels map[string]string `json:"-"`
|
||||||
|
|
||||||
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
|
PublicPortMapping PortMapping `json:"public_ports"` // non-zero publicPort:types.Port
|
||||||
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
|
PrivatePortMapping PortMapping `json:"private_ports"` // privatePort:types.Port
|
||||||
PublicIP string `json:"public_ip" yaml:"-"`
|
PublicIP string `json:"public_ip"`
|
||||||
PrivateIP string `json:"private_ip" yaml:"-"`
|
PrivateIP string `json:"private_ip"`
|
||||||
NetworkMode string `json:"network_mode" yaml:"-"`
|
NetworkMode string `json:"network_mode"`
|
||||||
|
|
||||||
Aliases []string `json:"aliases" yaml:"-"`
|
Aliases []string `json:"aliases"`
|
||||||
IsExcluded bool `json:"is_excluded" yaml:"-"`
|
IsExcluded bool `json:"is_excluded"`
|
||||||
IsExplicit bool `json:"is_explicit" yaml:"-"`
|
IsExplicit bool `json:"is_explicit"`
|
||||||
IsDatabase bool `json:"is_database" yaml:"-"`
|
IsDatabase bool `json:"is_database"`
|
||||||
IdleTimeout string `json:"idle_timeout,omitempty" yaml:"-"`
|
IdleTimeout string `json:"idle_timeout,omitempty"`
|
||||||
WakeTimeout string `json:"wake_timeout,omitempty" yaml:"-"`
|
WakeTimeout string `json:"wake_timeout,omitempty"`
|
||||||
StopMethod string `json:"stop_method,omitempty" yaml:"-"`
|
StopMethod string `json:"stop_method,omitempty"`
|
||||||
StopTimeout string `json:"stop_timeout,omitempty" yaml:"-"` // stop_method = "stop" only
|
StopTimeout string `json:"stop_timeout,omitempty"` // stop_method = "stop" only
|
||||||
StopSignal string `json:"stop_signal,omitempty" yaml:"-"` // stop_method = "stop" | "kill" only
|
StopSignal string `json:"stop_signal,omitempty"` // stop_method = "stop" | "kill" only
|
||||||
Running bool `json:"running" yaml:"-"`
|
Running bool `json:"running"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -6,16 +6,16 @@ type (
|
||||||
Category []*Item
|
Category []*Item
|
||||||
|
|
||||||
Item struct {
|
Item struct {
|
||||||
Show bool `json:"show" yaml:"show"`
|
Show bool `json:"show"`
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name"`
|
||||||
Icon string `json:"icon" yaml:"icon"`
|
Icon string `json:"icon"`
|
||||||
URL string `json:"url" yaml:"url"` // alias + domain
|
URL string `json:"url"` // alias + domain
|
||||||
Category string `json:"category" yaml:"category"`
|
Category string `json:"category"`
|
||||||
Description string `json:"description" yaml:"description" aliases:"desc"`
|
Description string `json:"description" aliases:"desc"`
|
||||||
WidgetConfig map[string]any `json:"widget_config" yaml:",flow" aliases:"widget"`
|
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
|
||||||
|
|
||||||
SourceType string `json:"source_type" yaml:"-"`
|
SourceType string `json:"source_type"`
|
||||||
AltURL string `json:"alt_url" yaml:"-"` // original proxy target
|
AltURL string `json:"alt_url"` // original proxy target
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package accesslog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -37,7 +38,7 @@ const logTimeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||||
func NewFileAccessLogger(parent *task.Task, cfg *Config) (*AccessLogger, error) {
|
func NewFileAccessLogger(parent *task.Task, cfg *Config) (*AccessLogger, error) {
|
||||||
f, err := os.OpenFile(cfg.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
f, err := os.OpenFile(cfg.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("access log open error: %w", err)
|
||||||
}
|
}
|
||||||
return NewAccessLogger(parent, f, cfg), nil
|
return NewAccessLogger(parent, f, cfg), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package accesslog_test
|
package accesslog_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -13,25 +14,11 @@ import (
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testWritter struct {
|
|
||||||
line string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *testWritter) Write(p []byte) (n int, err error) {
|
|
||||||
w.line = string(p)
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *testWritter) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var tw testWritter
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
remote = "192.168.1.1"
|
remote = "192.168.1.1"
|
||||||
u = "http://example.com/?bar=baz&foo=bar"
|
host = "example.com"
|
||||||
uRedacted = "http://example.com/?bar=" + RedactedValue + "&foo=" + RedactedValue
|
uri = "/?bar=baz&foo=bar"
|
||||||
|
uriRedacted = "/?bar=" + RedactedValue + "&foo=" + RedactedValue
|
||||||
referer = "https://www.google.com/"
|
referer = "https://www.google.com/"
|
||||||
proto = "HTTP/1.1"
|
proto = "HTTP/1.1"
|
||||||
ua = "Go-http-client/1.1"
|
ua = "Go-http-client/1.1"
|
||||||
|
@ -41,7 +28,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testURL = E.Must(url.Parse(u))
|
testURL = E.Must(url.Parse("http://" + host + uri))
|
||||||
req = &http.Request{
|
req = &http.Request{
|
||||||
RemoteAddr: remote,
|
RemoteAddr: remote,
|
||||||
Method: method,
|
Method: method,
|
||||||
|
@ -62,17 +49,21 @@ var (
|
||||||
ContentLength: contentLength,
|
ContentLength: contentLength,
|
||||||
Header: http.Header{"Content-Type": []string{"text/plain"}},
|
Header: http.Header{"Content-Type": []string{"text/plain"}},
|
||||||
}
|
}
|
||||||
task = taskPkg.GlobalTask("test logger")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func fmtLog(cfg *Config) string {
|
||||||
|
var line bytes.Buffer
|
||||||
|
logger := NewAccessLogger(taskPkg.GlobalTask("test logger"), nil, cfg)
|
||||||
|
logger.Format(&line, req, resp)
|
||||||
|
return line.String()
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccessLoggerCommon(t *testing.T) {
|
func TestAccessLoggerCommon(t *testing.T) {
|
||||||
config := DefaultConfig
|
config := DefaultConfig
|
||||||
config.Format = FormatCommon
|
config.Format = FormatCommon
|
||||||
logger := NewAccessLogger(task, &tw, &config)
|
ExpectEqual(t, fmtLog(&config),
|
||||||
logger.Log(req, resp)
|
fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d",
|
||||||
ExpectEqual(t, tw.line,
|
host, remote, TestTimeNow, method, uri, proto, status, contentLength,
|
||||||
fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d\n",
|
|
||||||
remote, TestTimeNow, method, u, proto, status, contentLength,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -80,11 +71,9 @@ func TestAccessLoggerCommon(t *testing.T) {
|
||||||
func TestAccessLoggerCombined(t *testing.T) {
|
func TestAccessLoggerCombined(t *testing.T) {
|
||||||
config := DefaultConfig
|
config := DefaultConfig
|
||||||
config.Format = FormatCombined
|
config.Format = FormatCombined
|
||||||
logger := NewAccessLogger(task, &tw, &config)
|
ExpectEqual(t, fmtLog(&config),
|
||||||
logger.Log(req, resp)
|
fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"",
|
||||||
ExpectEqual(t, tw.line,
|
host, remote, TestTimeNow, method, uri, proto, status, contentLength, referer, ua,
|
||||||
fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"\n",
|
|
||||||
remote, TestTimeNow, method, u, proto, status, contentLength, referer, ua,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -93,11 +82,9 @@ func TestAccessLoggerRedactQuery(t *testing.T) {
|
||||||
config := DefaultConfig
|
config := DefaultConfig
|
||||||
config.Format = FormatCommon
|
config.Format = FormatCommon
|
||||||
config.Fields.Query.DefaultMode = FieldModeRedact
|
config.Fields.Query.DefaultMode = FieldModeRedact
|
||||||
logger := NewAccessLogger(task, &tw, &config)
|
ExpectEqual(t, fmtLog(&config),
|
||||||
logger.Log(req, resp)
|
fmt.Sprintf("%s %s - - [%s] \"%s %s %s\" %d %d",
|
||||||
ExpectEqual(t, tw.line,
|
host, remote, TestTimeNow, method, uriRedacted, proto, status, contentLength,
|
||||||
fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d\n",
|
|
||||||
remote, TestTimeNow, method, uRedacted, proto, status, contentLength,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -105,10 +92,8 @@ func TestAccessLoggerRedactQuery(t *testing.T) {
|
||||||
func getJSONEntry(t *testing.T, config *Config) JSONLogEntry {
|
func getJSONEntry(t *testing.T, config *Config) JSONLogEntry {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
config.Format = FormatJSON
|
config.Format = FormatJSON
|
||||||
logger := NewAccessLogger(task, &tw, config)
|
|
||||||
logger.Log(req, resp)
|
|
||||||
var entry JSONLogEntry
|
var entry JSONLogEntry
|
||||||
err := json.Unmarshal([]byte(tw.line), &entry)
|
err := json.Unmarshal([]byte(fmtLog(config)), &entry)
|
||||||
ExpectNoError(t, err)
|
ExpectNoError(t, err)
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ func TestNewConfig(t *testing.T) {
|
||||||
labels := map[string]string{
|
labels := map[string]string{
|
||||||
"proxy.buffer_size": "10",
|
"proxy.buffer_size": "10",
|
||||||
"proxy.format": "combined",
|
"proxy.format": "combined",
|
||||||
"proxy.file_path": "/tmp/access.log",
|
"proxy.path": "/tmp/access.log",
|
||||||
"proxy.filters.status_codes.values": "200-299",
|
"proxy.filters.status_codes.values": "200-299",
|
||||||
"proxy.filters.method.values": "GET, POST",
|
"proxy.filters.method.values": "GET, POST",
|
||||||
"proxy.filters.headers.values": "foo=bar, baz",
|
"proxy.filters.headers.values": "foo=bar, baz",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Link string `json:"link" yaml:"link"`
|
Link string `json:"link"`
|
||||||
Mode Mode `json:"mode" yaml:"mode"`
|
Mode Mode `json:"mode"`
|
||||||
Weight Weight `json:"weight" yaml:"weight"`
|
Weight Weight `json:"weight"`
|
||||||
Options map[string]any `json:"options,omitempty" yaml:"options,omitempty"`
|
Options map[string]any `json:"options,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
W "github.com/yusing/go-proxy/internal/watcher"
|
W "github.com/yusing/go-proxy/internal/watcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,8 +31,9 @@ func FileProviderImpl(filename string) (ProviderImpl, error) {
|
||||||
return impl, nil
|
return impl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Validate(data []byte) E.Error {
|
func Validate(data []byte) (err E.Error) {
|
||||||
return U.ValidateYaml(U.GetSchema(common.FileProviderSchemaPath), data)
|
_, err = utils.DeserializeYAMLMap[*route.RawEntry](data)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FileProvider) String() string {
|
func (p *FileProvider) String() string {
|
||||||
|
@ -45,22 +46,17 @@ func (p *FileProvider) Logger() *zerolog.Logger {
|
||||||
|
|
||||||
func (p *FileProvider) loadRoutesImpl() (route.Routes, E.Error) {
|
func (p *FileProvider) loadRoutesImpl() (route.Routes, E.Error) {
|
||||||
routes := route.NewRoutes()
|
routes := route.NewRoutes()
|
||||||
entries := route.NewProxyEntries()
|
|
||||||
|
|
||||||
data, err := os.ReadFile(p.path)
|
data, err := os.ReadFile(p.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return routes, E.From(err)
|
return routes, E.From(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := entries.UnmarshalFromYAML(data); err != nil {
|
entries, err := utils.DeserializeYAMLMap[*route.RawEntry](data)
|
||||||
return routes, E.From(err)
|
if err == nil {
|
||||||
|
return route.FromEntries(entries)
|
||||||
}
|
}
|
||||||
|
return routes, E.From(err)
|
||||||
if err := Validate(data); err != nil {
|
|
||||||
E.LogWarn("validation failure", err.Subject(p.fileName))
|
|
||||||
}
|
|
||||||
|
|
||||||
return route.FromEntries(entries)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FileProvider) NewWatcher() W.Watcher {
|
func (p *FileProvider) NewWatcher() W.Watcher {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: support stream load balance
|
// TODO: support stream load balance.
|
||||||
type StreamRoute struct {
|
type StreamRoute struct {
|
||||||
*entry.StreamEntry
|
*entry.StreamEntry
|
||||||
|
|
||||||
|
|
|
@ -24,20 +24,20 @@ type (
|
||||||
|
|
||||||
// raw entry object before validation
|
// raw entry object before validation
|
||||||
// loaded from docker labels or yaml file
|
// loaded from docker labels or yaml file
|
||||||
Alias string `json:"-" yaml:"-"`
|
Alias string `json:"-"`
|
||||||
Scheme string `json:"scheme,omitempty" yaml:"scheme"`
|
Scheme string `json:"scheme,omitempty"`
|
||||||
Host string `json:"host,omitempty" yaml:"host"`
|
Host string `json:"host,omitempty"`
|
||||||
Port string `json:"port,omitempty" yaml:"port"`
|
Port string `json:"port,omitempty"`
|
||||||
NoTLSVerify bool `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only
|
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
|
||||||
PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
|
PathPatterns []string `json:"path_patterns,omitempty"`
|
||||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"`
|
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||||
LoadBalance *loadbalance.Config `json:"load_balance,omitempty" yaml:"load_balance"`
|
LoadBalance *loadbalance.Config `json:"load_balance,omitempty"`
|
||||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
|
||||||
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`
|
Homepage *homepage.Item `json:"homepage,omitempty"`
|
||||||
AccessLog *accesslog.Config `json:"access_log,omitempty" yaml:"access_log"`
|
AccessLog *accesslog.Config `json:"access_log,omitempty"`
|
||||||
|
|
||||||
/* Docker only */
|
/* Docker only */
|
||||||
Container *docker.Container `json:"container,omitempty" yaml:"-"`
|
Container *docker.Container `json:"container,omitempty"`
|
||||||
|
|
||||||
finalized bool
|
finalized bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package functional
|
package functional
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
@ -195,31 +194,6 @@ func (m Map[KT, VT]) Has(k KT) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalFromYAML unmarshals a yaml byte slice into the map.
|
|
||||||
//
|
|
||||||
// It overwrites all existing key-value pairs in the map.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
//
|
|
||||||
// data: yaml byte slice to unmarshal
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
//
|
|
||||||
// error: if the unmarshaling fails
|
|
||||||
func (m Map[KT, VT]) UnmarshalFromYAML(data []byte) error {
|
|
||||||
if m.Size() != 0 {
|
|
||||||
return errors.New("cannot unmarshal into non-empty map")
|
|
||||||
}
|
|
||||||
tmp := make(map[KT]VT)
|
|
||||||
if err := yaml.Unmarshal(data, tmp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for k, v := range tmp {
|
|
||||||
m.Store(k, v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Map[KT, VT]) String() string {
|
func (m Map[KT, VT]) String() string {
|
||||||
tmp := make(map[KT]VT, m.Size())
|
tmp := make(map[KT]VT, m.Size())
|
||||||
m.RangeAll(func(k KT, v VT) {
|
m.RangeAll(func(k KT, v VT) {
|
||||||
|
|
|
@ -2,10 +2,8 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
@ -151,19 +149,3 @@ func Copy(dst *ContextWriter, src *ContextReader) (err error) {
|
||||||
func Copy2(ctx context.Context, dst io.Writer, src io.Reader) error {
|
func Copy2(ctx context.Context, dst io.Writer, src io.Reader) error {
|
||||||
return Copy(&ContextWriter{ctx: ctx, Writer: dst}, &ContextReader{ctx: ctx, Reader: src})
|
return Copy(&ContextWriter{ctx: ctx, Writer: dst}, &ContextReader{ctx: ctx, Reader: src})
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadJSON[T any](path string, pointer *T) error {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return json.Unmarshal(data, pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveJSON[T any](path string, pointer *T, perm os.FileMode) error {
|
|
||||||
data, err := json.Marshal(pointer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(path, data, perm)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/santhosh-tekuri/jsonschema"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
schemaCompiler = jsonschema.NewCompiler()
|
|
||||||
schemaStorage = make(map[string]*jsonschema.Schema)
|
|
||||||
schemaMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSchema(path string) *jsonschema.Schema {
|
|
||||||
if schema, ok := schemaStorage[path]; ok {
|
|
||||||
return schema
|
|
||||||
}
|
|
||||||
schemaMu.Lock()
|
|
||||||
defer schemaMu.Unlock()
|
|
||||||
if schema, ok := schemaStorage[path]; ok {
|
|
||||||
return schema
|
|
||||||
}
|
|
||||||
schema := schemaCompiler.MustCompile(path)
|
|
||||||
schemaStorage[path] = schema
|
|
||||||
return schema
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -12,8 +12,8 @@ import (
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/santhosh-tekuri/jsonschema"
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -30,36 +30,6 @@ var (
|
||||||
ErrUnknownField = E.New("unknown field")
|
ErrUnknownField = E.New("unknown field")
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.Error {
|
|
||||||
var i any
|
|
||||||
|
|
||||||
err := yaml.Unmarshal(data, &i)
|
|
||||||
if err != nil {
|
|
||||||
return E.From(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := json.Marshal(i)
|
|
||||||
if err != nil {
|
|
||||||
return E.From(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = schema.Validate(bytes.NewReader(m))
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var valErr *jsonschema.ValidationError
|
|
||||||
if !errors.As(err, &valErr) {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b := E.NewBuilder("yaml validation error")
|
|
||||||
for _, e := range valErr.Causes {
|
|
||||||
b.Adds(e.Message)
|
|
||||||
}
|
|
||||||
return b.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize converts the given data into a map[string]any representation.
|
// Serialize converts the given data into a map[string]any representation.
|
||||||
//
|
//
|
||||||
// It uses reflection to inspect the data type and handle different kinds of data.
|
// It uses reflection to inspect the data type and handle different kinds of data.
|
||||||
|
@ -482,10 +452,38 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E
|
||||||
return true, Convert(reflect.ValueOf(tmp), dst)
|
return true, Convert(reflect.ValueOf(tmp), dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeserializeJSON(j map[string]string, target any) error {
|
func DeserializeYAML[T any](data []byte, target T) E.Error {
|
||||||
data, err := json.Marshal(j)
|
m := make(map[string]any)
|
||||||
|
if err := yaml.Unmarshal(data, m); err != nil {
|
||||||
|
return E.From(err)
|
||||||
|
}
|
||||||
|
return Deserialize(m, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeserializeYAMLMap[V any](data []byte) (_ functional.Map[string, V], err E.Error) {
|
||||||
|
m := make(map[string]any)
|
||||||
|
if err = E.From(yaml.Unmarshal(data, m)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m2 := make(map[string]V, len(m))
|
||||||
|
if err = Deserialize(m, m2); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return functional.NewMapFrom(m2), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadJSON[T any](path string, dst *T) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return json.Unmarshal(data, target)
|
return json.Unmarshal(data, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveJSON[T any](path string, src *T, perm os.FileMode) error {
|
||||||
|
data, err := json.Marshal(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, perm)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthCheckConfig struct {
|
type HealthCheckConfig struct {
|
||||||
Disable bool `json:"disable,omitempty" yaml:"disable" aliases:"disabled"`
|
Disable bool `json:"disable,omitempty" aliases:"disabled"`
|
||||||
Path string `json:"path,omitempty" yaml:"path"`
|
Path string `json:"path,omitempty" validate:"omitempty,uri,startswith=/"`
|
||||||
UseGet bool `json:"use_get,omitempty" yaml:"use_get"`
|
UseGet bool `json:"use_get,omitempty"`
|
||||||
Interval time.Duration `json:"interval" yaml:"interval"`
|
Interval time.Duration `json:"interval" validate:"omitempty,min=1s"`
|
||||||
Timeout time.Duration `json:"timeout" yaml:"timeout"`
|
Timeout time.Duration `json:"timeout" validate:"omitempty,min=1s"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue