From 21fcceb391658b951917124587665b389ed53711 Mon Sep 17 00:00:00 2001 From: yusing Date: Tue, 17 Sep 2024 12:06:58 +0800 Subject: [PATCH] v0.5.0-rc4: initial support for ovh, provider generator implementation update, replaced all interface{} to any --- README.md | 2 + docs/dns_providers.md | 33 +++++++++-- go.work | 2 +- schema/config.schema.json | 78 ++++++++++++++++++++++++- src/api/v1/stats.go | 2 +- src/autocert/constants.go | 3 + src/autocert/provider.go | 12 +--- src/autocert/provider_test/ovh_test.go | 49 ++++++++++++++++ src/config/config.go | 10 ++-- src/docker/homepage_label.go | 2 +- src/docker/label_parser_test.go | 14 ++--- src/go.mod | 7 ++- src/go.sum | 10 ++++ src/models/autocert_config.go | 2 +- src/route/stream_route.go | 2 +- src/route/tcp_route.go | 4 +- src/route/udp_route.go | 4 +- src/utils/functional/functional.go | 14 ++--- src/utils/functional/map.go | 8 +-- src/utils/serialization.go | 81 +++++++++++++++++++++++--- src/utils/testing.go | 11 +++- 21 files changed, 286 insertions(+), 64 deletions(-) create mode 100644 src/autocert/provider_test/ovh_test.go diff --git a/README.md b/README.md index 93c3f35..9757d0d 100755 --- a/README.md +++ b/README.md @@ -114,6 +114,8 @@ See [providers.example.yml](providers.example.yml) for examples - Cert "renewal" is actually obtaining a new cert instead of renewing the existing one +- `autocert` config is not hot-reloadable + [🔼Back to top](#table-of-content) ## Build it yourself diff --git a/docs/dns_providers.md b/docs/dns_providers.md index 69343fb..57ee195 100644 --- a/docs/dns_providers.md +++ b/docs/dns_providers.md @@ -1,11 +1,13 @@ # Supported DNS Providers -- [Cloudflare](#cloudflare) -- [CloudDNS](#clouddns) -- [DuckDNS](#duckdns) -- [Implement other DNS providers](#implement-other-dns-providers) - + +- [Supported DNS Providers](#supported-dns-providers) + - [Cloudflare](#cloudflare) + - [CloudDNS](#clouddns) + - [DuckDNS](#duckdns) + - [OVHCloud](#ovhcloud) + - [Implement other DNS providers](#implement-other-dns-providers) ## Cloudflare @@ -23,10 +25,29 @@ Follow [this guide](https://cloudkul.com/blog/automcatic-renew-and-generate-ssl- ## DuckDNS -`token`: DuckDNS Token +- `token`: DuckDNS Token Tested by [earvingad](https://github.com/earvingad) +## OVHCloud + +_Note, `application_key` and `oauth2_config` **CANNOT** be used together_ + +- `api_endpoint`: Endpoint URL, or one of + - `ovh-eu`, + - `ovh-ca`, + - `ovh-us`, + - `kimsufi-eu`, + - `kimsufi-ca`, + - `soyoustart-eu`, + - `soyoustart-ca` +- `application_secret` +- `application_key` +- `consumer_key` +- `oauth2_config`: Client ID and Client Secret + - `client_id` + - `client_secret` + ## Implement other DNS providers See [add_dns_provider.md](docs/add_dns_provider.md) diff --git a/go.work b/go.work index 1aa0ac6..aad4eaa 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.22 +go 1.22.0 toolchain go1.23.1 diff --git a/schema/config.schema.json b/schema/config.schema.json index e9a865b..96f9115 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -37,7 +37,7 @@ "title": "DNS Challenge Provider", "default": "local", "type": "string", - "enum": ["local", "cloudflare", "clouddns", "duckdns"] + "enum": ["local", "cloudflare", "clouddns", "duckdns", "ovh"] }, "options": { "title": "Provider specific options", @@ -135,6 +135,82 @@ } } } + }, + { + "if": { + "properties": { + "provider": { + "const": "ovh" + } + } + }, + "then": { + "properties": { + "options": { + "required": ["application_secret", "consumer_key"], + "additionalProperties": false, + "oneOf": [ + { + "required": ["application_key"] + }, + { + "required": ["oauth2_config"] + } + ], + "properties": { + "api_endpoint": { + "description": "OVH API endpoint", + "default": "ovh-eu", + "anyOf": [ + { + "enum": [ + "ovh-eu", + "ovh-ca", + "ovh-us", + "kimsufi-eu", + "kimsufi-ca", + "soyoustart-eu", + "soyoustart-ca" + ] + }, + { + "type": "string", + "format": "uri" + } + ] + }, + "application_secret": { + "description": "OVH Application Secret", + "type": "string" + }, + "consumer_key": { + "description": "OVH Consumer Key", + "type": "string" + }, + "application_key": { + "description": "OVH Application Key", + "type": "string" + }, + "oauth2_config": { + "description": "OVH OAuth2 config", + "type": "object", + "additionalProperties": false, + "properties": { + "client_id": { + "description": "OVH Client ID", + "type": "string" + }, + "client_secret": { + "description": "OVH Client Secret", + "type": "string" + } + }, + "required": ["client_id", "client_secret"] + } + } + } + } + } } ] }, diff --git a/src/api/v1/stats.go b/src/api/v1/stats.go index 794f1ae..154fe01 100644 --- a/src/api/v1/stats.go +++ b/src/api/v1/stats.go @@ -10,7 +10,7 @@ import ( ) func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) { - stats := map[string]interface{}{ + stats := map[string]any{ "proxies": cfg.Statistics(), "uptime": utils.FormatDuration(server.GetProxyServer().Uptime()), } diff --git a/src/autocert/constants.go b/src/autocert/constants.go index 30fd702..8dadbd9 100644 --- a/src/autocert/constants.go +++ b/src/autocert/constants.go @@ -4,6 +4,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/clouddns" "github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/go-acme/lego/v4/providers/dns/duckdns" + "github.com/go-acme/lego/v4/providers/dns/ovh" "github.com/sirupsen/logrus" ) @@ -19,6 +20,7 @@ const ( ProviderCloudflare = "cloudflare" ProviderClouddns = "clouddns" ProviderDuckdns = "duckdns" + ProviderOVH = "ovh" ) var providersGenMap = map[string]ProviderGenerator{ @@ -26,6 +28,7 @@ var providersGenMap = map[string]ProviderGenerator{ ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig), ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig), ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig), + ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig), } var logger = logrus.WithField("module", "autocert") diff --git a/src/autocert/provider.go b/src/autocert/provider.go index f0e7076..ab0bc6b 100644 --- a/src/autocert/provider.go +++ b/src/autocert/provider.go @@ -272,23 +272,13 @@ func getCertExpiries(cert *tls.Certificate) (CertExpiries, E.NestedError) { return r, E.Nil() } -func setOptions[T interface{}](cfg *T, opt M.AutocertProviderOpt) E.NestedError { - for k, v := range opt { - err := U.SetFieldFromSnake(cfg, k, v) - if err.HasError() { - return E.Failure("set autocert option").Subject(k).With(err) - } - } - return E.Nil() -} - func providerGenerator[CT any, PT challenge.Provider]( defaultCfg func() *CT, newProvider func(*CT) (PT, error), ) ProviderGenerator { return func(opt M.AutocertProviderOpt) (challenge.Provider, E.NestedError) { cfg := defaultCfg() - err := setOptions(cfg, opt) + err := U.Deserialize(opt, cfg) if err.HasError() { return nil, err } diff --git a/src/autocert/provider_test/ovh_test.go b/src/autocert/provider_test/ovh_test.go new file mode 100644 index 0000000..e5e40ad --- /dev/null +++ b/src/autocert/provider_test/ovh_test.go @@ -0,0 +1,49 @@ +package provider_test + +import ( + "testing" + + "github.com/go-acme/lego/v4/providers/dns/ovh" + . "github.com/yusing/go-proxy/utils" + "gopkg.in/yaml.v3" +) + +// type Config struct { +// APIEndpoint string + +// ApplicationKey string +// ApplicationSecret string +// ConsumerKey string + +// OAuth2Config *OAuth2Config + +// PropagationTimeout time.Duration +// PollingInterval time.Duration +// TTL int +// HTTPClient *http.Client +// } + +func TestOVH(t *testing.T) { + cfg := &ovh.Config{} + testYaml := ` +api_endpoint: https://eu.api.ovh.com +application_key: +application_secret: +consumer_key: +oauth2_config: + client_id: + client_secret: +` + cfgExpected := &ovh.Config{ + APIEndpoint: "https://eu.api.ovh.com", + ApplicationKey: "", + ApplicationSecret: "", + ConsumerKey: "", + OAuth2Config: &ovh.OAuth2Config{ClientID: "", ClientSecret: ""}, + } + testYaml = testYaml[1:] // remove first \n + opt := make(map[string]any) + ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), opt)) + ExpectNoError(t, Deserialize(opt, cfg)) + ExpectEqual(t, cfg, cfgExpected) +} diff --git a/src/config/config.go b/src/config/config.go index 3b28f41..624f277 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -95,7 +95,7 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject { prName := p.GetName() p.GetCurrentRoutes().EachKV(func(a string, r R.Route) { obj, err := U.Serialize(r) - if err != nil { + if err.HasError() { cfg.l.Error(err) return } @@ -114,13 +114,13 @@ func (cfg *Config) RoutesByAlias() map[string]U.SerializedObject { return routes } -func (cfg *Config) Statistics() map[string]interface{} { +func (cfg *Config) Statistics() map[string]any { nTotalStreams := 0 nTotalRPs := 0 - providerStats := make(map[string]interface{}) + providerStats := make(map[string]any) cfg.proxyProviders.Each(func(p *PR.Provider) { - stats := make(map[string]interface{}) + stats := make(map[string]any) nStreams := 0 nRPs := 0 p.GetCurrentRoutes().EachKV(func(a string, r R.Route) { @@ -141,7 +141,7 @@ func (cfg *Config) Statistics() map[string]interface{} { providerStats[p.GetName()] = stats }) - return map[string]interface{}{ + return map[string]any{ "num_total_streams": nTotalStreams, "num_total_reverse_proxies": nTotalRPs, "providers": providerStats, diff --git a/src/docker/homepage_label.go b/src/docker/homepage_label.go index 3b1909e..4ad7146 100644 --- a/src/docker/homepage_label.go +++ b/src/docker/homepage_label.go @@ -9,7 +9,7 @@ type ( Icon string Category string Description string - WidgetConfig map[string]interface{} + WidgetConfig map[string]any } ) diff --git a/src/docker/label_parser_test.go b/src/docker/label_parser_test.go index 7dbc281..7aac189 100644 --- a/src/docker/label_parser_test.go +++ b/src/docker/label_parser_test.go @@ -19,7 +19,7 @@ func TestHomePageLabel(t *testing.T) { field := "ip" v := "bar" pl, err := ParseLabel(makeLabel(NSHomePage, alias, field), v) - ExpectErrNil(t, err) + ExpectNoError(t, err) if pl.Target != alias { t.Errorf("Expected alias=%s, got %s", alias, pl.Target) } @@ -34,7 +34,7 @@ func TestHomePageLabel(t *testing.T) { func TestStringProxyLabel(t *testing.T) { v := "bar" pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v) - ExpectErrNil(t, err) + ExpectNoError(t, err) ExpectEqual(t, pl.Value, v) } @@ -52,7 +52,7 @@ func TestBoolProxyLabelValid(t *testing.T) { for k, v := range tests { pl, err := ParseLabel(makeLabel(NSProxy, "foo", "no_tls_verify"), k) - ExpectErrNil(t, err) + ExpectNoError(t, err) ExpectEqual(t, pl.Value, v) } } @@ -78,7 +78,7 @@ X-Custom-Header2: boo` } pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v) - ExpectErrNil(t, err) + ExpectNoError(t, err) hGot := ExpectType[map[string]string](t, pl.Value) if hGot != nil && !reflect.DeepEqual(h, hGot) { t.Errorf("Expected %v, got %v", h, hGot) @@ -109,7 +109,7 @@ func TestHideHeadersProxyLabel(t *testing.T) { ` v = strings.TrimPrefix(v, "\n") pl, err := ParseLabel(makeLabel(NSProxy, "foo", "hide_headers"), v) - ExpectErrNil(t, err) + ExpectNoError(t, err) sGot := ExpectType[[]string](t, pl.Value) sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"} if sGot != nil { @@ -120,7 +120,7 @@ func TestHideHeadersProxyLabel(t *testing.T) { func TestCommaSepProxyLabelSingle(t *testing.T) { v := "a" pl, err := ParseLabel("proxy.aliases", v) - ExpectErrNil(t, err) + ExpectNoError(t, err) sGot := ExpectType[[]string](t, pl.Value) sWant := []string{"a"} if sGot != nil { @@ -132,7 +132,7 @@ func TestCommaSepProxyLabelSingle(t *testing.T) { func TestCommaSepProxyLabelMulti(t *testing.T) { v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3" pl, err := ParseLabel("proxy.aliases", v) - ExpectErrNil(t, err) + ExpectNoError(t, err) sGot := ExpectType[[]string](t, pl.Value) sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"} if sGot != nil { diff --git a/src/go.mod b/src/go.mod index 200d596..25ea342 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,8 +1,6 @@ module github.com/yusing/go-proxy -go 1.22 - -toolchain go1.23.1 +go 1.22.0 require ( github.com/docker/cli v27.2.1+incompatible @@ -36,6 +34,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/ovh/go-ovh v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect @@ -45,10 +44,12 @@ require ( go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/mod v0.21.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.25.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index fff00c8..4fbab95 100644 --- a/src/go.sum +++ b/src/go.sum @@ -45,12 +45,16 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -63,6 +67,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI= +github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -110,6 +116,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -149,6 +157,8 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/models/autocert_config.go b/src/models/autocert_config.go index 928f484..042fc80 100644 --- a/src/models/autocert_config.go +++ b/src/models/autocert_config.go @@ -9,5 +9,5 @@ type ( Provider string `json:"provider"` Options AutocertProviderOpt `yaml:",flow" json:"options"` } - AutocertProviderOpt map[string]string + AutocertProviderOpt map[string]any ) diff --git a/src/route/stream_route.go b/src/route/stream_route.go index 7078cb5..f11abdb 100755 --- a/src/route/stream_route.go +++ b/src/route/stream_route.go @@ -37,7 +37,6 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) { base := &StreamRoute{ StreamEntry: entry, wg: sync.WaitGroup{}, - stopCh: make(chan struct{}, 1), connCh: make(chan any), } if entry.Scheme.ListeningScheme.IsTCP() { @@ -57,6 +56,7 @@ func (r *StreamRoute) Start() E.NestedError { if r.started.Load() { return E.Invalid("state", "already started") } + r.stopCh = make(chan struct{}, 1) r.wg.Wait() if err := r.Setup(); err != nil { return E.Failure("setup").With(err) diff --git a/src/route/tcp_route.go b/src/route/tcp_route.go index 8bcfba8..6e80159 100755 --- a/src/route/tcp_route.go +++ b/src/route/tcp_route.go @@ -38,11 +38,11 @@ func (route *TCPRoute) Setup() error { return nil } -func (route *TCPRoute) Accept() (interface{}, error) { +func (route *TCPRoute) Accept() (any, error) { return route.listener.Accept() } -func (route *TCPRoute) Handle(c interface{}) error { +func (route *TCPRoute) Handle(c any) error { clientConn := c.(net.Conn) defer clientConn.Close() diff --git a/src/route/udp_route.go b/src/route/udp_route.go index 1b69a0a..8767712 100755 --- a/src/route/udp_route.go +++ b/src/route/udp_route.go @@ -55,7 +55,7 @@ func (route *UDPRoute) Setup() error { return nil } -func (route *UDPRoute) Accept() (interface{}, error) { +func (route *UDPRoute) Accept() (any, error) { in := route.listeningConn buffer := make([]byte, udpBufferSize) @@ -103,7 +103,7 @@ func (route *UDPRoute) Accept() (interface{}, error) { return conn, err } -func (route *UDPRoute) Handle(c interface{}) error { +func (route *UDPRoute) Handle(c any) error { return c.(*UDPConn).Start() } diff --git a/src/utils/functional/functional.go b/src/utils/functional/functional.go index 254d8fb..e8bcf9c 100644 --- a/src/utils/functional/functional.go +++ b/src/utils/functional/functional.go @@ -2,25 +2,25 @@ package functional import "sync" -func ForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) { +func ForEachKey[K comparable, V any](obj map[K]V, do func(K)) { for k := range obj { do(k) } } -func ForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) { +func ForEachValue[K comparable, V any](obj map[K]V, do func(V)) { for _, v := range obj { do(v) } } -func ForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) { +func ForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) { for k, v := range obj { do(k, v) } } -func ParallelForEach[T interface{}](obj []T, do func(T)) { +func ParallelForEach[T any](obj []T, do func(T)) { var wg sync.WaitGroup wg.Add(len(obj)) for _, v := range obj { @@ -32,7 +32,7 @@ func ParallelForEach[T interface{}](obj []T, do func(T)) { wg.Wait() } -func ParallelForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) { +func ParallelForEachKey[K comparable, V any](obj map[K]V, do func(K)) { var wg sync.WaitGroup wg.Add(len(obj)) for k := range obj { @@ -44,7 +44,7 @@ func ParallelForEachKey[K comparable, V interface{}](obj map[K]V, do func(K)) { wg.Wait() } -func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) { +func ParallelForEachValue[K comparable, V any](obj map[K]V, do func(V)) { var wg sync.WaitGroup wg.Add(len(obj)) for _, v := range obj { @@ -56,7 +56,7 @@ func ParallelForEachValue[K comparable, V interface{}](obj map[K]V, do func(V)) wg.Wait() } -func ParallelForEachKV[K comparable, V interface{}](obj map[K]V, do func(K, V)) { +func ParallelForEachKV[K comparable, V any](obj map[K]V, do func(K, V)) { var wg sync.WaitGroup wg.Add(len(obj)) for k, v := range obj { diff --git a/src/utils/functional/map.go b/src/utils/functional/map.go index 007c13b..1832c88 100644 --- a/src/utils/functional/map.go +++ b/src/utils/functional/map.go @@ -9,7 +9,7 @@ import ( E "github.com/yusing/go-proxy/error" ) -type Map[KT comparable, VT interface{}] struct { +type Map[KT comparable, VT any] struct { m map[KT]VT defVals map[KT]VT sync.RWMutex @@ -22,7 +22,7 @@ type Map[KT comparable, VT interface{}] struct { // // Return: // - *Map[KT, VT]: a pointer to the newly created Map. -func NewMap[KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] { +func NewMap[KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] { return NewMapFrom(make(map[KT]VT), dv...) } @@ -36,7 +36,7 @@ func NewMap[KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] { // // Return: // - *Map[KT, VT]: a pointer to the newly created Map. -func NewMapOf[M Map[KT, VT], KT comparable, VT interface{}](dv ...map[KT]VT) *Map[KT, VT] { +func NewMapOf[M Map[KT, VT], KT comparable, VT any](dv ...map[KT]VT) *Map[KT, VT] { return NewMapFrom(make(map[KT]VT), dv...) } @@ -48,7 +48,7 @@ func NewMapOf[M Map[KT, VT], KT comparable, VT interface{}](dv ...map[KT]VT) *Ma // // Return: // - *Map[KT, VT]: a pointer to the newly created Map. -func NewMapFrom[KT comparable, VT interface{}](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] { +func NewMapFrom[KT comparable, VT any](from map[KT]VT, dv ...map[KT]VT) *Map[KT, VT] { if len(dv) > 0 { return &Map[KT, VT]{m: from, defVals: dv[0]} } diff --git a/src/utils/serialization.go b/src/utils/serialization.go index fd382db..f62f1bd 100644 --- a/src/utils/serialization.go +++ b/src/utils/serialization.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "reflect" + "strings" "github.com/santhosh-tekuri/jsonschema" E "github.com/yusing/go-proxy/error" @@ -12,7 +13,7 @@ import ( ) func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError { - var i interface{} + var i any err := yaml.Unmarshal(data, &i) if err != nil { @@ -55,7 +56,7 @@ func TryJsonStringify(o any) string { return string(b) } -// Serialize converts the given data into a map[string]interface{} 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. // For a struct, it extracts the fields using the json tag if present, or the field name if not. @@ -66,9 +67,9 @@ func TryJsonStringify(o any) string { // - data: The data to be converted into a map. // // Returns: -// - result: The resulting map[string]interface{} representation of the data. +// - result: The resulting map[string]any representation of the data. // - error: An error if the data type is unsupported or if there is an error during conversion. -func Serialize(data interface{}) (SerializedObject, error) { +func Serialize(data any) (SerializedObject, E.NestedError) { result := make(map[string]any) // Use reflection to inspect the data type @@ -76,7 +77,7 @@ func Serialize(data interface{}) (SerializedObject, error) { // Check if the value is valid if !value.IsValid() { - return nil, fmt.Errorf("invalid data") + return nil, E.Invalid("data", fmt.Sprintf("type: %T", data)) } // Dereference pointers if necessary @@ -107,7 +108,7 @@ func Serialize(data interface{}) (SerializedObject, error) { } else if field.Anonymous { // If the field is an embedded struct, add its fields to the result fieldMap, err := Serialize(value.Field(i).Interface()) - if err != nil { + if err.HasError() { return nil, err } for k, v := range fieldMap { @@ -118,10 +119,72 @@ func Serialize(data interface{}) (SerializedObject, error) { } } default: - return nil, fmt.Errorf("unsupported type: %s", value.Kind()) + // return nil, fmt.Errorf("unsupported type: %s", value.Kind()) + return nil, E.Unsupported("type", value.Kind()) } - return result, nil + return result, E.Nil() } -type SerializedObject map[string]any +func Deserialize(src map[string]any, target any) E.NestedError { + // convert data fields to lower no-snake + // convert target fields to lower + // then check if the field of data is in the target + mapping := make(map[string]string) + t := reflect.TypeOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + snakeCaseField := strings.ToLower(field.Name) + mapping[snakeCaseField] = field.Name + } + for k, v := range src { + kCleaned := toLowerNoSnake(k) + if fieldName, ok := mapping[kCleaned]; ok { + prop := reflect.ValueOf(target).Elem().FieldByName(fieldName) + propType := prop.Type() + isPtr := prop.Kind() == reflect.Ptr + if prop.CanSet() { + val := reflect.ValueOf(v) + vType := val.Type() + switch { + case isPtr && vType.ConvertibleTo(propType.Elem()): + ptr := reflect.New(propType.Elem()) + ptr.Elem().Set(val.Convert(propType.Elem())) + prop.Set(ptr) + case vType.ConvertibleTo(propType): + prop.Set(val.Convert(propType)) + case isPtr: + var vSerialized SerializedObject + vSerialized, ok = v.(SerializedObject) + if !ok { + if vType.ConvertibleTo(reflect.TypeFor[SerializedObject]()) { + vSerialized = val.Convert(reflect.TypeFor[SerializedObject]()).Interface().(SerializedObject) + } else { + return E.Failure(fmt.Sprintf("convert %s (%T) to %s", k, v, reflect.TypeFor[SerializedObject]())) + } + } + propNew := reflect.New(propType.Elem()) + err := Deserialize(vSerialized, propNew.Interface()) + if err.HasError() { + return E.Failure("set field").With(k).With(err) + } + prop.Set(propNew) + default: + return E.Unsupported("field", k).Extraf("type=%s", propType) + } + } else { + return E.Unsupported("field", k).Extraf("type=%s", propType) + } + } else { + return E.Failure("unknown field").With(k) + } + } + + return E.Nil() +} + +func toLowerNoSnake(s string) string { + return strings.ToLower(strings.ReplaceAll(s, "_", "")) +} + +type SerializedObject = map[string]any diff --git a/src/utils/testing.go b/src/utils/testing.go index c51a8ee..62c9387 100644 --- a/src/utils/testing.go +++ b/src/utils/testing.go @@ -7,9 +7,16 @@ import ( E "github.com/yusing/go-proxy/error" ) -func ExpectErrNil(t *testing.T, err E.NestedError) { +func ExpectNoError(t *testing.T, err error) { t.Helper() - if err.HasError() { + var noError bool + switch t := err.(type) { + case E.NestedError: + noError = t.NoError() + default: + noError = err == nil + } + if !noError { t.Errorf("expected err=nil, got %s", err.Error()) } }