diff --git a/README.md b/README.md index bcc5834..f271d61 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,50 @@ For example, you've bought a VPS from [Digital Ocean](https://www.digitalocean.com/). Then it might be a good idea to generate a secret for _digitalocean.com_ then. + +### Simple run mode + +mtg supports 2 modes: simple and normal. Simple mode allows starting +proxy with a small subset of configuration options you usually want to +modify. This is quite good for oneliners that you can copy-paste and do +not bother about external files whatsoever. + +Let's take a look: + +```console +Usage: mtg simple-run + +Run proxy without config file. + +Arguments: + A host:port to bind proxy to. + Proxy secret. + +Flags: + -h, --help Show context-sensitive help. + -v, --version Print version. + + -d, --debug Run in debug mode. + -c, --concurrency=8192 Max number of concurrent connection to proxy. + -b, --tcp-buffer="4KB" Size of TCP buffer to use. + -i, --prefer-ip="prefer-ipv6" IP preference. By default we prefer IPv6 with fallback to IPv4. + -p, --domain-fronting-port=443 A port to access for domain fronting. + -n, --doh-ip=9.9.9.9 IP address of DNS-over-HTTP to use. + -t, --timeout=10s Network timeout to use + -a, --antireplay-cache-size="1MB" A size of anti-replay cache to use. +``` + +So, if you want to startup a proxy with CLI only, you can do something like + +```console +$ mtg simple-run -n 1.1.1.1 -t 30s -a 512kib 127.0.0.1:3128 7hBO-dCS4EBzenlKbdLFxyNnb29nbGUuY29t +``` + +The rest of the configuration will be taken from default values. But +a simple run is fine if you do not have any special requirements or +granular tuning. If you want it, please checkout the configuration +files. + ### Prepare a configuration file Please checkout an example configuration file. All options except of diff --git a/internal/cli/access.go b/internal/cli/access.go index 15e6bfa..23a47ce 100644 --- a/internal/cli/access.go +++ b/internal/cli/access.go @@ -12,6 +12,10 @@ import ( "strconv" "strings" "sync" + + "github.com/9seconds/mtg/v2/internal/config" + "github.com/9seconds/mtg/v2/internal/utils" + "github.com/9seconds/mtg/v2/mtglib" ) type accessResponse struct { @@ -33,26 +37,27 @@ type accessResponseURLs struct { } type Access struct { - base - + ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` // nolint: lll PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` // nolint: lll Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` // nolint: lll Hex bool `kong:"help='Print secret in hex encoding.',short='x'"` } -func (c *Access) Run(cli *CLI, version string) error { - if err := c.ReadConfig(version); err != nil { +func (a *Access) Run(cli *CLI, version string) error { + conf, err := utils.ReadConfig(a.ConfigPath) + if err != nil { return fmt.Errorf("cannot init config: %w", err) } - return c.Execute(cli) -} - -func (c *Access) Execute(cli *CLI) error { resp := &accessResponse{} - resp.Secret.Base64 = c.Config.Secret.Base64() - resp.Secret.Hex = c.Config.Secret.Hex() + resp.Secret.Base64 = conf.Secret.Base64() + resp.Secret.Hex = conf.Secret.Hex() + + ntw, err := makeNetwork(conf, version) + if err != nil { + return fmt.Errorf("cannot init network: %w", err) + } wg := &sync.WaitGroup{} wg.Add(2) // nolint: gomnd @@ -60,31 +65,31 @@ func (c *Access) Execute(cli *CLI) error { go func() { defer wg.Done() - ip := cli.Access.PublicIPv4 + ip := a.PublicIPv4 if ip == nil { - ip = c.getIP("tcp4") + ip = a.getIP(ntw, "tcp4") } if ip != nil { ip = ip.To4() } - resp.IPv4 = c.makeURLs(ip, cli) + resp.IPv4 = a.makeURLs(conf, ip) }() go func() { defer wg.Done() - ip := cli.Access.PublicIPv6 + ip := a.PublicIPv6 if ip == nil { - ip = c.getIP("tcp6") + ip = a.getIP(ntw, "tcp6") } if ip != nil { ip = ip.To16() } - resp.IPv6 = c.makeURLs(ip, cli) + resp.IPv6 = a.makeURLs(conf, ip) }() wg.Wait() @@ -100,9 +105,9 @@ func (c *Access) Execute(cli *CLI) error { return nil } -func (c *Access) getIP(protocol string) net.IP { - client := c.Network.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) { - return c.Network.DialContext(ctx, protocol, address) // nolint: wrapcheck +func (a *Access) getIP(ntw mtglib.Network, protocol string) net.IP { + client := ntw.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) { + return ntw.DialContext(ctx, protocol, address) // nolint: wrapcheck }) req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx @@ -134,24 +139,24 @@ func (c *Access) getIP(protocol string) net.IP { return net.ParseIP(strings.TrimSpace(string(data))) } -func (c *Access) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs { +func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs { if ip == nil { return nil } - portNo := cli.Access.Port + portNo := a.Port if portNo == 0 { - portNo = c.Config.BindTo.PortValue(0) + portNo = conf.BindTo.Port } values := url.Values{} values.Set("server", ip.String()) values.Set("port", strconv.Itoa(int(portNo))) - if cli.Access.Hex { - values.Set("secret", c.Config.Secret.Hex()) + if a.Hex { + values.Set("secret", conf.Secret.Hex()) } else { - values.Set("secret", c.Config.Secret.Base64()) + values.Set("secret", conf.Secret.Base64()) } urlQuery := values.Encode() @@ -171,22 +176,8 @@ func (c *Access) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs { RawQuery: urlQuery, }).String(), } - rv.TgQrCode = c.makeQRCode(rv.TgURL) - rv.TmeQrCode = c.makeQRCode(rv.TmeURL) + rv.TgQrCode = utils.MakeQRCodeURL(rv.TgURL) + rv.TmeQrCode = utils.MakeQRCodeURL(rv.TmeURL) return rv } - -func (c *Access) makeQRCode(data string) string { - values := url.Values{} - values.Set("qzone", "4") - values.Set("format", "svg") - values.Set("data", data) - - return (&url.URL{ - Scheme: "https", - Host: "api.qrserver.com", - Path: "v1/create-qr-code", - RawQuery: values.Encode(), - }).String() -} diff --git a/internal/cli/access_test.go b/internal/cli/access_test.go deleted file mode 100644 index b55d40a..0000000 --- a/internal/cli/access_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package cli_test - -import ( - "net" - "net/http" - "testing" - - "github.com/9seconds/mtg/v2/internal/config" - "github.com/9seconds/mtg/v2/internal/testlib" - "github.com/9seconds/mtg/v2/mtglib" - "github.com/jarcoal/httpmock" - "github.com/stretchr/testify/suite" - "github.com/xeipuuv/gojsonschema" -) - -var accressResponseJSONSchema = func() *gojsonschema.Schema { - schema, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(` -{ - "type": "object", - "required": ["secret"], - "additionalProperties": true, - "properties": { - "secret": { - "type": "object", - "required": [ - "hex", - "base64" - ], - "additionalProperties": false, - "properties": { - "hex": { - "type": "string", - "minLength": 34 - }, - "base64": { - "type": "string", - "minLength": 10 - } - } - }, - "ipv4": { - "$ref": "#/definitions/ip" - }, - "ipv6": { - "$ref": "#/definitions/ip" - } - }, - "definitions": { - "ip": { - "type": "object", - "required": [ - "ip", - "port", - "tg_url", - "tg_qrcode", - "tme_url", - "tme_qrcode" - ], - "additionalProperties": false, - "properties": { - "ip": { - "type": "string", - "minLength": 1, - "anyOf": [ - { - "format": "ipv4" - }, - { - "format": "ipv6" - } - ] - }, - "port": { - "type": "integer", - "multipleOf": 1.0, - "exclusiveMinimum": 0, - "exclusiveMaximum": 65536 - }, - "tg_url": { - "type": "string", - "minLength": 1, - "format": "uri" - }, - "tg_qrcode": { - "type": "string", - "minLength": 1, - "format": "uri" - }, - "tme_url": { - "type": "string", - "minLength": 1, - "format": "uri" - }, - "tme_qrcode": { - "type": "string", - "minLength": 1, - "format": "uri" - } - } - } - } -} - `)) - if err != nil { - panic(err) - } - - return schema -}() - -type AccessTestSuite struct { - CommonTestSuite -} - -func (suite *AccessTestSuite) SetupTest() { - suite.CommonTestSuite.SetupTest() - - suite.cli.Access.Config = &config.Config{} - suite.cli.Access.Config.Secret = mtglib.GenerateSecret("google.com") - suite.cli.Access.Network = suite.networkMock - - suite.NoError( - suite.cli.Access.Config.BindTo.UnmarshalText([]byte("0.0.0.0:80"))) -} - -func (suite *AccessTestSuite) TestGenerateNoCalls() { - suite.cli.Access.PublicIPv4 = net.ParseIP("10.0.0.10") - suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334") - - output := testlib.CaptureStdout(func() { - suite.NoError(suite.cli.Access.Execute(suite.cli)) - }) - - validated, err := accressResponseJSONSchema.Validate( - gojsonschema.NewStringLoader(output)) - suite.NoError(err) - suite.Empty(validated.Errors()) - suite.True(validated.Valid()) - - suite.Contains(output, "10.0.0.10") - suite.Contains(output, "2001:db8:85a3::8a2e:370:7334") - suite.Contains(output, "ipv4") - suite.Contains(output, "ipv6") - suite.Contains(output, suite.cli.Access.Config.Secret.Base64()) - suite.Contains(output, suite.cli.Access.Config.Secret.Hex()) -} - -func (suite *AccessTestSuite) TestGenerateIPv4Call() { - suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334") - - httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co", - httpmock.NewStringResponder(http.StatusOK, "10.11.12.13")) - - output := testlib.CaptureStdout(func() { - suite.NoError(suite.cli.Access.Execute(suite.cli)) - }) - - validated, err := accressResponseJSONSchema.Validate( - gojsonschema.NewStringLoader(output)) - suite.NoError(err) - suite.Empty(validated.Errors()) - suite.True(validated.Valid()) - - suite.Contains(output, "10.11.12.13") - suite.Contains(output, "2001:db8:85a3::8a2e:370:7334") - suite.Contains(output, "ipv4") - suite.Contains(output, "ipv6") - suite.Contains(output, suite.cli.Access.Config.Secret.Base64()) - suite.Contains(output, suite.cli.Access.Config.Secret.Hex()) -} - -func (suite *AccessTestSuite) TestIPv4CallFail() { - suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334") - - httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co", - httpmock.NewStringResponder(http.StatusForbidden, "")) - - output := testlib.CaptureStdout(func() { - suite.NoError(suite.cli.Access.Execute(suite.cli)) - }) - - validated, err := accressResponseJSONSchema.Validate( - gojsonschema.NewStringLoader(output)) - suite.NoError(err) - suite.Empty(validated.Errors()) - suite.True(validated.Valid()) - - suite.Contains(output, "2001:db8:85a3::8a2e:370:7334") - suite.NotContains(output, "ipv4") - suite.Contains(output, "ipv6") - suite.Contains(output, suite.cli.Access.Config.Secret.Base64()) - suite.Contains(output, suite.cli.Access.Config.Secret.Hex()) -} - -func TestAccess(t *testing.T) { // nolint: paralleltest - suite.Run(t, &AccessTestSuite{}) -} diff --git a/internal/cli/base.go b/internal/cli/base.go deleted file mode 100644 index 0bdc93b..0000000 --- a/internal/cli/base.go +++ /dev/null @@ -1,81 +0,0 @@ -package cli - -import ( - "fmt" - "net" - "net/url" - "os" - - "github.com/9seconds/mtg/v2/internal/config" - "github.com/9seconds/mtg/v2/mtglib" - "github.com/9seconds/mtg/v2/network" -) - -type base struct { - ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll - - Network mtglib.Network `kong:"-"` - Config *config.Config `kong:"-"` -} - -func (b *base) ReadConfig(version string) error { - content, err := os.ReadFile(b.ConfigPath) - if err != nil { - return fmt.Errorf("cannot read config file: %w", err) - } - - conf, err := config.Parse(content) - if err != nil { - return fmt.Errorf("cannot parse config: %w", err) - } - - ntw, err := b.makeNetwork(conf, version) - if err != nil { - return fmt.Errorf("cannot build a network: %w", err) - } - - b.Config = conf - b.Network = ntw - - return nil -} - -func (b *base) makeNetwork(conf *config.Config, version string) (mtglib.Network, error) { - tcpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultTimeout) - httpTimeout := conf.Network.Timeout.HTTP.Value(network.DefaultHTTPTimeout) - dohIP := conf.Network.DOHIP.Value(net.ParseIP(network.DefaultDOHHostname)).String() - bufferSize := conf.TCPBuffer.Value(network.DefaultBufferSize) - userAgent := "mtg/" + version - - baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize)) - if err != nil { - return nil, fmt.Errorf("cannot build a default dialer: %w", err) - } - - proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies)) - - for _, v := range conf.Network.Proxies { - if value := v.Value(nil); value != nil { - proxyURLs = append(proxyURLs, v.Value(nil)) - } - } - - switch len(proxyURLs) { - case 0: - return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck - case 1: - socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0]) - if err != nil { - return nil, fmt.Errorf("cannot build socks5 dialer: %w", err) - } - - return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck - } - - socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs) - if err != nil { - return nil, fmt.Errorf("cannot build socks5 dialer: %w", err) - } - - return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck -} diff --git a/internal/cli/base_internal_test.go b/internal/cli/base_internal_test.go deleted file mode 100644 index 51909d4..0000000 --- a/internal/cli/base_internal_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package cli - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/suite" -) - -type BaseTestSuite struct { - suite.Suite - - b base -} - -func (suite *BaseTestSuite) SetupTest() { - suite.b = base{} -} - -func (suite *BaseTestSuite) TestReadConfigNok() { - suite.b.ConfigPath = filepath.Join("testdata", "unknown") - suite.Error(suite.b.ReadConfig("dev")) -} - -func (suite *BaseTestSuite) TestReadConfig() { - suite.b.ConfigPath = filepath.Join("testdata", "minimal.toml") - suite.NoError(suite.b.ReadConfig("dev")) -} - -func TestBase(t *testing.T) { - t.Parallel() - suite.Run(t, &BaseTestSuite{}) -} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d2d813c..f287ad8 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -5,6 +5,7 @@ import "github.com/alecthomas/kong" type CLI struct { GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"` Access Access `kong:"cmd,help='Print access information.'"` - Run Proxy `kong:"cmd,help='Run proxy.'"` + Run Run `kong:"cmd,help='Run proxy.'"` + SimpleRun SimpleRun `kong:"cmd,help='Run proxy without config file.'"` Version kong.VersionFlag `kong:"help='Print version.',short='v'"` } diff --git a/internal/cli/generate_secret.go b/internal/cli/generate_secret.go index e3fb0e8..17b3b09 100644 --- a/internal/cli/generate_secret.go +++ b/internal/cli/generate_secret.go @@ -11,10 +11,10 @@ type GenerateSecret struct { Hex bool `kong:"help='Print secret in hex encoding.',short='x'"` } -func (c *GenerateSecret) Run(cli *CLI, _ string) error { +func (g *GenerateSecret) Run(cli *CLI, _ string) error { secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName) - if cli.GenerateSecret.Hex { + if g.Hex { fmt.Println(secret.Hex()) // nolint: forbidigo } else { fmt.Println(secret.Base64()) // nolint: forbidigo diff --git a/internal/cli/generate_secret_test.go b/internal/cli/generate_secret_test.go deleted file mode 100644 index 64c6002..0000000 --- a/internal/cli/generate_secret_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package cli_test - -import ( - "strings" - "testing" - - "github.com/9seconds/mtg/v2/internal/testlib" - "github.com/9seconds/mtg/v2/mtglib" - "github.com/stretchr/testify/suite" -) - -type GenerateSecretTestSuite struct { - CommonTestSuite -} - -func (suite *GenerateSecretTestSuite) SetupTest() { - suite.CommonTestSuite.SetupTest() - - suite.cli.GenerateSecret.HostName = "google.com" -} - -func (suite *GenerateSecretTestSuite) TestDefault() { - output := testlib.CaptureStdout(func() { - suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev")) - }) - suite.True(strings.HasPrefix(output, "7")) - - secret, err := mtglib.ParseSecret(output) - suite.NoError(err) - suite.True(secret.Valid()) - suite.Equal("google.com", secret.Host) -} - -func (suite *GenerateSecretTestSuite) TestHex() { - suite.cli.GenerateSecret.Hex = true - - output := testlib.CaptureStdout(func() { - suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev")) - }) - suite.True(strings.HasPrefix(output, "ee")) - - secret, err := mtglib.ParseSecret(output) - suite.NoError(err) - suite.True(secret.Valid()) - suite.Equal("google.com", secret.Host) -} - -func TestGenerateSecret(t *testing.T) { - t.Parallel() - suite.Run(t, &GenerateSecretTestSuite{}) -} diff --git a/internal/cli/init_test.go b/internal/cli/init_test.go deleted file mode 100644 index e94e829..0000000 --- a/internal/cli/init_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package cli_test - -import ( - "net/http" - - "github.com/9seconds/mtg/v2/internal/cli" - "github.com/9seconds/mtg/v2/internal/testlib" - "github.com/jarcoal/httpmock" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" -) - -type CommonTestSuite struct { - suite.Suite - - cli *cli.CLI - networkMock *testlib.MtglibNetworkMock - httpClient *http.Client -} - -func (suite *CommonTestSuite) SetupTest() { - suite.networkMock = &testlib.MtglibNetworkMock{} - suite.httpClient = &http.Client{} - suite.cli = &cli.CLI{} - - httpmock.ActivateNonDefault(suite.httpClient) - - suite.networkMock. - On("MakeHTTPClient", mock.Anything). - Maybe(). - Return(suite.httpClient) -} - -func (suite *CommonTestSuite) TearDownTest() { - suite.networkMock.AssertExpectations(suite.T()) - httpmock.DeactivateAndReset() -} diff --git a/internal/cli/proxy.go b/internal/cli/proxy.go deleted file mode 100644 index b20b39d..0000000 --- a/internal/cli/proxy.go +++ /dev/null @@ -1,167 +0,0 @@ -package cli - -import ( - "fmt" - "net" - "os" - - "github.com/9seconds/mtg/v2/antireplay" - "github.com/9seconds/mtg/v2/events" - "github.com/9seconds/mtg/v2/internal/utils" - "github.com/9seconds/mtg/v2/ipblocklist" - "github.com/9seconds/mtg/v2/logger" - "github.com/9seconds/mtg/v2/mtglib" - "github.com/9seconds/mtg/v2/stats" - "github.com/rs/zerolog" -) - -type Proxy struct { - base -} - -func (c *Proxy) Run(cli *CLI, version string) error { - if err := c.ReadConfig(version); err != nil { - return fmt.Errorf("cannot init config: %w", err) - } - - return c.Execute() -} - -func (c *Proxy) Execute() error { - zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs - zerolog.TimestampFieldName = "timestamp" - zerolog.LevelFieldName = "level" - - if c.Config.Debug { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - } else { - zerolog.SetGlobalLevel(zerolog.WarnLevel) - } - - ctx := utils.RootContext() - opts := mtglib.ProxyOpts{ - Logger: logger.NewZeroLogger(zerolog.New(os.Stdout).With().Timestamp().Logger()), - Network: c.Network, - AntiReplayCache: antireplay.NewNoop(), - IPBlocklist: ipblocklist.NewNoop(), - EventStream: events.NewNoopStream(), - - Secret: c.Config.Secret, - BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize), - DomainFrontingPort: c.Config.DomainFrontingPort.Value(mtglib.DefaultDomainFrontingPort), - IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout), - PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP), - } - - opts.Logger.BindStr("configuration", c.Config.String()).Debug("configuration") - - c.setupAntiReplayCache(&opts) - - if err := c.setupIPBlocklist(&opts); err != nil { - return fmt.Errorf("cannot setup ipblocklist: %w", err) - } - - if err := c.setupEventStream(&opts); err != nil { - return fmt.Errorf("cannot setup event stream: %w", err) - } - - proxy, err := mtglib.NewProxy(opts) - if err != nil { - return fmt.Errorf("cannot create a proxy: %w", err) - } - - listener, err := net.Listen("tcp", c.Config.BindTo.String()) - if err != nil { - return fmt.Errorf("cannot start proxy: %w", err) - } - - go proxy.Serve(listener) // nolint: errcheck - - <-ctx.Done() - listener.Close() - proxy.Shutdown() - - return nil -} - -func (c *Proxy) setupAntiReplayCache(opts *mtglib.ProxyOpts) { - if !c.Config.Defense.AntiReplay.Enabled { - return - } - - opts.AntiReplayCache = antireplay.NewStableBloomFilter( - c.Config.Defense.AntiReplay.MaxSize.Value(antireplay.DefaultStableBloomFilterMaxSize), - c.Config.Defense.AntiReplay.ErrorRate.Value(antireplay.DefaultStableBloomFilterErrorRate), - ) -} - -func (c *Proxy) setupIPBlocklist(opts *mtglib.ProxyOpts) error { - if !c.Config.Defense.Blocklist.Enabled { - return nil - } - - remoteURLs := []string{} - localFiles := []string{} - - for _, v := range c.Config.Defense.Blocklist.URLs { - if v.IsRemote() { - remoteURLs = append(remoteURLs, v.String()) - } else { - localFiles = append(localFiles, v.String()) - } - } - - firehol, err := ipblocklist.NewFirehol(opts.Logger.Named("ipblockist"), - c.Network, - c.Config.Defense.Blocklist.DownloadConcurrency, - remoteURLs, - localFiles) - if err != nil { - return err // nolint: wrapcheck - } - - go firehol.Run(c.Config.Defense.Blocklist.UpdateEach.Value(ipblocklist.DefaultFireholUpdateEach)) - - opts.IPBlocklist = firehol - - return nil -} - -func (c *Proxy) setupEventStream(opts *mtglib.ProxyOpts) error { - factories := make([]events.ObserverFactory, 0, 2) - - if c.Config.Stats.StatsD.Enabled { - statsdFactory, err := stats.NewStatsd( - c.Config.Stats.StatsD.Address.String(), - opts.Logger.Named("statsd"), - c.Config.Stats.StatsD.MetricPrefix.Value(stats.DefaultStatsdMetricPrefix), - c.Config.Stats.StatsD.TagFormat.Value(stats.DefaultStatsdTagFormat)) - if err != nil { - return fmt.Errorf("cannot build statsd observer: %w", err) - } - - factories = append(factories, statsdFactory.Make) - } - - if c.Config.Stats.Prometheus.Enabled { - prometheus := stats.NewPrometheus( - c.Config.Stats.Prometheus.MetricPrefix.Value(stats.DefaultMetricPrefix), - c.Config.Stats.Prometheus.HTTPPath.Value("/"), - ) - - listener, err := net.Listen("tcp", c.Config.Stats.Prometheus.BindTo.String()) - if err != nil { - return fmt.Errorf("cannot start a listener for prometheus: %w", err) - } - - go prometheus.Serve(listener) // nolint: errcheck - - factories = append(factories, prometheus.Make) - } - - if len(factories) > 0 { - opts.EventStream = events.NewEventStream(factories) - } - - return nil -} diff --git a/internal/cli/run.go b/internal/cli/run.go new file mode 100644 index 0000000..390f30a --- /dev/null +++ b/internal/cli/run.go @@ -0,0 +1,20 @@ +package cli + +import ( + "fmt" + + "github.com/9seconds/mtg/v2/internal/utils" +) + +type Run struct { + ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll +} + +func (r *Run) Run(cli *CLI, version string) error { + conf, err := utils.ReadConfig(r.ConfigPath) + if err != nil { + return fmt.Errorf("cannot init config: %w", err) + } + + return runProxy(conf, version) +} diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go new file mode 100644 index 0000000..745319f --- /dev/null +++ b/internal/cli/run_proxy.go @@ -0,0 +1,209 @@ +package cli + +import ( + "fmt" + "net" + "net/url" + "os" + + "github.com/9seconds/mtg/v2/antireplay" + "github.com/9seconds/mtg/v2/events" + "github.com/9seconds/mtg/v2/internal/config" + "github.com/9seconds/mtg/v2/internal/utils" + "github.com/9seconds/mtg/v2/ipblocklist" + "github.com/9seconds/mtg/v2/logger" + "github.com/9seconds/mtg/v2/mtglib" + "github.com/9seconds/mtg/v2/network" + "github.com/9seconds/mtg/v2/stats" + "github.com/rs/zerolog" +) + +func makeLogger(conf *config.Config) mtglib.Logger { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs + zerolog.TimestampFieldName = "timestamp" + zerolog.LevelFieldName = "level" + + if conf.Debug.Get(false) { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } else { + zerolog.SetGlobalLevel(zerolog.WarnLevel) + } + + baseLogger := zerolog.New(os.Stdout).With().Timestamp().Logger() + + return logger.NewZeroLogger(baseLogger) +} + +func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) { + tcpTimeout := conf.Network.Timeout.TCP.Get(network.DefaultTimeout) + httpTimeout := conf.Network.Timeout.HTTP.Get(network.DefaultHTTPTimeout) + dohIP := conf.Network.DOHIP.Get(net.ParseIP(network.DefaultDOHHostname)).String() + bufferSize := conf.TCPBuffer.Get(network.DefaultBufferSize) + userAgent := "mtg/" + version + + baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize)) + if err != nil { + return nil, fmt.Errorf("cannot build a default dialer: %w", err) + } + + if len(conf.Network.Proxies) == 0 { + return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck + } + + proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies)) + + for _, v := range conf.Network.Proxies { + if value := v.Get(nil); value != nil { + proxyURLs = append(proxyURLs, value) + } + } + + if len(proxyURLs) == 1 { + socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0]) + if err != nil { + return nil, fmt.Errorf("cannot build socks5 dialer: %w", err) + } + + return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck + } + + socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs) + if err != nil { + return nil, fmt.Errorf("cannot build socks5 dialer: %w", err) + } + + return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck +} + +func makeAntiReplayCache(conf *config.Config) mtglib.AntiReplayCache { + if !conf.Defense.AntiReplay.Enabled.Get(false) { + return antireplay.NewNoop() + } + + return antireplay.NewStableBloomFilter( + conf.Defense.AntiReplay.MaxSize.Get(antireplay.DefaultStableBloomFilterMaxSize), + conf.Defense.AntiReplay.ErrorRate.Get(antireplay.DefaultStableBloomFilterErrorRate), + ) +} + +func makeIPBlocklist(conf *config.Config, logger mtglib.Logger, ntw mtglib.Network) (mtglib.IPBlocklist, error) { + if !conf.Defense.Blocklist.Enabled.Get(false) { + return ipblocklist.NewNoop(), nil + } + + remoteURLs := []string{} + localFiles := []string{} + + for _, v := range conf.Defense.Blocklist.URLs { + if v.IsRemote() { + remoteURLs = append(remoteURLs, v.String()) + } else { + localFiles = append(localFiles, v.String()) + } + } + + firehol, err := ipblocklist.NewFirehol(logger.Named("ipblockist"), + ntw, + conf.Defense.Blocklist.DownloadConcurrency.Get(1), + remoteURLs, + localFiles) + if err != nil { + return nil, fmt.Errorf("incorrect parameters for firehol: %w", err) + } + + return firehol, nil +} + +func makeEventStream(conf *config.Config, logger mtglib.Logger) (mtglib.EventStream, error) { + factories := make([]events.ObserverFactory, 0, 2) + + if conf.Stats.StatsD.Enabled.Get(false) { + statsdFactory, err := stats.NewStatsd( + conf.Stats.StatsD.Address.Get(""), + logger.Named("statsd"), + conf.Stats.StatsD.MetricPrefix.Get(stats.DefaultStatsdMetricPrefix), + conf.Stats.StatsD.TagFormat.Get(stats.DefaultStatsdTagFormat)) + if err != nil { + return nil, fmt.Errorf("cannot build statsd observer: %w", err) + } + + factories = append(factories, statsdFactory.Make) + } + + if conf.Stats.Prometheus.Enabled.Get(false) { + prometheus := stats.NewPrometheus( + conf.Stats.Prometheus.MetricPrefix.Get(stats.DefaultMetricPrefix), + conf.Stats.Prometheus.HTTPPath.Get("/"), + ) + + listener, err := net.Listen("tcp", conf.Stats.Prometheus.BindTo.Get("")) + if err != nil { + return nil, fmt.Errorf("cannot start a listener for prometheus: %w", err) + } + + go prometheus.Serve(listener) // nolint: errcheck + + factories = append(factories, prometheus.Make) + } + + if len(factories) > 0 { + return events.NewEventStream(factories), nil + } + + return events.NewNoopStream(), nil +} + +func runProxy(conf *config.Config, version string) error { + logger := makeLogger(conf) + + logger.BindStr("configuration", conf.String()).Debug("configuration") + + ntw, err := makeNetwork(conf, version) + if err != nil { + return fmt.Errorf("cannot build network: %w", err) + } + + blocklist, err := makeIPBlocklist(conf, logger, ntw) + if err != nil { + return fmt.Errorf("cannot build ip blocklist: %w", err) + } + + eventStream, err := makeEventStream(conf, logger) + if err != nil { + return fmt.Errorf("cannot build event stream: %w", err) + } + + opts := mtglib.ProxyOpts{ + Logger: logger, + Network: ntw, + AntiReplayCache: makeAntiReplayCache(conf), + IPBlocklist: blocklist, + EventStream: eventStream, + + Secret: conf.Secret, + BufferSize: conf.TCPBuffer.Get(mtglib.DefaultBufferSize), + DomainFrontingPort: conf.DomainFrontingPort.Get(mtglib.DefaultDomainFrontingPort), + IdleTimeout: conf.Network.Timeout.Idle.Get(mtglib.DefaultIdleTimeout), + PreferIP: conf.PreferIP.Get(mtglib.DefaultPreferIP), + } + + proxy, err := mtglib.NewProxy(opts) + if err != nil { + return fmt.Errorf("cannot create a proxy: %w", err) + } + + listener, err := net.Listen("tcp", conf.BindTo.Get("")) + if err != nil { + return fmt.Errorf("cannot start proxy: %w", err) + } + + ctx := utils.RootContext() + + go proxy.Serve(listener) // nolint: errcheck + + <-ctx.Done() + listener.Close() + proxy.Shutdown() + + return nil +} diff --git a/internal/cli/simple_run.go b/internal/cli/simple_run.go new file mode 100644 index 0000000..4d1726a --- /dev/null +++ b/internal/cli/simple_run.go @@ -0,0 +1,84 @@ +package cli + +import ( + "fmt" + "net" + "strconv" + "time" + + "github.com/9seconds/mtg/v2/internal/config" +) + +type SimpleRun struct { + BindTo string `kong:"arg,required,name='bind-to',help='A host:port to bind proxy to.'"` + Secret string `kong:"arg,required,name='secret',help='Proxy secret.'"` + + Debug bool `kong:"name='debug',short='d',help='Run in debug mode.'"` // nolint: lll + Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` // nolint: lll + TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Size of TCP buffer to use.'"` // nolint: lll + PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` // nolint: lll + DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` // nolint: lll + DOHIP net.IP `kong:"name='doh-ip',short='n',default='9.9.9.9',help='IP address of DNS-over-HTTP to use.'"` // nolint: lll + Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` // nolint: lll + AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` // nolint: lll +} + +func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop + conf := &config.Config{} + + if err := conf.BindTo.Set(s.BindTo); err != nil { + return fmt.Errorf("incorrect bind-to parameter: %w", err) + } + + if err := conf.Secret.Set(s.Secret); err != nil { + return fmt.Errorf("incorrect secret: %w", err) + } + + if err := conf.Concurrency.Set(strconv.FormatUint(s.Concurrency, 10)); err != nil { + return fmt.Errorf("incorrect concurrency: %w", err) + } + + if err := conf.TCPBuffer.Set(s.TCPBuffer); err != nil { + return fmt.Errorf("incorrect tcp-buffer: %w", err) + } + + if err := conf.PreferIP.Set(s.PreferIP); err != nil { + return fmt.Errorf("incorrect prefer-ip: %w", err) + } + + if err := conf.DomainFrontingPort.Set(strconv.FormatUint(s.DomainFrontingPort, 10)); err != nil { + return fmt.Errorf("incorrect domain-fronting-port: %w", err) + } + + if err := conf.Network.DOHIP.Set(s.DOHIP.String()); err != nil { + return fmt.Errorf("incorrect doh-ip: %w", err) + } + + if err := conf.Network.Timeout.TCP.Set(s.Timeout.String()); err != nil { + return fmt.Errorf("incorrect timeout: %w", err) + } + + if err := conf.Network.Timeout.HTTP.Set(s.Timeout.String()); err != nil { + return fmt.Errorf("incorrect timeout: %w", err) + } + + if err := conf.Network.Timeout.Idle.Set(s.Timeout.String()); err != nil { + return fmt.Errorf("incorrect timeout: %w", err) + } + + if err := conf.Defense.AntiReplay.MaxSize.Set(s.AntiReplayCacheSize); err != nil { + return fmt.Errorf("incorrect antireplay-cache-size: %w", err) + } + + conf.Debug.Value = s.Debug + conf.Defense.AntiReplay.Enabled.Value = true + conf.Defense.Blocklist.Enabled.Value = false + conf.Stats.StatsD.Enabled.Value = false + conf.Stats.Prometheus.Enabled.Value = false + + if err := conf.Validate(); err != nil { + return fmt.Errorf("invalid result configuration: %w", err) + } + + return runProxy(conf, version) +} diff --git a/internal/config/config.go b/internal/config/config.go index c8e88d3..6230758 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,27 +6,26 @@ import ( "fmt" "github.com/9seconds/mtg/v2/mtglib" - "github.com/pelletier/go-toml" ) type Config struct { - Debug bool `json:"debug"` - Secret mtglib.Secret `json:"secret"` - BindTo TypeHostPort `json:"bindTo"` - TCPBuffer TypeBytes `json:"tcpBuffer"` - PreferIP TypePreferIP `json:"preferIp"` - DomainFrontingPort TypePort `json:"domainFrontingPort"` - TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` - Concurrency uint `json:"concurrency"` + Debug TypeBool `json:"debug"` + Secret mtglib.Secret `json:"secret"` + BindTo TypeHostPort `json:"bindTo"` + TCPBuffer TypeBytes `json:"tcpBuffer"` + PreferIP TypePreferIP `json:"preferIp"` + DomainFrontingPort TypePort `json:"domainFrontingPort"` + TolerateTimeSkewness TypeDuration `json:"tolerateTimeSkewness"` + Concurrency TypeConcurrency `json:"concurrency"` Defense struct { AntiReplay struct { - Enabled bool `json:"enabled"` + Enabled TypeBool `json:"enabled"` MaxSize TypeBytes `json:"maxSize"` ErrorRate TypeErrorRate `json:"errorRate"` } `json:"antiReplay"` Blocklist struct { - Enabled bool `json:"enabled"` - DownloadConcurrency uint `json:"downloadConcurrency"` + Enabled TypeBool `json:"enabled"` + DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"` URLs []TypeBlocklistURI `json:"urls"` UpdateEach TypeDuration `json:"updateEach"` } `json:"blocklist"` @@ -37,18 +36,18 @@ type Config struct { HTTP TypeDuration `json:"http"` Idle TypeDuration `json:"idle"` } `json:"timeout"` - DOHIP TypeIP `json:"dohIp"` - Proxies []TypeURL `json:"proxies"` + DOHIP TypeIP `json:"dohIp"` + Proxies []TypeProxyURL `json:"proxies"` } `json:"network"` Stats struct { StatsD struct { - Enabled bool `json:"enabled"` + Enabled TypeBool `json:"enabled"` Address TypeHostPort `json:"address"` MetricPrefix TypeMetricPrefix `json:"metricPrefix"` TagFormat TypeStatsdTagFormat `json:"tagFormat"` } `json:"statsd"` Prometheus struct { - Enabled bool `json:"enabled"` + Enabled TypeBool `json:"enabled"` BindTo TypeHostPort `json:"bindTo"` HTTPPath TypeHTTPPath `json:"httpPath"` MetricPrefix TypeMetricPrefix `json:"metricPrefix"` @@ -61,7 +60,7 @@ func (c *Config) Validate() error { return fmt.Errorf("invalid secret %s", c.Secret.String()) } - if len(c.BindTo.HostValue(nil)) == 0 || c.BindTo.PortValue(0) == 0 { + if c.BindTo.Get("") == "" { return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String()) } @@ -80,78 +79,3 @@ func (c *Config) String() string { return buf.String() } - -type configRaw struct { - Debug bool `toml:"debug" json:"debug,omitempty"` - Secret string `toml:"secret" json:"secret"` - BindTo string `toml:"bind-to" json:"bindTo"` - TCPBuffer string `toml:"tcp-buffer" json:"tcpBuffer,omitempty"` - PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"` - DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"` - TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` - Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` - Defense struct { - AntiReplay struct { - Enabled bool `toml:"enabled" json:"enabled,omitempty"` - MaxSize string `toml:"max-size" json:"maxSize,omitempty"` - ErrorRate float64 `toml:"error-rate" json:"errorRate,omitempty"` - } `toml:"anti-replay" json:"antiReplay,omitempty"` - Blocklist struct { - Enabled bool `toml:"enabled" json:"enabled,omitempty"` - DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"` - URLs []string `toml:"urls" json:"urls,omitempty"` - UpdateEach string `toml:"update-each" json:"updateEach,omitempty"` - } `toml:"blocklist" json:"blocklist,omitempty"` - } `toml:"defense" json:"defense,omitempty"` - Network struct { - Timeout struct { - TCP string `toml:"tcp" json:"tcp,omitempty"` - HTTP string `toml:"http" json:"http,omitempty"` - Idle string `toml:"idle" json:"idle,omitempty"` - } `toml:"timeout" json:"timeout,omitempty"` - DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` - Proxies []string `toml:"proxies" json:"proxies,omitempty"` - } `toml:"network" json:"network,omitempty"` - Stats struct { - StatsD struct { - Enabled bool `toml:"enabled" json:"enabled,omitempty"` - Address string `toml:"address" json:"address,omitempty"` - MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` - TagFormat string `toml:"tag-format" json:"tagFormat,omitempty"` - } `toml:"statsd" json:"statsd,omitempty"` - Prometheus struct { - Enabled bool `toml:"enabled" json:"enabled,omitempty"` - BindTo string `toml:"bind-to" json:"bindTo,omitempty"` - HTTPPath string `toml:"http-path" json:"httpPath,omitempty"` - MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` - } `toml:"prometheus" json:"prometheus,omitempty"` - } `toml:"stats" json:"stats,omitempty"` -} - -func Parse(rawData []byte) (*Config, error) { - rawConf := &configRaw{} - jsonBuf := &bytes.Buffer{} - conf := &Config{} - - jsonEncoder := json.NewEncoder(jsonBuf) - jsonEncoder.SetEscapeHTML(false) - jsonEncoder.SetIndent("", "") - - if err := toml.Unmarshal(rawData, rawConf); err != nil { - return nil, fmt.Errorf("cannot parse toml config: %w", err) - } - - if err := jsonEncoder.Encode(rawConf); err != nil { - panic(err) - } - - if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil { - return nil, fmt.Errorf("cannot parse a config: %w", err) - } - - if err := conf.Validate(); err != nil { - return nil, fmt.Errorf("cannot validate config: %w", err) - } - - return conf, nil -} diff --git a/internal/config/parse.go b/internal/config/parse.go new file mode 100644 index 0000000..f4e7a05 --- /dev/null +++ b/internal/config/parse.go @@ -0,0 +1,80 @@ +package config + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/pelletier/go-toml" +) + +type tomlConfig struct { + Debug bool `toml:"debug" json:"debug,omitempty"` + Secret string `toml:"secret" json:"secret"` + BindTo string `toml:"bind-to" json:"bindTo"` + TCPBuffer string `toml:"tcp-buffer" json:"tcpBuffer,omitempty"` + PreferIP string `toml:"prefer-ip" json:"preferIp,omitempty"` + DomainFrontingPort uint `toml:"domain-fronting-port" json:"domainFrontingPort,omitempty"` + TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerateTimeSkewness,omitempty"` + Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"` + Defense struct { + AntiReplay struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + MaxSize string `toml:"max-size" json:"maxSize,omitempty"` + ErrorRate float64 `toml:"error-rate" json:"errorRate,omitempty"` + } `toml:"anti-replay" json:"antiReplay,omitempty"` + Blocklist struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + DownloadConcurrency uint `toml:"download-concurrency" json:"downloadConcurrency,omitempty"` + URLs []string `toml:"urls" json:"urls,omitempty"` + UpdateEach string `toml:"update-each" json:"updateEach,omitempty"` + } `toml:"blocklist" json:"blocklist,omitempty"` + } `toml:"defense" json:"defense,omitempty"` + Network struct { + Timeout struct { + TCP string `toml:"tcp" json:"tcp,omitempty"` + HTTP string `toml:"http" json:"http,omitempty"` + Idle string `toml:"idle" json:"idle,omitempty"` + } `toml:"timeout" json:"timeout,omitempty"` + DOHIP string `toml:"doh-ip" json:"dohIp,omitempty"` + Proxies []string `toml:"proxies" json:"proxies,omitempty"` + } `toml:"network" json:"network,omitempty"` + Stats struct { + StatsD struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + Address string `toml:"address" json:"address,omitempty"` + MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` + TagFormat string `toml:"tag-format" json:"tagFormat,omitempty"` + } `toml:"statsd" json:"statsd,omitempty"` + Prometheus struct { + Enabled bool `toml:"enabled" json:"enabled,omitempty"` + BindTo string `toml:"bind-to" json:"bindTo,omitempty"` + HTTPPath string `toml:"http-path" json:"httpPath,omitempty"` + MetricPrefix string `toml:"metric-prefix" json:"metricPrefix,omitempty"` + } `toml:"prometheus" json:"prometheus,omitempty"` + } `toml:"stats" json:"stats,omitempty"` +} + +func Parse(rawData []byte) (*Config, error) { + tomlConf := &tomlConfig{} + jsonBuf := &bytes.Buffer{} + conf := &Config{} + + jsonEncoder := json.NewEncoder(jsonBuf) + jsonEncoder.SetEscapeHTML(false) + jsonEncoder.SetIndent("", "") + + if err := toml.Unmarshal(rawData, tomlConf); err != nil { + return nil, fmt.Errorf("cannot parse toml config: %w", err) + } + + if err := jsonEncoder.Encode(tomlConf); err != nil { + panic(err) + } + + if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil { + return nil, fmt.Errorf("cannot parse a config: %w", err) + } + + return conf, nil +} diff --git a/internal/config/type_blocklist_uri.go b/internal/config/type_blocklist_uri.go index 6bcf6b5..620ea51 100644 --- a/internal/config/type_blocklist_uri.go +++ b/internal/config/type_blocklist_uri.go @@ -8,61 +8,70 @@ import ( ) type TypeBlocklistURI struct { - value string + Value string } -func (c *TypeBlocklistURI) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - text := string(data) - if filepath.IsAbs(text) { - if _, err := os.Stat(text); os.IsNotExist(err) { - return fmt.Errorf("filepath %s does not exist", text) +func (t *TypeBlocklistURI) Set(value string) error { + if stat, err := os.Stat(value); err == nil || os.IsExist(err) { + switch { + case stat.IsDir(): + return fmt.Errorf("value is correct filepath but directory") + case stat.Mode().Perm()&0o400 == 0: + return fmt.Errorf("value is correct filepath but not readable") } - c.value = text + value, err = filepath.Abs(value) + if err != nil { + return fmt.Errorf( + "value is correct filepath but cannot resolve absolute (%s): %w", + value, err) + } + + t.Value = value return nil } - parsedURL, err := url.Parse(text) + parsedURL, err := url.Parse(value) if err != nil { - return fmt.Errorf("incorrect url: %w", err) + return fmt.Errorf("incorrect url (%s): %w", value, err) } switch parsedURL.Scheme { - case "http", "https": // nolint: goconst + case "http", "https": default: - return fmt.Errorf("unknown schema %s", parsedURL.Scheme) + return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value) } if parsedURL.Host == "" { - return fmt.Errorf("incorrect url %s", text) + return fmt.Errorf("incorrect url %s", value) } - c.value = parsedURL.String() + t.Value = parsedURL.String() return nil } -func (c TypeBlocklistURI) MarshalText() ([]byte, error) { - return []byte(c.value), nil -} - -func (c TypeBlocklistURI) String() string { - return c.value -} - -func (c TypeBlocklistURI) IsRemote() bool { - return !filepath.IsAbs(c.value) -} - -func (c TypeBlocklistURI) Value(defaultValue string) string { - if c.value == "" { +func (t TypeBlocklistURI) Get(defaultValue string) string { + if t.Value == "" { return defaultValue } - return c.value + return t.Value +} + +func (t TypeBlocklistURI) IsRemote() bool { + return !filepath.IsAbs(t.Value) +} + +func (t *TypeBlocklistURI) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBlocklistURI) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBlocklistURI) String() string { + return t.Value } diff --git a/internal/config/type_blocklist_uri_test.go b/internal/config/type_blocklist_uri_test.go index cdba55a..3cf76bc 100644 --- a/internal/config/type_blocklist_uri_test.go +++ b/internal/config/type_blocklist_uri_test.go @@ -1,12 +1,10 @@ package config_test import ( - "crypto/rand" - "encoding/base64" "encoding/json" "os" "path/filepath" - "strconv" + "strings" "testing" "github.com/9seconds/mtg/v2/internal/config" @@ -20,42 +18,28 @@ type typeBlocklistURITestStruct struct { type TypeBlocklistURITestSuite struct { suite.Suite + + directory string + absDirectory string } -func (suite *TypeBlocklistURITestSuite) TestUnmarshalNil() { - typ := &config.TypeBlocklistURI{} - suite.NoError(typ.UnmarshalText(nil)) - suite.Empty(typ.String()) -} +func (suite *TypeBlocklistURITestSuite) SetupSuite() { + dir, _ := os.Getwd() + absDir, _ := filepath.Abs(dir) -func (suite *TypeBlocklistURITestSuite) TestUnknownSchema() { - typ := &config.TypeBlocklistURI{} - suite.Error(typ.UnmarshalText([]byte("gopher://lalala"))) -} - -func (suite *TypeBlocklistURITestSuite) TestEmptyHost() { - typ := &config.TypeBlocklistURI{} - suite.Error(typ.UnmarshalText([]byte("https:///path"))) -} - -func (suite *TypeBlocklistURITestSuite) TestIncorrectURL() { - typ := &config.TypeBlocklistURI{} - suite.Error(typ.UnmarshalText([]byte("h:/--"))) + suite.directory = dir + suite.absDirectory = absDir } func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() { - rnd := make([]byte, 48) - - rand.Read(rnd) // nolint: errcheck - - unknownPath := base64.StdEncoding.EncodeToString(rnd) - testData := []string{ - "1", - unknownPath, - "/" + unknownPath, - "http:/", - "gopher://lalalal", + "gopher://lalala", + "https:///paths", + "h:/=", + filepath.Join(suite.directory, "___"), + filepath.Join(suite.absDirectory, "___"), + suite.directory, + suite.absDirectory, } for _, v := range testData { @@ -71,13 +55,12 @@ func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() { } func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() { - dir, _ := os.Getwd() - dir, _ = filepath.Abs(dir) - testData := []string{ "http://lalala", - filepath.Join(dir, "config.go"), "https://lalala", + "https://lalala/path", + filepath.Join(suite.directory, "config.go"), + filepath.Join(suite.absDirectory, "config.go"), } for _, v := range testData { @@ -92,77 +75,9 @@ func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() { testStruct := &typeBlocklistURITestStruct{} assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, value, testStruct.Value.Value("")) - }) - } -} + assert.EqualValues(t, value, testStruct.Value.Get("")) -func (suite *TypeBlocklistURITestSuite) TestMarshalOk() { - dir, _ := os.Getwd() - dir, _ = filepath.Abs(dir) - - testData := []string{ - "http://lalalal", - filepath.Join(dir, "config.go"), - } - - for _, v := range testData { - name := v - - data, err := json.Marshal(map[string]string{ - "value": name, - }) - suite.NoError(err) - - suite.T().Run(name, func(t *testing.T) { - testStruct := &typeBlocklistURITestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, name, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - assert.Equal(t, name, string(marshalled)) - }) - } -} - -func (suite *TypeBlocklistURITestSuite) TestValue() { - testStruct := &typeBlocklistURITestStruct{} - - suite.Equal("http://lalala", testStruct.Value.Value("http://lalala")) - - data, err := json.Marshal(map[string]string{ - "value": "http://blablabla", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.Equal("http://blablabla", testStruct.Value.Value("")) -} - -func (suite *TypeBlocklistURITestSuite) TestIsRemote() { - dir, _ := os.Getwd() - dir, _ = filepath.Abs(dir) - - testData := map[bool]string{ - true: "http://lalalal", - false: filepath.Join(dir, "config.go"), - } - - for k, v := range testData { - ok := k - - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(strconv.FormatBool(ok), func(t *testing.T) { - testStruct := &typeBlocklistURITestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - - if ok { + if strings.HasPrefix(value, "http") { assert.True(t, testStruct.Value.IsRemote()) } else { assert.False(t, testStruct.Value.IsRemote()) @@ -171,6 +86,27 @@ func (suite *TypeBlocklistURITestSuite) TestIsRemote() { } } +func (suite *TypeBlocklistURITestSuite) TestMarshalOk() { + testStruct := &typeBlocklistURITestStruct{ + Value: config.TypeBlocklistURI{ + Value: "http://some.url/with/path", + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "http://some.url/with/path"}`, string(data)) +} + +func (suite *TypeBlocklistURITestSuite) TestGet() { + value := config.TypeBlocklistURI{} + suite.Equal("/path", value.Get("/path")) + + suite.NoError(value.Set("http://lalala.ru")) + suite.Equal("http://lalala.ru", value.Get("/path")) + suite.Equal("http://lalala.ru", value.Get("")) +} + func TestTypeBlocklistURI(t *testing.T) { t.Parallel() suite.Run(t, &TypeBlocklistURITestSuite{}) diff --git a/internal/config/type_bool.go b/internal/config/type_bool.go new file mode 100644 index 0000000..f1ec9d5 --- /dev/null +++ b/internal/config/type_bool.go @@ -0,0 +1,37 @@ +package config + +import ( + "fmt" + "strconv" +) + +type TypeBool struct { + Value bool +} + +func (t *TypeBool) Set(data string) error { + parsed, err := strconv.ParseBool(data) + if err != nil { + return fmt.Errorf("incorrect bool value: %s", data) + } + + t.Value = parsed + + return nil +} + +func (t TypeBool) Get(defaultValue bool) bool { + return t.Value || defaultValue +} + +func (t *TypeBool) UnmarshalJSON(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBool) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBool) String() string { + return strconv.FormatBool(t.Value) +} diff --git a/internal/config/type_bool_test.go b/internal/config/type_bool_test.go new file mode 100644 index 0000000..99d0488 --- /dev/null +++ b/internal/config/type_bool_test.go @@ -0,0 +1,107 @@ +package config_test + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/9seconds/mtg/v2/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBoolTestStruct struct { + Value config.TypeBool `json:"value"` +} + +type TypeBoolTestSuite struct { + suite.Suite +} + +func (suite *TypeBoolTestSuite) TestUnmarshalFail() { + testData := []interface{}{ + "", + "np", + "нет", + int(10), + []int{}, + } + + for _, v := range testData { + data, err := json.Marshal(map[string]interface{}{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(fmt.Sprintf("%v", v), func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{})) + }) + } +} + +func (suite *TypeBoolTestSuite) TestUnmarshalOk() { + testData := []bool{ + true, + false, + } + + for _, v := range testData { + value := v + + data, err := json.Marshal(map[string]bool{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(strconv.FormatBool(v), func(t *testing.T) { + testStruct := &typeBoolTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + + if value { + assert.True(t, testStruct.Value.Value) + } else { + assert.False(t, testStruct.Value.Value) + } + }) + } +} + +func (suite *TypeBoolTestSuite) TestMarshalOk() { + for _, v := range []bool{true, false} { + value := v + + suite.T().Run(strconv.FormatBool(v), func(t *testing.T) { + testStruct := typeBoolTestStruct{ + Value: config.TypeBool{ + Value: value, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]bool{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *TypeBoolTestSuite) TestGet() { + value := config.TypeBool{} + suite.False(value.Get(false)) + suite.True(value.Get(true)) + + value.Value = true + suite.True(value.Get(false)) + suite.True(value.Get(true)) +} + +func TestTypeBool(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeBoolTestSuite{}) +} diff --git a/internal/config/type_bytes.go b/internal/config/type_bytes.go index 4c7d7d6..254d556 100644 --- a/internal/config/type_bytes.go +++ b/internal/config/type_bytes.go @@ -7,48 +7,49 @@ import ( "github.com/alecthomas/units" ) +var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB") + type TypeBytes struct { - value units.Base2Bytes + Value units.Base2Bytes } -func (c *TypeBytes) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } +func (t *TypeBytes) Set(value string) error { + normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) - normalizedData := strings.ToUpper(string(data)) - normalizedData = strings.ReplaceAll(normalizedData, "IB", "iB") - - value, err := units.ParseBase2Bytes(normalizedData) + parsedValue, err := units.ParseBase2Bytes(normalizedValue) if err != nil { - return fmt.Errorf("incorrect bytes value: %w", err) + return fmt.Errorf("incorrect bytes value (%v): %w", value, err) } - if value < 0 { - return fmt.Errorf("%d should be positive number", value) + if parsedValue < 0 { + return fmt.Errorf("bytes should be positive (%s)", value) } - c.value = value + t.Value = parsedValue return nil } -func (c TypeBytes) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeBytes) String() string { - if c.value == 0 { - return "" - } - - return strings.ToLower(c.value.String()) -} - -func (c TypeBytes) Value(defaultValue uint) uint { - if c.value == 0 { +func (t TypeBytes) Get(defaultValue uint) uint { + if t.Value == 0 { return defaultValue } - return uint(c.value) + return uint(t.Value) +} + +func (t *TypeBytes) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBytes) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBytes) String() string { + if t.Value == 0 { + return "" + } + + return strings.ToLower(t.Value.String()) } diff --git a/internal/config/type_bytes_test.go b/internal/config/type_bytes_test.go index e395274..bee0922 100644 --- a/internal/config/type_bytes_test.go +++ b/internal/config/type_bytes_test.go @@ -17,19 +17,12 @@ type TypeBytesTestSuite struct { suite.Suite } -func (suite *TypeBytesTestSuite) TestUnmarshalNil() { - typ := &config.TypeBytes{} - suite.NoError(typ.UnmarshalText(nil)) - suite.Empty(typ.String()) -} - func (suite *TypeBytesTestSuite) TestUnmarshalFail() { testData := []string{ "1m", "1", "-1kb", "-1kib", - "-1QB", } for _, v := range testData { @@ -65,53 +58,26 @@ func (suite *TypeBytesTestSuite) TestUnmarshalOk() { testStruct := &typeBytesTestStruct{} assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, value, testStruct.Value.Value(0)) + assert.EqualValues(t, value, testStruct.Value.Get(0)) }) } } func (suite *TypeBytesTestSuite) TestMarshalOk() { - testData := []string{ - "1b", - "1kib", - "2mib", - } + value := typeBytesTestStruct{} + suite.NoError(value.Value.Set("1kib")) - for _, v := range testData { - name := v - - data, err := json.Marshal(map[string]string{ - "value": name, - }) - suite.NoError(err) - - suite.T().Run(name, func(t *testing.T) { - testStruct := &typeBytesTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, name, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - assert.Equal(t, name, string(marshalled)) - }) - } + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "1kib"}`, string(data)) } -func (suite *TypeBytesTestSuite) TestValue() { - testStruct := &typeBytesTestStruct{} +func (suite *TypeBytesTestSuite) TestGet() { + value := config.TypeBytes{} + suite.EqualValues(1000, value.Get(1000)) - suite.EqualValues(0, testStruct.Value.Value(0)) - suite.EqualValues(1, testStruct.Value.Value(1)) - - data, err := json.Marshal(map[string]string{ - "value": "1kb", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.EqualValues(1024, testStruct.Value.Value(0)) - suite.EqualValues(1024, testStruct.Value.Value(1)) + suite.NoError(value.Set("1mib")) + suite.EqualValues(1048576, value.Get(1000)) } func TestTypeBytes(t *testing.T) { diff --git a/internal/config/type_concurrency.go b/internal/config/type_concurrency.go new file mode 100644 index 0000000..4f39b3b --- /dev/null +++ b/internal/config/type_concurrency.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + "strconv" +) + +type TypeConcurrency struct { + Value uint +} + +func (t *TypeConcurrency) Set(value string) error { + concurrencyValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return fmt.Errorf("value is not uint (%s): %w", value, err) + } + + if concurrencyValue == 0 { + return fmt.Errorf("value should be >0 (%s)", value) + } + + t.Value = uint(concurrencyValue) + + return nil +} + +func (t TypeConcurrency) Get(defaultValue uint) uint { + if t.Value == 0 { + return defaultValue + } + + return t.Value +} + +func (t *TypeConcurrency) UnmarshalJSON(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeConcurrency) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeConcurrency) String() string { + return strconv.FormatUint(uint64(t.Value), 10) +} diff --git a/internal/config/type_concurrency_test.go b/internal/config/type_concurrency_test.go new file mode 100644 index 0000000..cf18024 --- /dev/null +++ b/internal/config/type_concurrency_test.go @@ -0,0 +1,73 @@ +package config_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeConcurrencyTestStruct struct { + Value config.TypeConcurrency `json:"value"` +} + +type TypeConcurrencyTestSuite struct { + suite.Suite +} + +func (suite *TypeConcurrencyTestSuite) TestUnmarshalFail() { + testData := []string{ + "-1", + "0", + "0.0", + "1.0", + "1.1", + ".", + "some_value", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeConcurrencyTestStruct{})) + }) + } +} + +func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() { + testStruct := &typeConcurrencyTestStruct{} + + suite.NoError(json.Unmarshal([]byte(`{"value": 1}`), testStruct)) + suite.EqualValues(1, testStruct.Value.Get(2)) +} + +func (suite *TypeConcurrencyTestSuite) TestMarshalOk() { + testStruct := &typeConcurrencyTestStruct{ + Value: config.TypeConcurrency{ + Value: 2, + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": 2}`, string(data)) +} + +func (suite *TypeConcurrencyTestSuite) TestGet() { + value := config.TypeConcurrency{} + suite.EqualValues(1, value.Get(1)) + + value.Value = 3 + suite.EqualValues(3, value.Get(1)) +} + +func TestTypeConcurrency(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeConcurrencyTestSuite{}) +} diff --git a/internal/config/type_duration.go b/internal/config/type_duration.go index 8971a25..676f5cb 100644 --- a/internal/config/type_duration.go +++ b/internal/config/type_duration.go @@ -6,41 +6,48 @@ import ( "time" ) +var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "") + type TypeDuration struct { - value time.Duration + Value time.Duration } -func (c *TypeDuration) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - dur, err := time.ParseDuration(strings.ToLower(string(data))) +func (t *TypeDuration) Set(value string) error { + parsedValue, err := time.ParseDuration( + typeDurationStringCleaner.Replace(strings.ToLower(value))) if err != nil { - return fmt.Errorf("incorrect duration: %w", err) + return fmt.Errorf("incorrect duration (%s): %w", value, err) } - if dur < 0 { - return fmt.Errorf("%s should be positive duration", dur) + if parsedValue < 0 { + return fmt.Errorf("duration has to be a positive: %s", value) } - c.value = dur + t.Value = parsedValue return nil } -func (c TypeDuration) MarshalText() ([]byte, error) { - return []byte(c.value.String()), nil -} - -func (c TypeDuration) String() string { - return c.value.String() -} - -func (c TypeDuration) Value(defaultValue time.Duration) time.Duration { - if c.value == 0 { +func (t TypeDuration) Get(defaultValue time.Duration) time.Duration { + if t.Value == 0 { return defaultValue } - return c.value + return t.Value +} + +func (t *TypeDuration) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeDuration) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeDuration) String() string { + if t.Value == 0 { + return "" + } + + return t.Value.String() } diff --git a/internal/config/type_duration_test.go b/internal/config/type_duration_test.go index ad2dd4e..5d0e204 100644 --- a/internal/config/type_duration_test.go +++ b/internal/config/type_duration_test.go @@ -18,18 +18,12 @@ type TypeDurationTestSuite struct { suite.Suite } -func (suite *TypeDurationTestSuite) TestUnmarshalNil() { - typ := &config.TypeDuration{} - suite.NoError(typ.UnmarshalText(nil)) - suite.EqualValues(0, typ.Value(0)) -} - func (suite *TypeDurationTestSuite) TestUnmarshalFail() { testData := []string{ - "1t", - "1", "-1s", - "-1h", + "1 seconds ago", + "1s ago", + "", } for _, v := range testData { @@ -47,8 +41,11 @@ func (suite *TypeDurationTestSuite) TestUnmarshalFail() { func (suite *TypeDurationTestSuite) TestUnmarshalOk() { testData := map[string]time.Duration{ "1s": time.Second, - "1m": time.Minute, - "2h1s": 2*time.Hour + time.Second, + "0": 0 * time.Second, + "0s": 0 * time.Second, + "1\tM": time.Minute, + "1H": time.Hour, + "1 h": time.Hour, } for k, v := range testData { @@ -63,53 +60,48 @@ func (suite *TypeDurationTestSuite) TestUnmarshalOk() { testStruct := &typeDurationTestStruct{} assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.Value(0)) + assert.Equal(t, value, testStruct.Value.Value) }) } } func (suite *TypeDurationTestSuite) TestMarshalOk() { - testData := []string{ - "1s", - "1m0s", - "2h0m1s", + testData := map[string]string{ + "1s": "1s", + "0": "", + "0s": "", + "0ms": "", + "1 H": "1h0m0s", } - for _, v := range testData { - name := v + for k, v := range testData { + value := k + expected := v - data, err := json.Marshal(map[string]string{ - "value": name, - }) - suite.NoError(err) - - suite.T().Run(name, func(t *testing.T) { + suite.T().Run(value, func(t *testing.T) { testStruct := &typeDurationTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, name, testStruct.Value.String()) + assert.NoError(t, testStruct.Value.Set(value)) - marshalled, err := testStruct.Value.MarshalText() + data, err := json.Marshal(testStruct) assert.NoError(t, err) - assert.Equal(t, name, string(marshalled)) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": expected, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(data)) }) } } -func (suite *TypeDurationTestSuite) TestValue() { - testStruct := &typeDurationTestStruct{} +func (suite *TypeDurationTestSuite) TestGet() { + value := config.TypeDuration{} + suite.Equal(time.Second, value.Get(time.Second)) - suite.EqualValues(0, testStruct.Value.Value(0)) - suite.Equal(time.Second, testStruct.Value.Value(time.Second)) - - data, err := json.Marshal(map[string]string{ - "value": "1s", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.Equal(time.Second, testStruct.Value.Value(0)) - suite.Equal(time.Second, testStruct.Value.Value(time.Minute)) + value.Value = 3 * time.Second + suite.Equal(3*time.Second, value.Get(time.Hour)) } func TestTypeDuration(t *testing.T) { diff --git a/internal/config/type_error_rate.go b/internal/config/type_error_rate.go index 0214956..4e30e09 100644 --- a/internal/config/type_error_rate.go +++ b/internal/config/type_error_rate.go @@ -8,36 +8,40 @@ import ( const typeErrorRateIgnoreLess = 1e-8 type TypeErrorRate struct { - value float64 + Value float64 } -func (c *TypeErrorRate) UnmarshalJSON(data []byte) error { - value, err := strconv.ParseFloat(string(data), 64) +func (t *TypeErrorRate) Set(value string) error { + parsedValue, err := strconv.ParseFloat(value, 64) if err != nil { - return fmt.Errorf("incorrect float value: %w", err) + return fmt.Errorf("value is not a float (%s): %w", value, err) } - if value <= 0 || value >= 100 { - return fmt.Errorf("%f should be 0 < x < 100", value) + if parsedValue <= 0.0 || parsedValue >= 100.0 { + return fmt.Errorf("value should be 0 < x < 100 (%s)", value) } - c.value = value + t.Value = parsedValue return nil } -func (c *TypeErrorRate) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeErrorRate) String() string { - return strconv.FormatFloat(c.value, 'f', -1, 64) -} - -func (c TypeErrorRate) Value(defaultValue float64) float64 { - if c.value < typeErrorRateIgnoreLess { +func (t TypeErrorRate) Get(defaultValue float64) float64 { + if t.Value < typeErrorRateIgnoreLess { return defaultValue } - return c.value + return t.Value +} + +func (t *TypeErrorRate) UnmarshalJSON(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeErrorRate) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeErrorRate) String() string { + return strconv.FormatFloat(t.Value, 'f', -1, 64) } diff --git a/internal/config/type_error_rate_test.go b/internal/config/type_error_rate_test.go index de99f09..44d17c2 100644 --- a/internal/config/type_error_rate_test.go +++ b/internal/config/type_error_rate_test.go @@ -2,7 +2,6 @@ package config_test import ( "encoding/json" - "strconv" "testing" "github.com/9seconds/mtg/v2/internal/config" @@ -19,104 +18,61 @@ type TypeErrorRateTestSuite struct { } func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() { - testData := []float64{ - 1000, - -100, - -0.0001, + testData := []string{ + "", + "1s", + "1,", + "1,2", + ".", + "3.4.5", + "3.5.", + ".3.5", + "some word", + "1e2", + "-1.0", } for _, v := range testData { - data, err := json.Marshal(map[string]float64{ + data, err := json.Marshal(map[string]string{ "value": v, }) suite.NoError(err) - suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) { + suite.T().Run(v, func(t *testing.T) { assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{})) }) } - - data, err := json.Marshal(map[string]string{ - "value": "hello", - }) - suite.NoError(err) - suite.Error(json.Unmarshal(data, &typeErrorRateTestStruct{})) } func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() { - testData := []float64{ - 1, - 55.5, - 0.0001, - 1e-6, - } + data, err := json.Marshal(map[string]float64{ + "value": 1.0, + }) + suite.NoError(err) - for _, v := range testData { - value := v - - data, err := json.Marshal(map[string]float64{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) { - testStruct := &typeErrorRateTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.InEpsilon(t, value, testStruct.Value.Value(0), 1e-10) - }) - } + testStruct := &typeErrorRateTestStruct{} + suite.NoError(json.Unmarshal(data, testStruct)) + suite.InEpsilon(1.0, testStruct.Value.Value, 1e-10) } func (suite *TypeErrorRateTestSuite) TestMarshalOk() { - testData := []float64{ - 1, - 55.5, - 0.0001, - 1e-6, + testStruct := typeErrorRateTestStruct{ + Value: config.TypeErrorRate{ + Value: 1.01, + }, } - for _, v := range testData { - value := v - - data, err := json.Marshal(map[string]float64{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) { - testStruct := &typeErrorRateTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - - parsed, err := strconv.ParseFloat(testStruct.Value.String(), 64) - assert.NoError(t, err) - assert.InEpsilon(t, value, parsed, 1e-10) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - - parsed, err = strconv.ParseFloat(string(marshalled), 64) - assert.NoError(t, err) - assert.InEpsilon(t, value, parsed, 1e-10) - }) - } + encodedJSON, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": 1.01}`, string(encodedJSON)) } -func (suite *TypeErrorRateTestSuite) TestValue() { - testStruct := &typeErrorRateTestStruct{} +func (suite *TypeErrorRateTestSuite) TestGet() { + value := config.TypeErrorRate{} + suite.InEpsilon(1.0, value.Get(1.0), 1e-10) - suite.InEpsilon(1, testStruct.Value.Value(1), 1e-10) - suite.InEpsilon(2, testStruct.Value.Value(2), 1e-10) - - data, err := json.Marshal(map[string]float64{ - "value": 1, - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.InEpsilon(1, testStruct.Value.Value(2), 1e-10) - suite.InEpsilon(1, testStruct.Value.Value(3), 1e-10) + value.Value = 5.0 + suite.InEpsilon(5.0, value.Get(1.0), 1e-10) } func TestTypeErrorRate(t *testing.T) { diff --git a/internal/config/type_hostport.go b/internal/config/type_hostport.go index 4bc58f1..665ba10 100644 --- a/internal/config/type_hostport.go +++ b/internal/config/type_hostport.go @@ -7,61 +7,57 @@ import ( ) type TypeHostPort struct { - host TypeIP - port TypePort + Value string + Host string + Port uint } -func (c *TypeHostPort) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - text := string(data) - - host, port, err := net.SplitHostPort(text) +func (t *TypeHostPort) Set(value string) error { + host, port, err := net.SplitHostPort(value) if err != nil { - return fmt.Errorf("incorrect host:port syntax: %w", err) + return fmt.Errorf("incorrect host:port value (%v): %w", value, err) } - if port == "" { - return fmt.Errorf("port in %s host:port pair cannot be empty", text) + portValue, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return fmt.Errorf("incorrect port number (%v): %w", value, err) } - if err := c.port.UnmarshalJSON([]byte(port)); err != nil { - return fmt.Errorf("incorrect port in host:port: %w", err) + if portValue == 0 { + return fmt.Errorf("incorrect port number (%s)", value) } - if err := c.host.UnmarshalText([]byte(host)); err != nil { - return fmt.Errorf("incorrect host: %w", err) + if host == "" { + return fmt.Errorf("empty host: %s", value) } + if net.ParseIP(host) == nil { + return fmt.Errorf("host is not an IP address: %s", value) + } + + t.Value = net.JoinHostPort(host, port) + t.Port = uint(portValue) + t.Host = host + return nil } -func (c TypeHostPort) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeHostPort) String() string { - return c.Value(net.IP{}, 0) -} - -func (c TypeHostPort) HostValue(defaultValue net.IP) net.IP { - return c.host.Value(defaultValue) -} - -func (c TypeHostPort) PortValue(defaultValue uint) uint { - return c.port.Value(defaultValue) -} - -func (c TypeHostPort) Value(defaultHostValue net.IP, defaultPortValue uint) string { - host := c.HostValue(defaultHostValue) - port := c.PortValue(defaultPortValue) - - hostStr := "" - if len(host) > 0 { - hostStr = host.String() +func (t TypeHostPort) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue } - return net.JoinHostPort(hostStr, strconv.Itoa(int(port))) + return t.Value +} + +func (t *TypeHostPort) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeHostPort) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeHostPort) String() string { + return t.Value } diff --git a/internal/config/type_hostport_test.go b/internal/config/type_hostport_test.go index 6972e1b..5fc24ec 100644 --- a/internal/config/type_hostport_test.go +++ b/internal/config/type_hostport_test.go @@ -2,7 +2,6 @@ package config_test import ( "encoding/json" - "net" "testing" "github.com/9seconds/mtg/v2/internal/config" @@ -20,11 +19,13 @@ type TypeHostPortTestSuite struct { func (suite *TypeHostPortTestSuite) TestUnmarshalFail() { testData := []string{ - "10.0.0.10:aaa", - "10.0.0.10:", ":", - "xxx", - "xxx:80", + ":800", + "127.0.0.1:8000000", + "12...:80", + "", + "localhost", + "google.com:", } for _, v := range testData { @@ -41,9 +42,8 @@ func (suite *TypeHostPortTestSuite) TestUnmarshalFail() { func (suite *TypeHostPortTestSuite) TestUnmarshalOk() { testData := []string{ - "10.0.0.10:80", - "0.0.0.0:80", - ":8000", + "127.0.0.1:80", + "10.0.0.10:6553", } for _, v := range testData { @@ -56,57 +56,30 @@ func (suite *TypeHostPortTestSuite) TestUnmarshalOk() { suite.T().Run(v, func(t *testing.T) { testStruct := &typeHostPortTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, value, testStruct.Value.Value(nil, 0)) + assert.Equal(t, value, testStruct.Value.Value) }) } } func (suite *TypeHostPortTestSuite) TestMarshalOk() { - testData := []string{ - "10.0.0.10:80", - "0.0.0.0:80", - ":8000", + testStruct := typeHostPortTestStruct{ + Value: config.TypeHostPort{ + Value: "127.0.0.1:8000", + }, } - for _, v := range testData { - value := v - - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(v, func(t *testing.T) { - testStruct := &typeHostPortTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - assert.Equal(t, value, string(marshalled)) - }) - } + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data)) } -func (suite *TypeHostPortTestSuite) TestValue() { - testStruct := &typeHostPortTestStruct{} +func (suite *TypeHostPortTestSuite) TestGet() { + value := config.TypeHostPort{} + suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000")) - suite.EqualValues("127.0.0.1:80", - testStruct.Value.Value(net.ParseIP("127.0.0.1"), 80)) - suite.EqualValues("127.1.0.1:80", - testStruct.Value.Value(net.ParseIP("127.1.0.1"), 80)) - - data, err := json.Marshal(map[string]string{ - "value": "127.0.0.1:80", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(nil, 0)) - suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(net.ParseIP("10.0.0.10"), 3000)) + value.Value = "127.0.0.1:80" + suite.Equal("127.0.0.1:80", value.Get("127.0.0.1:9000")) } func TestTypeHostPort(t *testing.T) { diff --git a/internal/config/type_http_path.go b/internal/config/type_http_path.go index 56a7a4e..abd77f0 100644 --- a/internal/config/type_http_path.go +++ b/internal/config/type_http_path.go @@ -3,33 +3,31 @@ package config import "strings" type TypeHTTPPath struct { - value string + Value string } -func (c *TypeHTTPPath) UnmarshalText(data []byte) error { - if len(data) > 0 { - c.value = "/" + strings.Trim(string(data), "/") - } +func (t *TypeHTTPPath) Set(value string) error { + t.Value = "/" + strings.Trim(value, "/") return nil } -func (c TypeHTTPPath) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeHTTPPath) String() string { - if c.value == "" { - return "/" - } - - return c.value -} - -func (c TypeHTTPPath) Value(defaultValue string) string { - if c.value == "" { +func (t TypeHTTPPath) Get(defaultValue string) string { + if t.Value == "" { return defaultValue } - return c.value + return t.Value +} + +func (t *TypeHTTPPath) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeHTTPPath) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeHTTPPath) String() string { + return t.Value } diff --git a/internal/config/type_http_path_test.go b/internal/config/type_http_path_test.go index 01843db..ee764cc 100644 --- a/internal/config/type_http_path_test.go +++ b/internal/config/type_http_path_test.go @@ -17,72 +17,48 @@ type TypeHTTPPathTestSuite struct { suite.Suite } -func (suite *TypeHTTPPathTestSuite) TestUnmarshal() { - testData := []string{ - "/hello", - "hello", - "hello/", - "/hello/", +func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() { + testData := map[string]string{ + "": "/", + "/": "/", + "/path": "/path", + "path": "/path", } - for _, v := range testData { + for k, v := range testData { + value := v + data, err := json.Marshal(map[string]string{ - "value": v, + "value": k, }) suite.NoError(err) - suite.T().Run(v, func(t *testing.T) { + suite.T().Run(k, func(t *testing.T) { testStruct := &typeHTTPPathTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, "/hello", testStruct.Value.Value("")) + assert.Equal(t, value, testStruct.Value.Get("")) }) } } func (suite *TypeHTTPPathTestSuite) TestMarshalOk() { - testData := map[string]string{ - "": "/", - "/hello": "/hello", - "/hello/": "/hello", - "hello/": "/hello", - "hello": "/hello", + value := typeHTTPPathTestStruct{ + Value: config.TypeHTTPPath{ + Value: "/path", + }, } - for k, v := range testData { - toPass := k - compareWith := v - - data, err := json.Marshal(map[string]string{ - "value": toPass, - }) - suite.NoError(err) - - suite.T().Run(toPass, func(t *testing.T) { - testStruct := &typeHTTPPathTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, compareWith, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - assert.Equal(t, compareWith, string(marshalled)) - }) - } + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "/path"}`, string(data)) } -func (suite *TypeHTTPPathTestSuite) TestValue() { - testStruct := &typeHTTPPathTestStruct{} +func (suite *TypeHTTPPathTestSuite) TestGet() { + value := config.TypeHTTPPath{} + suite.Equal("/hello", value.Get("/hello")) - suite.Equal("/hello", testStruct.Value.Value("/hello")) - - data, err := json.Marshal(map[string]string{ - "value": "/map", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.Equal("/map", testStruct.Value.Value("/hello")) + suite.NoError(value.Set("/lalala")) + suite.Equal("/lalala", value.Get("/hello")) } func TestTypeHTTPPath(t *testing.T) { diff --git a/internal/config/type_ip.go b/internal/config/type_ip.go index 0fe61e7..c637181 100644 --- a/internal/config/type_ip.go +++ b/internal/config/type_ip.go @@ -6,40 +6,40 @@ import ( ) type TypeIP struct { - value net.IP + Value net.IP } -func (c *TypeIP) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - ip := net.ParseIP(string(data)) +func (t *TypeIP) Set(value string) error { + ip := net.ParseIP(value) if ip == nil { - return fmt.Errorf("incorrect ip address: %s", string(data)) + return fmt.Errorf("incorret ip %s", value) } - c.value = ip + t.Value = ip return nil } -func (c *TypeIP) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeIP) String() string { - if len(c.value) > 0 { - return c.value.String() - } - - return "" -} - -func (c TypeIP) Value(defaultValue net.IP) net.IP { - if c.value == nil { +func (t *TypeIP) Get(defaultValue net.IP) net.IP { + if len(t.Value) == 0 { return defaultValue } - return c.value + return t.Value +} + +func (t *TypeIP) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeIP) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeIP) String() string { + if len(t.Value) == 0 { + return "" + } + + return t.Value.String() } diff --git a/internal/config/type_ip_test.go b/internal/config/type_ip_test.go index 86f14ed..342c195 100644 --- a/internal/config/type_ip_test.go +++ b/internal/config/type_ip_test.go @@ -20,10 +20,11 @@ type TypeIPTestSuite struct { func (suite *TypeIPTestSuite) TestUnmarshalFail() { testData := []string{ - "0.0.10", - "10.0.0.10:", - "xxx:80", - "2001:0db8:85a3:0000:0000:8a2e:4", + "", + "....", + "0...", + "300.200.200.800", + "[]", } for _, v := range testData { @@ -39,74 +40,62 @@ func (suite *TypeIPTestSuite) TestUnmarshalFail() { } func (suite *TypeIPTestSuite) TestUnmarshalOk() { - testData := []string{ - "0.0.0.0", - "10.0.0.10", - "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + testData := map[string]string{ + "2001:0db8:85a3:0000:0000:8a2e:0370:7334": "2001:db8:85a3::8a2e:370:7334", + "127.0.0.1": "127.0.0.1", } - for _, v := range testData { - value := v + for k, v := range testData { + expected := v data, err := json.Marshal(map[string]string{ - "value": v, + "value": k, }) suite.NoError(err) - suite.T().Run(v, func(t *testing.T) { + suite.T().Run(k, func(t *testing.T) { testStruct := &typeIPTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, - net.ParseIP(value).String(), - testStruct.Value.Value(nil).String()) + assert.Equal(t, expected, testStruct.Value.Get(nil).String()) }) } } func (suite *TypeIPTestSuite) TestMarshalOk() { testData := []string{ - "0.0.0.0", - "10.0.0.10", - "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "2001:db8:85a3::8a2e:370:7334", + "127.0.0.1", } for _, v := range testData { - value := net.ParseIP(v).String() - - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) + value := v suite.T().Run(v, func(t *testing.T) { - testStruct := &typeIPTestStruct{} + testStruct := &typeIPTestStruct{ + Value: config.TypeIP{ + Value: net.ParseIP(value), + }, + } - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() + encodedJSON, err := json.Marshal(testStruct) assert.NoError(t, err) - assert.Equal(t, value, string(marshalled)) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) }) } } -func (suite *TypeIPTestSuite) TestValue() { - testStruct := &typeIPTestStruct{} - suite.Empty(testStruct.Value.String()) +func (suite *TypeIPTestSuite) TestGet() { + value := config.TypeIP{} + suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).String()) - suite.Nil(testStruct.Value.Value(nil)) - suite.Equal("127.1.0.1", testStruct.Value.Value(net.ParseIP("127.1.0.1")).String()) - - data, err := json.Marshal(map[string]string{ - "value": "127.0.0.1", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.Equal("127.0.0.1", testStruct.Value.Value(nil).String()) - suite.Equal("127.0.0.1", testStruct.Value.Value(net.ParseIP("10.0.0.10")).String()) + suite.NoError(value.Set("127.0.0.2")) + suite.Equal("127.0.0.2", value.Get(net.ParseIP("127.0.0.1")).String()) } func TestTypeIP(t *testing.T) { diff --git a/internal/config/type_metric_prefix.go b/internal/config/type_metric_prefix.go index 64b5f5b..e55438c 100644 --- a/internal/config/type_metric_prefix.go +++ b/internal/config/type_metric_prefix.go @@ -6,36 +6,35 @@ import ( ) type TypeMetricPrefix struct { - value string + Value string } -func (c *TypeMetricPrefix) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil +func (t *TypeMetricPrefix) Set(value string) error { + if ok, err := regexp.MatchString("^[a-z0-9]+$", value); !ok || err != nil { + return fmt.Errorf("incorrect metric prefix %s: %w", value, err) } - prefix := string(data) - if ok, err := regexp.MatchString("^[a-z0-9]+$", prefix); !ok || err != nil { - return fmt.Errorf("incorrect metric prefix: %s", prefix) - } - - c.value = prefix + t.Value = value return nil } -func (c TypeMetricPrefix) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeMetricPrefix) String() string { - return c.value -} - -func (c TypeMetricPrefix) Value(defaultValue string) string { - if c.value == "" { +func (t TypeMetricPrefix) Get(defaultValue string) string { + if t.Value == "" { return defaultValue } - return c.value + return t.Value +} + +func (t *TypeMetricPrefix) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeMetricPrefix) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeMetricPrefix) String() string { + return t.Value } diff --git a/internal/config/type_metric_prefix_test.go b/internal/config/type_metric_prefix_test.go index 0c3727d..eb60229 100644 --- a/internal/config/type_metric_prefix_test.go +++ b/internal/config/type_metric_prefix_test.go @@ -17,18 +17,13 @@ type TypeMetricPrefixTestSuite struct { suite.Suite } -func (suite *TypeMetricPrefixTestSuite) TestUnmarshalNil() { - typ := &config.TypeMetricPrefix{} - suite.NoError(typ.UnmarshalText(nil)) - suite.Empty(typ.String()) -} - func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() { testData := []string{ - "aaa.aaa", - "aaa-bbb", - "aaa:ccc", - "metric prefix", + "", + "-", + "hello/world", + "lala*", + "++sdf++", } for _, v := range testData { @@ -44,69 +39,29 @@ func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() { } func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() { - testData := []string{ - "mtg", - "mtg111", - } - - for _, v := range testData { - value := v - - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(v, func(t *testing.T) { - testStruct := &typeMetricPrefixTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.Value("")) - }) - } + testStruct := &typeMetricPrefixTestStruct{} + suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct)) + suite.Equal("mtg", testStruct.Value.Get("lalala")) } func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() { - testData := []string{ - "mtg", - "mtg111", + testStruct := &typeMetricPrefixTestStruct{ + Value: config.TypeMetricPrefix{ + Value: "mtg", + }, } - for _, v := range testData { - value := v - - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(v, func(t *testing.T) { - testStruct := &typeMetricPrefixTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - assert.Equal(t, value, string(marshalled)) - }) - } + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "mtg"}`, string(data)) } -func (suite *TypeMetricPrefixTestSuite) TestValue() { - testStruct := &typeMetricPrefixTestStruct{} +func (suite *TypeMetricPrefixTestSuite) TestGet() { + value := config.TypeMetricPrefix{} + suite.Equal("lalala", value.Get("lalala")) - suite.Equal("mtg", testStruct.Value.Value("mtg")) - suite.Equal("vvv", testStruct.Value.Value("vvv")) - - data, err := json.Marshal(map[string]string{ - "value": "aaa", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.Equal("aaa", testStruct.Value.Value("mtg")) - suite.Equal("aaa", testStruct.Value.Value("vvv")) + value.Value = "mtg" + suite.Equal("mtg", value.Get("lalala")) } func TestTypeMetricPrefix(t *testing.T) { diff --git a/internal/config/type_port.go b/internal/config/type_port.go index 892558e..cdab9ee 100644 --- a/internal/config/type_port.go +++ b/internal/config/type_port.go @@ -6,40 +6,40 @@ import ( ) type TypePort struct { - value uint + Value uint } -func (c *TypePort) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - return nil - } - - intValue, err := strconv.ParseUint(string(data), 10, 64) +func (t *TypePort) Set(value string) error { + portValue, err := strconv.ParseUint(value, 10, 16) if err != nil { - return fmt.Errorf("port number is not a number: %w", err) + return fmt.Errorf("incorrect port number (%v): %w", value, err) } - if intValue == 0 || intValue >= 65536 { - return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue) + if portValue == 0 { + return fmt.Errorf("incorrect port number (%s)", value) } - c.value = uint(intValue) + t.Value = uint(portValue) return nil } -func (c *TypePort) MarshalJSON() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypePort) String() string { - return strconv.Itoa(int(c.value)) -} - -func (c TypePort) Value(defaultValue uint) uint { - if c.value == 0 { +func (t TypePort) Get(defaultValue uint) uint { + if t.Value == 0 { return defaultValue } - return c.value + return t.Value +} + +func (t *TypePort) UnmarshalJSON(data []byte) error { + return t.Set(string(data)) +} + +func (t TypePort) MarshalJSON() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypePort) String() string { + return strconv.Itoa(int(t.Value)) } diff --git a/internal/config/type_port_test.go b/internal/config/type_port_test.go index 75fc920..9b7c239 100644 --- a/internal/config/type_port_test.go +++ b/internal/config/type_port_test.go @@ -2,7 +2,6 @@ package config_test import ( "encoding/json" - "strconv" "testing" "github.com/9seconds/mtg/v2/internal/config" @@ -18,97 +17,52 @@ type TypePortTestSuite struct { suite.Suite } -func (suite *TypePortTestSuite) TestUnmarshalNil() { - typ := &config.TypePort{} - suite.NoError(typ.UnmarshalJSON(nil)) - suite.Equal("0", typ.String()) -} - func (suite *TypePortTestSuite) TestUnmarshalFail() { - testData := []int{ - -1, - 1_000_000, + testData := []string{ + "", + "port", + "0", + "-1", + "1.5", + "70000", } for _, v := range testData { - data, err := json.Marshal(map[string]int{ + data, err := json.Marshal(map[string]string{ "value": v, }) suite.NoError(err) - suite.T().Run(strconv.Itoa(v), func(t *testing.T) { + suite.T().Run(v, func(t *testing.T) { assert.Error(t, json.Unmarshal(data, &typePortTestStruct{})) }) } } func (suite *TypePortTestSuite) TestUnmarshalOk() { - testData := []int{ - 1, - 1_000, - 65535, - } - - for _, v := range testData { - value := v - - data, err := json.Marshal(map[string]int{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(strconv.Itoa(v), func(t *testing.T) { - testStruct := &typePortTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, value, testStruct.Value.Value(0)) - }) - } + testStruct := &typePortTestStruct{} + suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct)) + suite.EqualValues(5, testStruct.Value.Value) } func (suite *TypePortTestSuite) TestMarshalOk() { - testData := map[string]int{ - "1": 1, - "1000": 1000, - "65535": 65535, + testStruct := &typePortTestStruct{ + Value: config.TypePort{ + Value: 10, + }, } - for k, v := range testData { - name := k - value := v - - data, err := json.Marshal(map[string]int{ - "value": value, - }) - suite.NoError(err) - - suite.T().Run(name, func(t *testing.T) { - testStruct := &typePortTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, name, testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalJSON() - assert.NoError(t, err) - assert.Equal(t, name, string(marshalled)) - }) - } + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value":10}`, string(data)) } -func (suite *TypePortTestSuite) TestValue() { - testStruct := &typePortTestStruct{} +func (suite *TypePortTestSuite) TestGet() { + value := config.TypePort{} + suite.EqualValues(10, value.Get(10)) - suite.EqualValues(0, testStruct.Value.Value(0)) - suite.EqualValues(1, testStruct.Value.Value(1)) - - data, err := json.Marshal(map[string]int{ - "value": 5, - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.EqualValues(5, testStruct.Value.Value(0)) - suite.EqualValues(5, testStruct.Value.Value(1)) + value.Value = 100 + suite.EqualValues(100, value.Get(10)) } func TestTypePort(t *testing.T) { diff --git a/internal/config/type_prefer_ip.go b/internal/config/type_prefer_ip.go index 0b30e82..8a07721 100644 --- a/internal/config/type_prefer_ip.go +++ b/internal/config/type_prefer_ip.go @@ -24,38 +24,39 @@ const ( ) type TypePreferIP struct { - value string + Value string } -func (c *TypePreferIP) UnmarshalText(data []byte) error { - if len(data) == 0 { +func (t *TypePreferIP) Set(value string) error { + value = strings.ToLower(value) + + switch value { + case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, + TypePreferOnlyIPv4, TypePreferOnlyIPv6: + t.Value = value + return nil - } - - text := strings.ToLower(string(data)) - - switch text { - case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, TypePreferOnlyIPv4, TypePreferOnlyIPv6: - c.value = text default: - return fmt.Errorf("incorrect prefer-ip value: %s", string(data)) + return fmt.Errorf("unsupported ip preference: %s", value) } - - return nil } -func (c TypePreferIP) MarshalText() ([]byte, error) { - return []byte(c.value), nil -} - -func (c *TypePreferIP) String() string { - return c.value -} - -func (c *TypePreferIP) Value(defaultValue string) string { - if c.value == "" { +func (t *TypePreferIP) Get(defaultValue string) string { + if t.Value == "" { return defaultValue } - return c.value + return t.Value +} + +func (t *TypePreferIP) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypePreferIP) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypePreferIP) String() string { + return t.Value } diff --git a/internal/config/type_prefer_ip_test.go b/internal/config/type_prefer_ip_test.go index 917b918..0051237 100644 --- a/internal/config/type_prefer_ip_test.go +++ b/internal/config/type_prefer_ip_test.go @@ -18,18 +18,12 @@ type TypePreferIPTestSuite struct { suite.Suite } -func (suite *TypePreferIPTestSuite) TestUnmarshalNil() { - typ := &config.TypePreferIP{} - suite.NoError(typ.UnmarshalText(nil)) - suite.Empty(typ.String()) -} - func (suite *TypePreferIPTestSuite) TestUnmarshalFail() { testData := []string{ - "p", - "ipv4", - "onlyipv4", - "ipv6prefer", + "", + "prefer", + "preferipv4", + config.TypePreferIPPreferIPv4 + "_", } for _, v := range testData { @@ -50,14 +44,10 @@ func (suite *TypePreferIPTestSuite) TestUnmarshalOk() { config.TypePreferIPPreferIPv6, config.TypePreferOnlyIPv4, config.TypePreferOnlyIPv6, - strings.ToUpper(config.TypePreferIPPreferIPv4), - strings.ToUpper(config.TypePreferIPPreferIPv6), - strings.ToUpper(config.TypePreferOnlyIPv4), - strings.ToUpper(config.TypePreferOnlyIPv6), - strings.ToLower(config.TypePreferIPPreferIPv4), - strings.ToLower(config.TypePreferIPPreferIPv6), - strings.ToLower(config.TypePreferOnlyIPv4), - strings.ToLower(config.TypePreferOnlyIPv6), + strings.ToTitle(config.TypePreferOnlyIPv4), + strings.ToTitle(config.TypePreferOnlyIPv6), + strings.ToTitle(config.TypePreferIPPreferIPv4), + strings.ToTitle(config.TypePreferIPPreferIPv6), } for _, v := range testData { @@ -70,11 +60,8 @@ func (suite *TypePreferIPTestSuite) TestUnmarshalOk() { suite.T().Run(v, func(t *testing.T) { testStruct := &typePreferIPTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, - strings.ToLower(value), - testStruct.Value.Value(config.TypePreferIPPreferIPv4)) + assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) }) } } @@ -85,55 +72,39 @@ func (suite *TypePreferIPTestSuite) TestMarshalOk() { config.TypePreferIPPreferIPv6, config.TypePreferOnlyIPv4, config.TypePreferOnlyIPv6, - strings.ToUpper(config.TypePreferIPPreferIPv4), - strings.ToUpper(config.TypePreferIPPreferIPv6), - strings.ToUpper(config.TypePreferOnlyIPv4), - strings.ToUpper(config.TypePreferOnlyIPv6), - strings.ToLower(config.TypePreferIPPreferIPv4), - strings.ToLower(config.TypePreferIPPreferIPv6), - strings.ToLower(config.TypePreferOnlyIPv4), - strings.ToLower(config.TypePreferOnlyIPv6), } for _, v := range testData { value := v - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - suite.T().Run(v, func(t *testing.T) { - testStruct := &typePreferIPTestStruct{} + testStruct := &typePreferIPTestStruct{ + Value: config.TypePreferIP{ + Value: value, + }, + } - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, strings.ToLower(value), testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() + encodedJSON, err := json.Marshal(testStruct) assert.NoError(t, err) - assert.Equal(t, strings.ToLower(value), string(marshalled)) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) }) } } -func (suite *TypePreferIPTestSuite) TestValue() { - testStruct := &typePreferIPTestStruct{} +func (suite *TypePreferIPTestSuite) TestGet() { + value := config.TypePreferIP{} + suite.Equal(config.TypePreferIPPreferIPv4, + value.Get(config.TypePreferIPPreferIPv4)) - suite.EqualValues(config.TypePreferIPPreferIPv4, - testStruct.Value.Value(config.TypePreferIPPreferIPv4)) - suite.EqualValues(config.TypePreferIPPreferIPv6, - testStruct.Value.Value(config.TypePreferIPPreferIPv6)) - - data, err := json.Marshal(map[string]string{ - "value": config.TypePreferOnlyIPv4, - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.EqualValues(config.TypePreferOnlyIPv4, - testStruct.Value.Value(config.TypePreferOnlyIPv6)) - suite.EqualValues(config.TypePreferOnlyIPv4, - testStruct.Value.Value(config.TypePreferIPPreferIPv6)) + suite.NoError(value.Set(config.TypePreferIPPreferIPv6)) + suite.Equal(config.TypePreferIPPreferIPv6, + value.Get(config.TypePreferIPPreferIPv4)) } func TestTypePreferIP(t *testing.T) { diff --git a/internal/config/type_proxy_url.go b/internal/config/type_proxy_url.go new file mode 100644 index 0000000..f3d0231 --- /dev/null +++ b/internal/config/type_proxy_url.go @@ -0,0 +1,61 @@ +package config + +import ( + "fmt" + "net" + "net/url" +) + +const typeProxyURLDefaultSOCKS5Port = "1080" + +type TypeProxyURL struct { + Value *url.URL +} + +func (t *TypeProxyURL) Set(value string) error { + parsedURL, err := url.Parse(value) + if err != nil { + return fmt.Errorf("value is not corect URL (%s): %w", value, err) + } + + if parsedURL.Host == "" { + return fmt.Errorf("url has to have a schema: %s", value) + } + + if parsedURL.Scheme != "socks5" { + return fmt.Errorf("unsupported schema: %s", parsedURL.Scheme) + } + + if _, _, err := net.SplitHostPort(parsedURL.Host); err != nil { + parsedURL.Host = net.JoinHostPort(parsedURL.Host, + typeProxyURLDefaultSOCKS5Port) + } + + t.Value = parsedURL + + return nil +} + +func (t *TypeProxyURL) Get(defaultValue *url.URL) *url.URL { + if t.Value == nil { + return defaultValue + } + + return t.Value +} + +func (t *TypeProxyURL) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeProxyURL) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeProxyURL) String() string { + if t.Value == nil { + return "" + } + + return t.Value.String() +} diff --git a/internal/config/type_proxy_url_test.go b/internal/config/type_proxy_url_test.go new file mode 100644 index 0000000..4bbe042 --- /dev/null +++ b/internal/config/type_proxy_url_test.go @@ -0,0 +1,96 @@ +package config_test + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/9seconds/mtg/v2/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeProxyURLTestStruct struct { + Value config.TypeProxyURL `json:"value"` +} + +type ProxyURLTestSuite struct { + suite.Suite +} + +func (suite *ProxyURLTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "socks5://", + "://lala", + "/path", + } + + for _, v := range testData { + data, err := json.Marshal(map[string]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeProxyURLTestStruct{})) + }) + } +} + +func (suite *ProxyURLTestSuite) TestUnmarshalOk() { + testData := map[string]string{ + "socks5://127.0.0.1/?open_threshold=1": "socks5://127.0.0.1:1080/?open_threshold=1", + "socks5://127.0.0.1:80": "socks5://127.0.0.1:80", + } + + for k, v := range testData { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeProxyURLTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + + parsed, _ := url.Parse(value) + + assert.Equal(t, parsed.Scheme, testStruct.Value.Get(nil).Scheme) + assert.Equal(t, parsed.Host, testStruct.Value.Get(nil).Host) + assert.Equal(t, parsed.RawQuery, testStruct.Value.Get(nil).RawQuery) + assert.Equal(t, parsed.Path, testStruct.Value.Get(nil).Path) + }) + } +} + +func (suite *ProxyURLTestSuite) TestMarshalOk() { + parsed, _ := url.Parse("socks5://127.0.0.1:1080?open_threshold=1") + testStruct := &typeProxyURLTestStruct{ + Value: config.TypeProxyURL{ + Value: parsed, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "socks5://127.0.0.1:1080?open_threshold=1"}`, + string(encodedJSON)) +} + +func (suite *ProxyURLTestSuite) TestGet() { + emptyURL := &url.URL{} + + value := config.TypeProxyURL{} + suite.Equal(emptyURL, value.Get(emptyURL)) + + value.Value = &url.URL{} + suite.Equal(value.Value, value.Get(emptyURL)) +} + +func TestTypeProxyURL(t *testing.T) { + t.Parallel() + suite.Run(t, &ProxyURLTestSuite{}) +} diff --git a/internal/config/type_statsd_tag_format.go b/internal/config/type_statsd_tag_format.go index 51ed34b..5301903 100644 --- a/internal/config/type_statsd_tag_format.go +++ b/internal/config/type_statsd_tag_format.go @@ -20,38 +20,39 @@ const ( ) type TypeStatsdTagFormat struct { - value string + Value string } -func (c *TypeStatsdTagFormat) UnmarshalText(data []byte) error { - if len(data) == 0 { +func (t *TypeStatsdTagFormat) Set(value string) error { + lowercasedValue := strings.ToLower(value) + + switch lowercasedValue { + case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb, + TypeStatsdTagFormatGraphite: + t.Value = lowercasedValue + return nil - } - - text := strings.ToLower(string(data)) - - switch text { - case TypeStatsdTagFormatInfluxdb, TypeStatsdTagFormatDatadog, TypeStatsdTagFormatGraphite: - c.value = text default: - return fmt.Errorf("incorrect tag format value: %s", string(data)) + return fmt.Errorf("unknown tag format %s", value) } - - return nil } -func (c TypeStatsdTagFormat) MarshalText() ([]byte, error) { - return []byte(c.value), nil -} - -func (c *TypeStatsdTagFormat) String() string { - return c.value -} - -func (c *TypeStatsdTagFormat) Value(defaultValue string) string { - if c.value == "" { +func (t TypeStatsdTagFormat) Get(defaultValue string) string { + if t.Value == "" { return defaultValue } - return c.value + return t.Value +} + +func (t *TypeStatsdTagFormat) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t *TypeStatsdTagFormat) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t *TypeStatsdTagFormat) String() string { + return t.Value } diff --git a/internal/config/type_statsd_tag_format_test.go b/internal/config/type_statsd_tag_format_test.go index b639ed4..455d90f 100644 --- a/internal/config/type_statsd_tag_format_test.go +++ b/internal/config/type_statsd_tag_format_test.go @@ -14,22 +14,14 @@ type typeStatsdTagFormatTestStruct struct { Value config.TypeStatsdTagFormat `json:"value"` } -type TypeStatsdTagFormatTestSuite struct { +type StatsdTagFormatTestSuite struct { suite.Suite } -func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalNil() { - typ := &config.TypeStatsdTagFormat{} - suite.NoError(typ.UnmarshalText(nil)) - suite.Equal("lalala", typ.Value("lalala")) -} - -func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalFail() { +func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() { testData := []string{ - "p", - "ipv4", - "onlyipv4", - "ipv6prefer", + "", + "dogdog", } for _, v := range testData { @@ -44,17 +36,14 @@ func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalFail() { } } -func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalOk() { +func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() { testData := []string{ - config.TypeStatsdTagFormatDatadog, config.TypeStatsdTagFormatInfluxdb, config.TypeStatsdTagFormatGraphite, - strings.ToUpper(config.TypeStatsdTagFormatDatadog), + config.TypeStatsdTagFormatDatadog, strings.ToUpper(config.TypeStatsdTagFormatInfluxdb), strings.ToUpper(config.TypeStatsdTagFormatGraphite), - strings.ToLower(config.TypeStatsdTagFormatDatadog), - strings.ToLower(config.TypeStatsdTagFormatInfluxdb), - strings.ToLower(config.TypeStatsdTagFormatGraphite), + strings.ToUpper(config.TypeStatsdTagFormatDatadog), } for _, v := range testData { @@ -67,70 +56,53 @@ func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalOk() { suite.T().Run(v, func(t *testing.T) { testStruct := &typeStatsdTagFormatTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, - strings.ToLower(value), - testStruct.Value.Value(config.TypeStatsdTagFormatDatadog)) + assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) }) } } -func (suite *TypeStatsdTagFormatTestSuite) TestMarshalOk() { +func (suite *StatsdTagFormatTestSuite) TestMarshalOk() { testData := []string{ - config.TypeStatsdTagFormatDatadog, config.TypeStatsdTagFormatInfluxdb, config.TypeStatsdTagFormatGraphite, - strings.ToUpper(config.TypeStatsdTagFormatDatadog), - strings.ToUpper(config.TypeStatsdTagFormatInfluxdb), - strings.ToUpper(config.TypeStatsdTagFormatGraphite), - strings.ToLower(config.TypeStatsdTagFormatDatadog), - strings.ToLower(config.TypeStatsdTagFormatInfluxdb), - strings.ToLower(config.TypeStatsdTagFormatGraphite), + config.TypeStatsdTagFormatDatadog, } for _, v := range testData { value := v - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - suite.T().Run(v, func(t *testing.T) { - testStruct := &typeStatsdTagFormatTestStruct{} + testStruct := &typeStatsdTagFormatTestStruct{ + Value: config.TypeStatsdTagFormat{ + Value: value, + }, + } - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, strings.ToLower(value), testStruct.Value.String()) - - marshalled, err := testStruct.Value.MarshalText() + encodedJSON, err := json.Marshal(testStruct) assert.NoError(t, err) - assert.Equal(t, strings.ToLower(value), string(marshalled)) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) }) } } -func (suite *TypeStatsdTagFormatTestSuite) TestValue() { - testStruct := &typePreferIPTestStruct{} +func (suite *StatsdTagFormatTestSuite) TestGet() { + value := config.TypeStatsdTagFormat{} + suite.Equal(config.TypeStatsdTagFormatDatadog, + value.Get(config.TypeStatsdTagFormatDatadog)) - suite.EqualValues(config.TypePreferIPPreferIPv4, - testStruct.Value.Value(config.TypePreferIPPreferIPv4)) - suite.EqualValues(config.TypePreferIPPreferIPv6, - testStruct.Value.Value(config.TypePreferIPPreferIPv6)) - - data, err := json.Marshal(map[string]string{ - "value": config.TypePreferOnlyIPv4, - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.EqualValues(config.TypePreferOnlyIPv4, - testStruct.Value.Value(config.TypePreferOnlyIPv6)) - suite.EqualValues(config.TypePreferOnlyIPv4, - testStruct.Value.Value(config.TypePreferIPPreferIPv6)) + suite.NoError(value.Set(config.TypeStatsdTagFormatInfluxdb)) + suite.Equal(config.TypeStatsdTagFormatInfluxdb, + value.Get(config.TypeStatsdTagFormatDatadog)) } func TestTypeStatsdTagFormat(t *testing.T) { t.Parallel() - suite.Run(t, &TypeStatsdTagFormatTestSuite{}) + suite.Run(t, &StatsdTagFormatTestSuite{}) } diff --git a/internal/config/type_url.go b/internal/config/type_url.go deleted file mode 100644 index 9250492..0000000 --- a/internal/config/type_url.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import ( - "fmt" - "net" - "net/url" -) - -type TypeURL struct { - value *url.URL -} - -func (c *TypeURL) UnmarshalText(data []byte) error { // nolint: cyclop - if len(data) == 0 { - return nil - } - - value, err := url.Parse(string(data)) - if err != nil { - return fmt.Errorf("incorrect URL: %w", err) - } - - switch value.Scheme { - case "http", "https", "socks5": - case "": - return fmt.Errorf("url %s has to have a schema", value) - default: - return fmt.Errorf("unsupported schema %s", value.Scheme) - } - - if value.Host == "" { - return fmt.Errorf("url %s has to have a host", value) - } - - if _, _, err := net.SplitHostPort(value.Host); err != nil { - switch value.Scheme { - case "http": - value.Host = net.JoinHostPort(value.Host, "80") - case "https": - value.Host = net.JoinHostPort(value.Host, "443") - case "socks5": - value.Host = net.JoinHostPort(value.Host, "1080") - default: - return fmt.Errorf("cannot set a default port for %s", value) - } - } - - c.value = value - - return nil -} - -func (c *TypeURL) MarshalText() ([]byte, error) { - return []byte(c.String()), nil -} - -func (c TypeURL) String() string { - if c.value == nil { - return "" - } - - return c.value.String() -} - -func (c TypeURL) Value(defaultValue *url.URL) *url.URL { - if c.value == nil { - return defaultValue - } - - return c.value -} diff --git a/internal/config/type_url_test.go b/internal/config/type_url_test.go deleted file mode 100644 index 4288172..0000000 --- a/internal/config/type_url_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package config_test - -import ( - "encoding/json" - "net/url" - "testing" - - "github.com/9seconds/mtg/v2/internal/config" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeURLTestStruct struct { - Value config.TypeURL `json:"value"` -} - -type TypeURLTestSuite struct { - suite.Suite -} - -func (suite *TypeURLTestSuite) TestUnmarshalNil() { - u, _ := url.Parse("https://google.com") - - typ := &config.TypeURL{} - suite.NoError(typ.UnmarshalText(nil)) - suite.Empty(typ.String()) - suite.Equal("https://google.com", typ.Value(u).String()) -} - -func (suite *TypeURLTestSuite) TestUnmarshalFail() { - testData := []string{ - "http:/aaa.com", - "ipv4", - "111", - "://111", - "http://aaa.com:xxx", - "gopher://aaa.com:888", - "gopher://aaa.com", - } - - for _, v := range testData { - data, err := json.Marshal(map[string]string{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(v, func(t *testing.T) { - assert.Error(t, json.Unmarshal(data, &typeURLTestStruct{})) - }) - } -} - -func (suite *TypeURLTestSuite) TestUnmarshalOk() { - testData := map[string]string{ - "https://10.0.0.10:80": "https://10.0.0.10:80", - "https://10.0.0.10:443": "https://10.0.0.10", - "http://10.0.0.10:8": "http://10.0.0.10:8", - "http://10.0.0.10:80": "http://10.0.0.10", - "socks5://10.0.0.10:1080": "socks5://10.0.0.10", - "socks5://10.0.0.10:888": "socks5://10.0.0.10:888", - } - - for k, v := range testData { - expected := k - actual := v - - data, err := json.Marshal(map[string]string{ - "value": actual, - }) - suite.NoError(err) - - suite.T().Run(actual, func(t *testing.T) { - testStruct := &typeURLTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, expected, testStruct.Value.Value(nil).String()) - - marshalled, err := testStruct.Value.MarshalText() - assert.NoError(t, err) - assert.Equal(t, expected, string(marshalled)) - }) - } -} - -func (suite *TypeURLTestSuite) TestValue() { - testStruct := &typeURLTestStruct{} - - u1, _ := url.Parse("https://10.0.0.10:80") - u2, _ := url.Parse("https://10.1.0.10:80") - - suite.Equal("https://10.0.0.10:80", testStruct.Value.Value(u1).String()) - suite.Equal("https://10.1.0.10:80", testStruct.Value.Value(u2).String()) - - data, err := json.Marshal(map[string]string{ - "value": "http://127.0.0.1:80", - }) - suite.NoError(err) - suite.NoError(json.Unmarshal(data, testStruct)) - - suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u1).String()) - suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u2).String()) -} - -func TestTypeURL(t *testing.T) { - t.Parallel() - suite.Run(t, &TypeURLTestSuite{}) -} diff --git a/internal/utils/make_qr_code_url.go b/internal/utils/make_qr_code_url.go new file mode 100644 index 0000000..fd140fb --- /dev/null +++ b/internal/utils/make_qr_code_url.go @@ -0,0 +1,19 @@ +package utils + +import "net/url" + +func MakeQRCodeURL(data string) string { + values := url.Values{} + values.Set("qzone", "4") + values.Set("format", "svg") + values.Set("data", data) + + rv := url.URL{ + Scheme: "https", + Host: "api.qrserver.com", + Path: "/v1/create-qr-code", + RawQuery: values.Encode(), + } + + return rv.String() +} diff --git a/internal/utils/make_qr_code_url_test.go b/internal/utils/make_qr_code_url_test.go new file mode 100644 index 0000000..cea1f91 --- /dev/null +++ b/internal/utils/make_qr_code_url_test.go @@ -0,0 +1,31 @@ +package utils_test + +import ( + "net/url" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/utils" + "github.com/stretchr/testify/suite" +) + +type MakeQRCodeURLTestSuite struct { + suite.Suite +} + +func (suite *MakeQRCodeURLTestSuite) TestSomeData() { + value := utils.MakeQRCodeURL("some data") + + parsed, err := url.Parse(value) + suite.NoError(err) + + suite.Equal("some data", parsed.Query().Get("data")) + suite.Equal("svg", parsed.Query().Get("format")) + suite.Equal("api.qrserver.com", strings.TrimPrefix(parsed.Host, "www.")) + suite.Equal("v1/create-qr-code", strings.Trim(parsed.Path, "/")) +} + +func TestMakeQRCodeURL(t *testing.T) { + t.Parallel() + suite.Run(t, &MakeQRCodeURLTestSuite{}) +} diff --git a/internal/utils/read_config.go b/internal/utils/read_config.go new file mode 100644 index 0000000..eee7b98 --- /dev/null +++ b/internal/utils/read_config.go @@ -0,0 +1,26 @@ +package utils + +import ( + "fmt" + "os" + + "github.com/9seconds/mtg/v2/internal/config" +) + +func ReadConfig(path string) (*config.Config, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read config file: %w", err) + } + + conf, err := config.Parse(content) + if err != nil { + return nil, fmt.Errorf("cannot parse config: %w", err) + } + + if err := conf.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + return conf, nil +} diff --git a/internal/utils/read_config_test.go b/internal/utils/read_config_test.go new file mode 100644 index 0000000..7ba06b0 --- /dev/null +++ b/internal/utils/read_config_test.go @@ -0,0 +1,55 @@ +package utils_test + +import ( + "path/filepath" + "testing" + + "github.com/9seconds/mtg/v2/internal/utils" + "github.com/stretchr/testify/suite" +) + +type ReadConfigTestSuite struct { + suite.Suite +} + +func (suite *ReadConfigTestSuite) GetConfigPath(filename string) string { + return filepath.Join("testdata", filename) +} + +func (suite *ReadConfigTestSuite) TestReadMinimal() { + conf, err := utils.ReadConfig(suite.GetConfigPath("minimal.toml")) + suite.NoError(err) + suite.NoError(conf.Validate()) + suite.Equal("0.0.0.0:80", conf.BindTo.Get("")) + suite.Equal("7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t", conf.Secret.Base64()) +} + +func (suite *ReadConfigTestSuite) TestReadAbsentFile() { + _, err := utils.ReadConfig(suite.GetConfigPath("unknown.file")) + suite.Error(err) +} + +func (suite *ReadConfigTestSuite) TestBrokenFile() { + _, err := utils.ReadConfig(suite.GetConfigPath("broken.toml")) + suite.Error(err) +} + +func (suite *ReadConfigTestSuite) TestMissedBindTo() { + _, err := utils.ReadConfig(suite.GetConfigPath("missed-bindto.toml")) + suite.Error(err) +} + +func (suite *ReadConfigTestSuite) TestMissedSecret() { + _, err := utils.ReadConfig(suite.GetConfigPath("missed-secret.toml")) + suite.Error(err) +} + +func (suite *ReadConfigTestSuite) TestEmpty() { + _, err := utils.ReadConfig(suite.GetConfigPath("empty.toml")) + suite.Error(err) +} + +func TestReadConfig(t *testing.T) { + t.Parallel() + suite.Run(t, &ReadConfigTestSuite{}) +} diff --git a/internal/utils/testdata/broken.toml b/internal/utils/testdata/broken.toml new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/internal/utils/testdata/broken.toml @@ -0,0 +1 @@ +11 diff --git a/internal/utils/testdata/empty.toml b/internal/utils/testdata/empty.toml new file mode 100644 index 0000000..e69de29 diff --git a/internal/cli/testdata/minimal.toml b/internal/utils/testdata/minimal.toml similarity index 100% rename from internal/cli/testdata/minimal.toml rename to internal/utils/testdata/minimal.toml diff --git a/internal/utils/testdata/missed-bindto.toml b/internal/utils/testdata/missed-bindto.toml new file mode 100644 index 0000000..82cce1b --- /dev/null +++ b/internal/utils/testdata/missed-bindto.toml @@ -0,0 +1,2 @@ + +secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t" diff --git a/internal/utils/testdata/missed-secret.toml b/internal/utils/testdata/missed-secret.toml new file mode 100644 index 0000000..90e9bb0 --- /dev/null +++ b/internal/utils/testdata/missed-secret.toml @@ -0,0 +1 @@ +bind-to = "0.0.0.0:80" diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index dc9b57b..2eac74e 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -55,15 +55,6 @@ type ProxyOpts struct { // This is an optional setting. Concurrency uint - // DomainFrontingPort is a port we use to connect to a fronting - // domain. - // - // This is required because secret does not specify a port. It - // specifies a hostname only. - // - // This is an optional setting. - DomainFrontingPort uint - // IdleTimeout is a timeout for relay when we have to break a // stream. // @@ -90,6 +81,15 @@ type ProxyOpts struct { // This is an optional setting. PreferIP string + // DomainFrontingPort is a port we use to connect to a fronting + // domain. + // + // This is required because secret does not specify a port. It + // specifies a hostname only. + // + // This is an optional setting. + DomainFrontingPort uint + // UseTestDCs defines if we have to connect to production or to staging // DCs of Telegram. // diff --git a/mtglib/secret.go b/mtglib/secret.go index 49fd106..eced181 100644 --- a/mtglib/secret.go +++ b/mtglib/secret.go @@ -58,7 +58,10 @@ func (s Secret) MarshalText() ([]byte, error) { // UnmarshalText is to support text.Unmarshaller interface. func (s *Secret) UnmarshalText(data []byte) error { - text := string(data) + return s.Set(string(data)) +} + +func (s *Secret) Set(text string) error { if text == "" { return ErrSecretEmpty }