From ec4f0656fb7cce2de8b57e848b1eb03c7b520014 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Thu, 22 Jul 2021 13:32:25 +0300 Subject: [PATCH 1/8] Add updated version of config --- internal/config2/config.go | 81 +++++++++++++ internal/config2/config_test.go | 54 +++++++++ internal/config2/parse.go | 80 +++++++++++++ internal/config2/testdata/broken.toml | 1 + internal/config2/testdata/minimal.toml | 2 + internal/config2/testdata/only_secret.toml | 1 + internal/config2/type_blocklist_uri.go | 77 ++++++++++++ internal/config2/type_blocklist_uri_test.go | 113 ++++++++++++++++++ internal/config2/type_bool.go | 40 +++++++ internal/config2/type_bool_test.go | 113 ++++++++++++++++++ internal/config2/type_bytes.go | 55 +++++++++ internal/config2/type_bytes_test.go | 86 +++++++++++++ internal/config2/type_concurrency.go | 45 +++++++ internal/config2/type_concurrency_test.go | 73 +++++++++++ internal/config2/type_duration.go | 53 ++++++++ internal/config2/type_duration_test.go | 110 +++++++++++++++++ internal/config2/type_error_rate.go | 47 ++++++++ internal/config2/type_error_rate_test.go | 94 +++++++++++++++ internal/config2/type_hostport.go | 59 +++++++++ internal/config2/type_hostport_test.go | 88 ++++++++++++++ internal/config2/type_http_path.go | 33 +++++ internal/config2/type_http_path_test.go | 67 +++++++++++ internal/config2/type_ip.go | 45 +++++++ internal/config2/type_ip_test.go | 104 ++++++++++++++++ internal/config2/type_metric_prefix.go | 40 +++++++ internal/config2/type_metric_prefix_test.go | 70 +++++++++++ internal/config2/type_port.go | 45 +++++++ internal/config2/type_port_test.go | 71 +++++++++++ internal/config2/type_prefer_ip.go | 62 ++++++++++ internal/config2/type_prefer_ip_test.go | 113 ++++++++++++++++++ internal/config2/type_proxy_url.go | 61 ++++++++++ internal/config2/type_proxy_url_test.go | 95 +++++++++++++++ internal/config2/type_statsd_tag_format.go | 58 +++++++++ .../config2/type_statsd_tag_format_test.go | 108 +++++++++++++++++ 34 files changed, 2244 insertions(+) create mode 100644 internal/config2/config.go create mode 100644 internal/config2/config_test.go create mode 100644 internal/config2/parse.go create mode 100644 internal/config2/testdata/broken.toml create mode 100644 internal/config2/testdata/minimal.toml create mode 100644 internal/config2/testdata/only_secret.toml create mode 100644 internal/config2/type_blocklist_uri.go create mode 100644 internal/config2/type_blocklist_uri_test.go create mode 100644 internal/config2/type_bool.go create mode 100644 internal/config2/type_bool_test.go create mode 100644 internal/config2/type_bytes.go create mode 100644 internal/config2/type_bytes_test.go create mode 100644 internal/config2/type_concurrency.go create mode 100644 internal/config2/type_concurrency_test.go create mode 100644 internal/config2/type_duration.go create mode 100644 internal/config2/type_duration_test.go create mode 100644 internal/config2/type_error_rate.go create mode 100644 internal/config2/type_error_rate_test.go create mode 100644 internal/config2/type_hostport.go create mode 100644 internal/config2/type_hostport_test.go create mode 100644 internal/config2/type_http_path.go create mode 100644 internal/config2/type_http_path_test.go create mode 100644 internal/config2/type_ip.go create mode 100644 internal/config2/type_ip_test.go create mode 100644 internal/config2/type_metric_prefix.go create mode 100644 internal/config2/type_metric_prefix_test.go create mode 100644 internal/config2/type_port.go create mode 100644 internal/config2/type_port_test.go create mode 100644 internal/config2/type_prefer_ip.go create mode 100644 internal/config2/type_prefer_ip_test.go create mode 100644 internal/config2/type_proxy_url.go create mode 100644 internal/config2/type_proxy_url_test.go create mode 100644 internal/config2/type_statsd_tag_format.go create mode 100644 internal/config2/type_statsd_tag_format_test.go diff --git a/internal/config2/config.go b/internal/config2/config.go new file mode 100644 index 0000000..1b89c78 --- /dev/null +++ b/internal/config2/config.go @@ -0,0 +1,81 @@ +package config2 + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/9seconds/mtg/v2/mtglib" +) + +type Config struct { + 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 TypeBool `json:"enabled"` + MaxSize TypeBytes `json:"maxSize"` + ErrorRate TypeErrorRate `json:"errorRate"` + } `json:"antiReplay"` + Blocklist struct { + Enabled TypeBool `json:"enabled"` + DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"` + URLs []TypeBlocklistURI `json:"urls"` + UpdateEach TypeDuration `json:"updateEach"` + } `json:"blocklist"` + } `json:"defense"` + Network struct { + Timeout struct { + TCP TypeDuration `json:"tcp"` + HTTP TypeDuration `json:"http"` + Idle TypeDuration `json:"idle"` + } `json:"timeout"` + DOHIP TypeIP `json:"dohIp"` + Proxies []TypeProxyURL `json:"proxies"` + } `json:"network"` + Stats struct { + StatsD struct { + Enabled TypeBool `json:"enabled"` + Address TypeHostPort `json:"address"` + MetricPrefix TypeMetricPrefix `json:"metricPrefix"` + TagFormat TypeStatsdTagFormat `json:"tagFormat"` + } `json:"statsd"` + Prometheus struct { + Enabled TypeBool `json:"enabled"` + BindTo TypeHostPort `json:"bindTo"` + HTTPPath TypeHTTPPath `json:"httpPath"` + MetricPrefix TypeMetricPrefix `json:"metricPrefix"` + } `json:"prometheus"` + } `json:"stats"` +} + +func (c *Config) Validate() error { + if !c.Secret.Valid() { + return fmt.Errorf("invalid secret %s", c.Secret.String()) + } + + if c.BindTo.Get("") == "" { + return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String()) + } + + return nil +} + +func (c *Config) String() string { + buf := &bytes.Buffer{} + encoder := json.NewEncoder(buf) + + encoder.SetEscapeHTML(false) + + if err := encoder.Encode(c); err != nil { + panic(err) + } + + return buf.String() +} diff --git a/internal/config2/config_test.go b/internal/config2/config_test.go new file mode 100644 index 0000000..76d32e7 --- /dev/null +++ b/internal/config2/config_test.go @@ -0,0 +1,54 @@ +package config2_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/suite" +) + +type ConfigTestSuite struct { + suite.Suite +} + +func (suite *ConfigTestSuite) ReadConfig(filename string) []byte { + data, err := os.ReadFile(filepath.Join("testdata", filename)) + suite.NoError(err) + + return data +} + +func (suite *ConfigTestSuite) TestParseEmpty() { + _, err := config2.Parse([]byte{}) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParseBrokenToml() { + _, err := config2.Parse(suite.ReadConfig("broken.toml")) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParseOnlySecret() { + _, err := config2.Parse(suite.ReadConfig("only_secret.toml")) + suite.Error(err) +} + +func (suite *ConfigTestSuite) TestParseMinimalConfig() { + conf, err := config2.Parse(suite.ReadConfig("minimal.toml")) + suite.NoError(err) + suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64()) + suite.Equal("0.0.0.0:3128", conf.BindTo.String()) +} + +func (suite *ConfigTestSuite) TestString() { + conf, err := config2.Parse(suite.ReadConfig("minimal.toml")) + suite.NoError(err) + suite.NotEmpty(conf.String()) +} + +func TestConfig(t *testing.T) { + t.Parallel() + suite.Run(t, &ConfigTestSuite{}) +} diff --git a/internal/config2/parse.go b/internal/config2/parse.go new file mode 100644 index 0000000..c63c762 --- /dev/null +++ b/internal/config2/parse.go @@ -0,0 +1,80 @@ +package config2 + +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/config2/testdata/broken.toml b/internal/config2/testdata/broken.toml new file mode 100644 index 0000000..d95f791 --- /dev/null +++ b/internal/config2/testdata/broken.toml @@ -0,0 +1 @@ +s = sdfsdfds diff --git a/internal/config2/testdata/minimal.toml b/internal/config2/testdata/minimal.toml new file mode 100644 index 0000000..9d0961a --- /dev/null +++ b/internal/config2/testdata/minimal.toml @@ -0,0 +1,2 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" +bind-to = "0.0.0.0:3128" diff --git a/internal/config2/testdata/only_secret.toml b/internal/config2/testdata/only_secret.toml new file mode 100644 index 0000000..f6b0bee --- /dev/null +++ b/internal/config2/testdata/only_secret.toml @@ -0,0 +1 @@ +secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" diff --git a/internal/config2/type_blocklist_uri.go b/internal/config2/type_blocklist_uri.go new file mode 100644 index 0000000..f73ee3c --- /dev/null +++ b/internal/config2/type_blocklist_uri.go @@ -0,0 +1,77 @@ +package config2 + +import ( + "fmt" + "net/url" + "os" + "path/filepath" +) + +type TypeBlocklistURI struct { + Value string +} + +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") + } + + 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(value) + if err != nil { + return fmt.Errorf("incorrect url (%s): %w", value, err) + } + + switch parsedURL.Scheme { + case "http", "https": + default: + return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value) + } + + if parsedURL.Host == "" { + return fmt.Errorf("incorrect url %s", value) + } + + t.Value = parsedURL.String() + + return nil +} + +func (t TypeBlocklistURI) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + 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/config2/type_blocklist_uri_test.go b/internal/config2/type_blocklist_uri_test.go new file mode 100644 index 0000000..cfecc8a --- /dev/null +++ b/internal/config2/type_blocklist_uri_test.go @@ -0,0 +1,113 @@ +package config2_test + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBlocklistURITestStruct struct { + Value config2.TypeBlocklistURI `json:"value"` +} + +type TypeBlocklistURITestSuite struct { + suite.Suite + + directory string + absDirectory string +} + +func (suite *TypeBlocklistURITestSuite) SetupSuite() { + dir, _ := os.Getwd() + absDir, _ := filepath.Abs(dir) + + suite.directory = dir + suite.absDirectory = absDir +} + +func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() { + testData := []string{ + "gopher://lalala", + "https:///paths", + "h:/=", + filepath.Join(suite.directory, "___"), + filepath.Join(suite.absDirectory, "___"), + suite.directory, + suite.absDirectory, + } + + 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, &typeBlocklistURITestStruct{})) + }) + } +} + +func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() { + testData := []string{ + "http://lalala", + "https://lalala", + "https://lalala/path", + filepath.Join(suite.directory, "config.go"), + filepath.Join(suite.absDirectory, "config.go"), + } + + 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 := &typeBlocklistURITestStruct{} + + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.EqualValues(t, value, testStruct.Value.Get("")) + + if strings.HasPrefix(value, "http") { + assert.True(t, testStruct.Value.IsRemote()) + } else { + assert.False(t, testStruct.Value.IsRemote()) + } + }) + } +} + +func (suite *TypeBlocklistURITestSuite) TestMarshalOk() { + testStruct := &typeBlocklistURITestStruct{ + Value: config2.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 := config2.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/config2/type_bool.go b/internal/config2/type_bool.go new file mode 100644 index 0000000..490d610 --- /dev/null +++ b/internal/config2/type_bool.go @@ -0,0 +1,40 @@ +package config2 + +import ( + "fmt" + "strconv" + "strings" +) + +type TypeBool struct { + Value bool +} + +func (t *TypeBool) Set(data string) error { + switch strings.ToLower(data) { + case "1", "y", "yes", "enabled", "true": + t.Value = true + case "0", "n", "no", "disabled", "false": + t.Value = false + default: + return fmt.Errorf("incorrect bool value %s", data) + } + + return nil +} + +func (t TypeBool) Get(defaultValue bool) bool { + return t.Value || defaultValue +} + +func (t *TypeBool) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeBool) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeBool) String() string { + return strconv.FormatBool(t.Value) +} diff --git a/internal/config2/type_bool_test.go b/internal/config2/type_bool_test.go new file mode 100644 index 0000000..0fa3b09 --- /dev/null +++ b/internal/config2/type_bool_test.go @@ -0,0 +1,113 @@ +package config2_test + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBoolTestStruct struct { + Value config2.TypeBool `json:"value"` +} + +type TypeBoolTestSuite struct { + suite.Suite +} + +func (suite *TypeBoolTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "np", + "нет", + } + + 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, &typeBoolTestStruct{})) + }) + } +} + +func (suite *TypeBoolTestSuite) TestUnmarshalOk() { + testData := map[string]bool{ + "0": false, + "N": false, + "nO": false, + "no": false, + "dISAbLEd": false, + "False": false, + "false": false, + + "1": true, + "y": true, + "Yes": true, + "yes": true, + "enABLED": true, + "True": true, + "TRUE": true, + "true": true, + } + + 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 := &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} { + name := strconv.FormatBool(v) + + suite.T().Run(name, func(t *testing.T) { + testStruct := typeBoolTestStruct{ + Value: config2.TypeBool{ + Value: v, + }, + } + + data, err := json.Marshal(testStruct) + assert.NoError(t, err) + assert.JSONEq(t, fmt.Sprintf(`{"value": "%s"}`, name), string(data)) + }) + } +} + +func (suite *TypeBoolTestSuite) TestGet() { + value := config2.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/config2/type_bytes.go b/internal/config2/type_bytes.go new file mode 100644 index 0000000..789ab54 --- /dev/null +++ b/internal/config2/type_bytes.go @@ -0,0 +1,55 @@ +package config2 + +import ( + "fmt" + "strings" + + "github.com/alecthomas/units" +) + +var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB") + +type TypeBytes struct { + Value units.Base2Bytes +} + +func (t *TypeBytes) Set(value string) error { + normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) + + parsedValue, err := units.ParseBase2Bytes(normalizedValue) + if err != nil { + return fmt.Errorf("incorrect bytes value (%v): %w", value, err) + } + + if parsedValue < 0 { + return fmt.Errorf("bytes should be positive (%s)", value) + } + + t.Value = parsedValue + + return nil +} + +func (t TypeBytes) Get(defaultValue uint) uint { + if t.Value == 0 { + return defaultValue + } + + 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/config2/type_bytes_test.go b/internal/config2/type_bytes_test.go new file mode 100644 index 0000000..c153fb2 --- /dev/null +++ b/internal/config2/type_bytes_test.go @@ -0,0 +1,86 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeBytesTestStruct struct { + Value config2.TypeBytes `json:"value"` +} + +type TypeBytesTestSuite struct { + suite.Suite +} + +func (suite *TypeBytesTestSuite) TestUnmarshalFail() { + testData := []string{ + "1m", + "1", + "-1kb", + "-1kib", + } + + 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, &typeBytesTestStruct{})) + }) + } +} + +func (suite *TypeBytesTestSuite) TestUnmarshalOk() { + testData := map[string]uint{ + "1b": 1, + "1kb": 1024, + "1kib": 1024, + "2mb": 2 * 1024 * 1024, + "2mib": 2 * 1024 * 1024, + } + + 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 := &typeBytesTestStruct{} + + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.EqualValues(t, value, testStruct.Value.Get(0)) + }) + } +} + +func (suite *TypeBytesTestSuite) TestMarshalOk() { + value := typeBytesTestStruct{} + suite.NoError(value.Value.Set("1kib")) + + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "1kib"}`, string(data)) +} + +func (suite *TypeBytesTestSuite) TestGet() { + value := config2.TypeBytes{} + suite.EqualValues(1000, value.Get(1000)) + + suite.NoError(value.Set("1mib")) + suite.EqualValues(1048576, value.Get(1000)) +} + +func TestTypeBytes(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeBytesTestSuite{}) +} diff --git a/internal/config2/type_concurrency.go b/internal/config2/type_concurrency.go new file mode 100644 index 0000000..e54f8ad --- /dev/null +++ b/internal/config2/type_concurrency.go @@ -0,0 +1,45 @@ +package config2 + +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/config2/type_concurrency_test.go b/internal/config2/type_concurrency_test.go new file mode 100644 index 0000000..f7227a5 --- /dev/null +++ b/internal/config2/type_concurrency_test.go @@ -0,0 +1,73 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeConcurrencyTestStruct struct { + Value config2.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: config2.TypeConcurrency{ + Value: 2, + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": 2}`, string(data)) +} + +func (suite *TypeConcurrencyTestSuite) TestGet() { + value := config2.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/config2/type_duration.go b/internal/config2/type_duration.go new file mode 100644 index 0000000..85db148 --- /dev/null +++ b/internal/config2/type_duration.go @@ -0,0 +1,53 @@ +package config2 + +import ( + "fmt" + "strings" + "time" +) + +var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "") + +type TypeDuration struct { + Value time.Duration +} + +func (t *TypeDuration) Set(value string) error { + parsedValue, err := time.ParseDuration( + typeDurationStringCleaner.Replace(strings.ToLower(value))) + if err != nil { + return fmt.Errorf("incorrect duration (%s): %w", value, err) + } + + if parsedValue < 0 { + return fmt.Errorf("duration has to be a positive: %s", value) + } + + t.Value = parsedValue + + return nil +} + +func (t TypeDuration) Get(defaultValue time.Duration) time.Duration { + if t.Value == 0 { + return defaultValue + } + + 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/config2/type_duration_test.go b/internal/config2/type_duration_test.go new file mode 100644 index 0000000..3f9b21e --- /dev/null +++ b/internal/config2/type_duration_test.go @@ -0,0 +1,110 @@ +package config2_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeDurationTestStruct struct { + Value config2.TypeDuration `json:"value"` +} + +type TypeDurationTestSuite struct { + suite.Suite +} + +func (suite *TypeDurationTestSuite) TestUnmarshalFail() { + testData := []string{ + "-1s", + "1 seconds ago", + "1s ago", + "", + } + + 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, &typeDurationTestStruct{})) + }) + } +} + +func (suite *TypeDurationTestSuite) TestUnmarshalOk() { + testData := map[string]time.Duration{ + "1s": 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 { + value := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeDurationTestStruct{} + + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, value, testStruct.Value.Value) + }) + } +} + +func (suite *TypeDurationTestSuite) TestMarshalOk() { + testData := map[string]string{ + "1s": "1s", + "0": "", + "0s": "", + "0ms": "", + "1 H": "1h0m0s", + } + + for k, v := range testData { + value := k + expected := v + + suite.T().Run(value, func(t *testing.T) { + testStruct := &typeDurationTestStruct{} + + assert.NoError(t, testStruct.Value.Set(value)) + + data, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJson, err := json.Marshal(map[string]string{ + "value": expected, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJson), string(data)) + }) + } +} + +func (suite *TypeDurationTestSuite) TestGet() { + value := config2.TypeDuration{} + suite.Equal(time.Second, value.Get(time.Second)) + + value.Value = 3 * time.Second + suite.Equal(3*time.Second, value.Get(time.Hour)) +} + +func TestTypeDuration(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeDurationTestSuite{}) +} diff --git a/internal/config2/type_error_rate.go b/internal/config2/type_error_rate.go new file mode 100644 index 0000000..1ce8ecc --- /dev/null +++ b/internal/config2/type_error_rate.go @@ -0,0 +1,47 @@ +package config2 + +import ( + "fmt" + "strconv" +) + +const typeErrorRateIgnoreLess = 1e-8 + +type TypeErrorRate struct { + Value float64 +} + +func (t *TypeErrorRate) Set(value string) error { + parsedValue, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("Value is not a float (%s): %w", value, err) + } + + if parsedValue <= 0.0 || parsedValue >= 100.0 { + return fmt.Errorf("Value should be 0 < x < 100 (%s)", value) + } + + t.Value = parsedValue + + return nil +} + +func (t TypeErrorRate) Get(defaultValue float64) float64 { + if t.Value < typeErrorRateIgnoreLess { + return defaultValue + } + + return t.Value +} + +func (t *TypeErrorRate) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeErrorRate) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t TypeErrorRate) String() string { + return strconv.FormatFloat(t.Value, 'f', -1, 64) +} diff --git a/internal/config2/type_error_rate_test.go b/internal/config2/type_error_rate_test.go new file mode 100644 index 0000000..5d8590a --- /dev/null +++ b/internal/config2/type_error_rate_test.go @@ -0,0 +1,94 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeErrorRateTestStruct struct { + Value config2.TypeErrorRate `json:"value"` +} + +type TypeErrorRateTestSuite struct { + suite.Suite +} + +func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() { + 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]string{ + "value": v, + }) + suite.NoError(err) + + suite.T().Run(v, func(t *testing.T) { + assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{})) + }) + } +} + +func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() { + testData := map[string]float64{ + "1": 1.0, + "1.0": 1.0, + "0.5": 0.5, + ".5": 0.5, + } + + 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 := &typeErrorRateTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.InEpsilon(t, value, testStruct.Value.Value, 1e-10) + }) + } +} + +func (suite *TypeErrorRateTestSuite) TestMarshalOk() { + testStruct := typeErrorRateTestStruct{ + Value: config2.TypeErrorRate{ + Value: 1.01, + }, + } + + encodedJson, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "1.01"}`, string(encodedJson)) +} + +func (suite *TypeErrorRateTestSuite) TestGet() { + value := config2.TypeErrorRate{} + suite.InEpsilon(1.0, value.Get(1.0), 1e-10) + + value.Value = 5.0 + suite.InEpsilon(5.0, value.Get(1.0), 1e-10) +} + +func TestTypeErrorRate(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeErrorRateTestSuite{}) +} diff --git a/internal/config2/type_hostport.go b/internal/config2/type_hostport.go new file mode 100644 index 0000000..73d45c0 --- /dev/null +++ b/internal/config2/type_hostport.go @@ -0,0 +1,59 @@ +package config2 + +import ( + "fmt" + "net" + "strconv" +) + +type TypeHostPort struct { + Value string +} + +func (t *TypeHostPort) Set(value string) error { + host, port, err := net.SplitHostPort(value) + if err != nil { + return fmt.Errorf("incorrect host:port value (%v): %w", value, err) + } + + portValue, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return fmt.Errorf("incorrect port number (%v): %w", value, err) + } + + if portValue == 0 { + return fmt.Errorf("incorrect port number (%s)", value) + } + + 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) + + return nil +} + +func (t TypeHostPort) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + 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/config2/type_hostport_test.go b/internal/config2/type_hostport_test.go new file mode 100644 index 0000000..dfaf395 --- /dev/null +++ b/internal/config2/type_hostport_test.go @@ -0,0 +1,88 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeHostPortTestStruct struct { + Value config2.TypeHostPort `json:"value"` +} + +type TypeHostPortTestSuite struct { + suite.Suite +} + +func (suite *TypeHostPortTestSuite) TestUnmarshalFail() { + testData := []string{ + ":", + ":800", + "127.0.0.1:8000000", + "12...:80", + "", + "localhost", + "google.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, &typeHostPortTestStruct{})) + }) + } +} + +func (suite *TypeHostPortTestSuite) TestUnmarshalOk() { + testData := []string{ + "127.0.0.1:80", + "10.0.0.10:6553", + } + + 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.Value) + }) + } +} + +func (suite *TypeHostPortTestSuite) TestMarshalOk() { + testStruct := typeHostPortTestStruct{ + Value: config2.TypeHostPort{ + Value: "127.0.0.1:8000", + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data)) +} + +func (suite *TypeHostPortTestSuite) TestGet() { + value := config2.TypeHostPort{} + suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000")) + + 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) { + t.Parallel() + suite.Run(t, &TypeHostPortTestSuite{}) +} diff --git a/internal/config2/type_http_path.go b/internal/config2/type_http_path.go new file mode 100644 index 0000000..d31ac57 --- /dev/null +++ b/internal/config2/type_http_path.go @@ -0,0 +1,33 @@ +package config2 + +import "strings" + +type TypeHTTPPath struct { + Value string +} + +func (t *TypeHTTPPath) Set(value string) error { + t.Value = "/" + strings.Trim(value, "/") + + return nil +} + +func (t TypeHTTPPath) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + 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/config2/type_http_path_test.go b/internal/config2/type_http_path_test.go new file mode 100644 index 0000000..014907e --- /dev/null +++ b/internal/config2/type_http_path_test.go @@ -0,0 +1,67 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeHTTPPathTestStruct struct { + Value config2.TypeHTTPPath `json:"value"` +} + +type TypeHTTPPathTestSuite struct { + suite.Suite +} + +func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() { + testData := map[string]string{ + "": "/", + "/": "/", + "/path": "/path", + "path": "/path", + } + + 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 := &typeHTTPPathTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, value, testStruct.Value.Get("")) + }) + } +} + +func (suite *TypeHTTPPathTestSuite) TestMarshalOk() { + value := typeHTTPPathTestStruct{ + Value: config2.TypeHTTPPath{ + Value: "/path", + }, + } + + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "/path"}`, string(data)) +} + +func (suite *TypeHTTPPathTestSuite) TestGet() { + value := config2.TypeHTTPPath{} + suite.Equal("/hello", value.Get("/hello")) + + suite.NoError(value.Set("/lalala")) + suite.Equal("/lalala", value.Get("/hello")) +} + +func TestTypeHTTPPath(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeHTTPPathTestSuite{}) +} diff --git a/internal/config2/type_ip.go b/internal/config2/type_ip.go new file mode 100644 index 0000000..207d22e --- /dev/null +++ b/internal/config2/type_ip.go @@ -0,0 +1,45 @@ +package config2 + +import ( + "fmt" + "net" +) + +type TypeIP struct { + Value net.IP +} + +func (t *TypeIP) Set(value string) error { + ip := net.ParseIP(value) + if ip == nil { + return fmt.Errorf("incorret ip %s", value) + } + + t.Value = ip + + return nil +} + +func (t *TypeIP) Get(defaultValue net.IP) net.IP { + if len(t.Value) == 0 { + return defaultValue + } + + 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/config2/type_ip_test.go b/internal/config2/type_ip_test.go new file mode 100644 index 0000000..4659cb0 --- /dev/null +++ b/internal/config2/type_ip_test.go @@ -0,0 +1,104 @@ +package config2_test + +import ( + "encoding/json" + "net" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeIPTestStruct struct { + Value config2.TypeIP `json:"value"` +} + +type TypeIPTestSuite struct { + suite.Suite +} + +func (suite *TypeIPTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "....", + "0...", + "300.200.200.800", + "[]", + } + + 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, &typeIPTestStruct{})) + }) + } +} + +func (suite *TypeIPTestSuite) TestUnmarshalOk() { + 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 k, v := range testData { + expected := v + + data, err := json.Marshal(map[string]string{ + "value": k, + }) + suite.NoError(err) + + suite.T().Run(k, func(t *testing.T) { + testStruct := &typeIPTestStruct{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, expected, testStruct.Value.Get(nil).String()) + }) + } +} + +func (suite *TypeIPTestSuite) TestMarshalOk() { + testData := []string{ + "2001:db8:85a3::8a2e:370:7334", + "127.0.0.1", + } + + for _, v := range testData { + value := v + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeIPTestStruct{ + Value: config2.TypeIP{ + Value: net.ParseIP(value), + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *TypeIPTestSuite) TestGet() { + value := config2.TypeIP{} + suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).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) { + t.Parallel() + suite.Run(t, &TypeIPTestSuite{}) +} diff --git a/internal/config2/type_metric_prefix.go b/internal/config2/type_metric_prefix.go new file mode 100644 index 0000000..d8507de --- /dev/null +++ b/internal/config2/type_metric_prefix.go @@ -0,0 +1,40 @@ +package config2 + +import ( + "fmt" + "regexp" +) + +type TypeMetricPrefix struct { + Value string +} + +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) + } + + t.Value = value + + return nil +} + +func (t TypeMetricPrefix) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + 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/config2/type_metric_prefix_test.go b/internal/config2/type_metric_prefix_test.go new file mode 100644 index 0000000..bed6497 --- /dev/null +++ b/internal/config2/type_metric_prefix_test.go @@ -0,0 +1,70 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeMetricPrefixTestStruct struct { + Value config2.TypeMetricPrefix `json:"value"` +} + +type TypeMetricPrefixTestSuite struct { + suite.Suite +} + +func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "-", + "hello/world", + "lala*", + "++sdf++", + } + + 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, &typeMetricPrefixTestStruct{})) + }) + } +} + +func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() { + testStruct := &typeMetricPrefixTestStruct{} + suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct)) + suite.Equal("mtg", testStruct.Value.Get("lalala")) +} + +func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() { + testStruct := &typeMetricPrefixTestStruct{ + Value: config2.TypeMetricPrefix{ + Value: "mtg", + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value": "mtg"}`, string(data)) +} + +func (suite *TypeMetricPrefixTestSuite) TestGet() { + value := config2.TypeMetricPrefix{} + suite.Equal("lalala", value.Get("lalala")) + + value.Value = "mtg" + suite.Equal("mtg", value.Get("lalala")) +} + +func TestTypeMetricPrefix(t *testing.T) { + t.Parallel() + suite.Run(t, &TypeMetricPrefixTestSuite{}) +} diff --git a/internal/config2/type_port.go b/internal/config2/type_port.go new file mode 100644 index 0000000..3a307d9 --- /dev/null +++ b/internal/config2/type_port.go @@ -0,0 +1,45 @@ +package config2 + +import ( + "fmt" + "strconv" +) + +type TypePort struct { + Value uint16 +} + +func (t *TypePort) Set(value string) error { + portValue, err := strconv.ParseUint(value, 10, 16) + if err != nil { + return fmt.Errorf("incorrect port number (%v): %w", value, err) + } + + if portValue == 0 { + return fmt.Errorf("incorrect port number (%s)", value) + } + + t.Value = uint16(portValue) + + return nil +} + +func (t TypePort) Get(defaultValue uint16) uint16 { + if t.Value == 0 { + return defaultValue + } + + 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/config2/type_port_test.go b/internal/config2/type_port_test.go new file mode 100644 index 0000000..544a2e9 --- /dev/null +++ b/internal/config2/type_port_test.go @@ -0,0 +1,71 @@ +package config2_test + +import ( + "encoding/json" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typePortTestStruct struct { + Value config2.TypePort `json:"value"` +} + +type TypePortTestSuite struct { + suite.Suite +} + +func (suite *TypePortTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "port", + "0", + "-1", + "1.5", + "70000", + } + + 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, &typePortTestStruct{})) + }) + } +} + +func (suite *TypePortTestSuite) TestUnmarshalOk() { + testStruct := &typePortTestStruct{} + suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct)) + suite.EqualValues(5, testStruct.Value.Value) +} + +func (suite *TypePortTestSuite) TestMarshalOk() { + testStruct := &typePortTestStruct{ + Value: config2.TypePort{ + Value: 10, + }, + } + + data, err := json.Marshal(testStruct) + suite.NoError(err) + suite.JSONEq(`{"value":10}`, string(data)) +} + +func (suite *TypePortTestSuite) TestGet() { + value := config2.TypePort{} + suite.EqualValues(10, value.Get(10)) + + value.Value = 100 + suite.EqualValues(100, value.Get(10)) +} + +func TestTypePort(t *testing.T) { + t.Parallel() + suite.Run(t, &TypePortTestSuite{}) +} diff --git a/internal/config2/type_prefer_ip.go b/internal/config2/type_prefer_ip.go new file mode 100644 index 0000000..3370a3a --- /dev/null +++ b/internal/config2/type_prefer_ip.go @@ -0,0 +1,62 @@ +package config2 + +import ( + "fmt" + "strings" +) + +const ( + // TypePreferIPPreferIPv4 states that you prefer to use IPv4 addresses + // but IPv6 is also possible. + TypePreferIPPreferIPv4 = "prefer-ipv4" + + // TypePreferIPPreferIPv6 states that you prefer to use IPv6 addresses + // but IPv4 is also possible. + TypePreferIPPreferIPv6 = "prefer-ipv6" + + // TypePreferOnlyIPv4 states that you prefer to use IPv4 addresses + // only. + TypePreferOnlyIPv4 = "only-ipv4" + + // TypePreferOnlyIPv6 states that you prefer to use IPv6 addresses + // only. + TypePreferOnlyIPv6 = "only-ipv6" +) + +type TypePreferIP struct { + Value string +} + +func (t *TypePreferIP) Set(value string) error { + value = strings.ToLower(value) + + switch value { + case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, + TypePreferOnlyIPv4, TypePreferOnlyIPv6: + t.Value = value + + return nil + default: + return fmt.Errorf("unsupported ip preference: %s", value) + } +} + +func (t *TypePreferIP) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + 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/config2/type_prefer_ip_test.go b/internal/config2/type_prefer_ip_test.go new file mode 100644 index 0000000..420ac13 --- /dev/null +++ b/internal/config2/type_prefer_ip_test.go @@ -0,0 +1,113 @@ +package config2_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typePreferIPTestStruct struct { + Value config2.TypePreferIP `json:"value"` +} + +type TypePreferIPTestSuite struct { + suite.Suite +} + +func (suite *TypePreferIPTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "prefer", + "preferipv4", + config2.TypePreferIPPreferIPv4 + "_", + } + + 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, &typePreferIPTestStruct{})) + }) + } +} + +func (suite *TypePreferIPTestSuite) TestUnmarshalOk() { + testData := []string{ + config2.TypePreferIPPreferIPv4, + config2.TypePreferIPPreferIPv6, + config2.TypePreferOnlyIPv4, + config2.TypePreferOnlyIPv6, + strings.ToTitle(config2.TypePreferOnlyIPv4), + strings.ToTitle(config2.TypePreferOnlyIPv6), + strings.ToTitle(config2.TypePreferIPPreferIPv4), + strings.ToTitle(config2.TypePreferIPPreferIPv6), + } + + 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{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) + }) + } +} + +func (suite *TypePreferIPTestSuite) TestMarshalOk() { + testData := []string{ + config2.TypePreferIPPreferIPv4, + config2.TypePreferIPPreferIPv6, + config2.TypePreferOnlyIPv4, + config2.TypePreferOnlyIPv6, + } + + for _, v := range testData { + value := v + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typePreferIPTestStruct{ + Value: config2.TypePreferIP{ + Value: value, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *TypePreferIPTestSuite) TestGet() { + value := config2.TypePreferIP{} + suite.Equal(config2.TypePreferIPPreferIPv4, + value.Get(config2.TypePreferIPPreferIPv4)) + + suite.NoError(value.Set(config2.TypePreferIPPreferIPv6)) + suite.Equal(config2.TypePreferIPPreferIPv6, + value.Get(config2.TypePreferIPPreferIPv4)) +} + +func TestTypePreferIP(t *testing.T) { + t.Parallel() + suite.Run(t, &TypePreferIPTestSuite{}) +} diff --git a/internal/config2/type_proxy_url.go b/internal/config2/type_proxy_url.go new file mode 100644 index 0000000..7336968 --- /dev/null +++ b/internal/config2/type_proxy_url.go @@ -0,0 +1,61 @@ +package config2 + +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/config2/type_proxy_url_test.go b/internal/config2/type_proxy_url_test.go new file mode 100644 index 0000000..f099231 --- /dev/null +++ b/internal/config2/type_proxy_url_test.go @@ -0,0 +1,95 @@ +package config2_test + +import ( + "encoding/json" + "net/url" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeProxyURLTestStruct struct { + Value config2.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: config2.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 := config2.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/config2/type_statsd_tag_format.go b/internal/config2/type_statsd_tag_format.go new file mode 100644 index 0000000..fb4f267 --- /dev/null +++ b/internal/config2/type_statsd_tag_format.go @@ -0,0 +1,58 @@ +package config2 + +import ( + "fmt" + "strings" +) + +const ( + // TypeStatsdTagFormatInfluxdb defines a tag format compatible with + // InfluxDB. + TypeStatsdTagFormatInfluxdb = "influxdb" + + // TypeStatsdTagFormatDatadog defines a tag format compatible with + // DataDog. + TypeStatsdTagFormatDatadog = "datadog" + + // TypeStatsdTagFormatGraphite defines a tag format compatible with + // Graphite. + TypeStatsdTagFormatGraphite = "graphite" +) + +type TypeStatsdTagFormat struct { + Value string +} + +func (t *TypeStatsdTagFormat) Set(value string) error { + lowercasedValue := strings.ToLower(value) + + switch lowercasedValue { + case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb, + TypeStatsdTagFormatGraphite: + t.Value = lowercasedValue + + return nil + default: + return fmt.Errorf("unknown tag format %s", value) + } +} + +func (t TypeStatsdTagFormat) Get(defaultValue string) string { + if t.Value == "" { + return defaultValue + } + + 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/config2/type_statsd_tag_format_test.go b/internal/config2/type_statsd_tag_format_test.go new file mode 100644 index 0000000..fb75b26 --- /dev/null +++ b/internal/config2/type_statsd_tag_format_test.go @@ -0,0 +1,108 @@ +package config2_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type typeStatsdTagFormatTestStruct struct { + Value config2.TypeStatsdTagFormat `json:"value"` +} + +type StatsdTagFormatTestSuite struct { + suite.Suite +} + +func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() { + testData := []string{ + "", + "dogdog", + } + + 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, &typeStatsdTagFormatTestStruct{})) + }) + } +} + +func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() { + testData := []string{ + config2.TypeStatsdTagFormatInfluxdb, + config2.TypeStatsdTagFormatGraphite, + config2.TypeStatsdTagFormatDatadog, + strings.ToUpper(config2.TypeStatsdTagFormatInfluxdb), + strings.ToUpper(config2.TypeStatsdTagFormatGraphite), + strings.ToUpper(config2.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{} + assert.NoError(t, json.Unmarshal(data, testStruct)) + assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) + }) + } +} + +func (suite *StatsdTagFormatTestSuite) TestMarshalOk() { + testData := []string{ + config2.TypeStatsdTagFormatInfluxdb, + config2.TypeStatsdTagFormatGraphite, + config2.TypeStatsdTagFormatDatadog, + } + + for _, v := range testData { + value := v + + suite.T().Run(v, func(t *testing.T) { + testStruct := &typeStatsdTagFormatTestStruct{ + Value: config2.TypeStatsdTagFormat{ + Value: value, + }, + } + + encodedJSON, err := json.Marshal(testStruct) + assert.NoError(t, err) + + expectedJSON, err := json.Marshal(map[string]string{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) + }) + } +} + +func (suite *StatsdTagFormatTestSuite) TestGet() { + value := config2.TypeStatsdTagFormat{} + suite.Equal(config2.TypeStatsdTagFormatDatadog, + value.Get(config2.TypeStatsdTagFormatDatadog)) + + suite.NoError(value.Set(config2.TypeStatsdTagFormatInfluxdb)) + suite.Equal(config2.TypeStatsdTagFormatInfluxdb, + value.Get(config2.TypeStatsdTagFormatDatadog)) +} + +func TestTypeStatsdTagFormat(t *testing.T) { + t.Parallel() + suite.Run(t, &StatsdTagFormatTestSuite{}) +} From 87ed1d1aa7dcdc97a837641bd258672ef53ce449 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Thu, 29 Jul 2021 16:21:22 +0300 Subject: [PATCH 2/8] Move config2 into config --- internal/cli/utils.go | 66 ++++++++ internal/config/config.go | 108 ++----------- internal/config/config_test.go | 2 +- internal/{config2 => config}/parse.go | 2 +- internal/config/type_blocklist_uri.go | 71 +++++---- internal/config/type_blocklist_uri_test.go | 148 +++++------------- internal/{config2 => config}/type_bool.go | 2 +- .../{config2 => config}/type_bool_test.go | 10 +- internal/config/type_bytes.go | 57 +++---- internal/config/type_bytes_test.go | 56 ++----- .../{config2 => config}/type_concurrency.go | 2 +- .../type_concurrency_test.go | 10 +- internal/config/type_duration.go | 51 +++--- internal/config/type_duration_test.go | 74 ++++----- internal/config/type_error_rate.go | 40 ++--- internal/config/type_error_rate_test.go | 103 +++++------- internal/config/type_hostport.go | 74 ++++----- internal/config/type_hostport_test.go | 69 +++----- internal/config/type_http_path.go | 38 +++-- internal/config/type_http_path_test.go | 72 +++------ internal/config/type_ip.go | 48 +++--- internal/config/type_ip_test.go | 79 ++++------ internal/config/type_metric_prefix.go | 41 +++-- internal/config/type_metric_prefix_test.go | 85 +++------- internal/config/type_port.go | 44 +++--- internal/config/type_port_test.go | 94 +++-------- internal/config/type_prefer_ip.go | 49 +++--- internal/config/type_prefer_ip_test.go | 87 ++++------ .../{config2 => config}/type_proxy_url.go | 2 +- .../type_proxy_url_test.go | 13 +- internal/config/type_statsd_tag_format.go | 49 +++--- .../config/type_statsd_tag_format_test.go | 90 ++++------- internal/config/type_url.go | 71 --------- internal/config/type_url_test.go | 107 ------------- internal/config2/config.go | 81 ---------- internal/config2/config_test.go | 54 ------- internal/config2/testdata/broken.toml | 1 - internal/config2/testdata/minimal.toml | 2 - internal/config2/testdata/only_secret.toml | 1 - internal/config2/type_blocklist_uri.go | 77 --------- internal/config2/type_blocklist_uri_test.go | 113 ------------- internal/config2/type_bytes.go | 55 ------- internal/config2/type_bytes_test.go | 86 ---------- internal/config2/type_duration.go | 53 ------- internal/config2/type_duration_test.go | 110 ------------- internal/config2/type_error_rate.go | 47 ------ internal/config2/type_error_rate_test.go | 94 ----------- internal/config2/type_hostport.go | 59 ------- internal/config2/type_hostport_test.go | 88 ----------- internal/config2/type_http_path.go | 33 ---- internal/config2/type_http_path_test.go | 67 -------- internal/config2/type_ip.go | 45 ------ internal/config2/type_ip_test.go | 104 ------------ internal/config2/type_metric_prefix.go | 40 ----- internal/config2/type_metric_prefix_test.go | 70 --------- internal/config2/type_port.go | 45 ------ internal/config2/type_port_test.go | 71 --------- internal/config2/type_prefer_ip.go | 62 -------- internal/config2/type_prefer_ip_test.go | 113 ------------- internal/config2/type_statsd_tag_format.go | 58 ------- .../config2/type_statsd_tag_format_test.go | 108 ------------- 61 files changed, 696 insertions(+), 2955 deletions(-) create mode 100644 internal/cli/utils.go rename internal/{config2 => config}/parse.go (99%) rename internal/{config2 => config}/type_bool.go (97%) rename internal/{config2 => config}/type_bool_test.go (92%) rename internal/{config2 => config}/type_concurrency.go (98%) rename internal/{config2 => config}/type_concurrency_test.go (87%) rename internal/{config2 => config}/type_proxy_url.go (98%) rename internal/{config2 => config}/type_proxy_url_test.go (90%) delete mode 100644 internal/config/type_url.go delete mode 100644 internal/config/type_url_test.go delete mode 100644 internal/config2/config.go delete mode 100644 internal/config2/config_test.go delete mode 100644 internal/config2/testdata/broken.toml delete mode 100644 internal/config2/testdata/minimal.toml delete mode 100644 internal/config2/testdata/only_secret.toml delete mode 100644 internal/config2/type_blocklist_uri.go delete mode 100644 internal/config2/type_blocklist_uri_test.go delete mode 100644 internal/config2/type_bytes.go delete mode 100644 internal/config2/type_bytes_test.go delete mode 100644 internal/config2/type_duration.go delete mode 100644 internal/config2/type_duration_test.go delete mode 100644 internal/config2/type_error_rate.go delete mode 100644 internal/config2/type_error_rate_test.go delete mode 100644 internal/config2/type_hostport.go delete mode 100644 internal/config2/type_hostport_test.go delete mode 100644 internal/config2/type_http_path.go delete mode 100644 internal/config2/type_http_path_test.go delete mode 100644 internal/config2/type_ip.go delete mode 100644 internal/config2/type_ip_test.go delete mode 100644 internal/config2/type_metric_prefix.go delete mode 100644 internal/config2/type_metric_prefix_test.go delete mode 100644 internal/config2/type_port.go delete mode 100644 internal/config2/type_port_test.go delete mode 100644 internal/config2/type_prefer_ip.go delete mode 100644 internal/config2/type_prefer_ip_test.go delete mode 100644 internal/config2/type_statsd_tag_format.go delete mode 100644 internal/config2/type_statsd_tag_format_test.go diff --git a/internal/cli/utils.go b/internal/cli/utils.go new file mode 100644 index 0000000..d07f047 --- /dev/null +++ b/internal/cli/utils.go @@ -0,0 +1,66 @@ +package cli + +import ( + "fmt" + "net" + "net/url" + "os" + + "github.com/9seconds/mtg/v2/internal/config2" + "github.com/9seconds/mtg/v2/mtglib" + "github.com/9seconds/mtg/v2/network" +) + +func readTOMLConfig(path string) (*config2.Config, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read config file: %w", err) + } + + conf, err := config2.Parse(content) + if err != nil { + return nil, fmt.Errorf("cannot parse config: %w", err) + } + + return conf, nil +} + +func makeNetwork(conf *config2.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 +} diff --git a/internal/config/config.go b/internal/config/config.go index c8e88d3..c05d33f 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/config_test.go b/internal/config/config_test.go index e6b19e0..f191221 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,7 +10,7 @@ import ( ) type ConfigTestSuite struct { - suite.Suite + suite.Suite } func (suite *ConfigTestSuite) ReadConfig(filename string) []byte { diff --git a/internal/config2/parse.go b/internal/config/parse.go similarity index 99% rename from internal/config2/parse.go rename to internal/config/parse.go index c63c762..f4e7a05 100644 --- a/internal/config2/parse.go +++ b/internal/config/parse.go @@ -1,4 +1,4 @@ -package config2 +package config import ( "bytes" diff --git a/internal/config/type_blocklist_uri.go b/internal/config/type_blocklist_uri.go index 6bcf6b5..8bd3677 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 - } +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") + } - text := string(data) - if filepath.IsAbs(text) { - if _, err := os.Stat(text); os.IsNotExist(err) { - return fmt.Errorf("filepath %s does not exist", text) + value, err = filepath.Abs(value) + if err != nil { + return fmt.Errorf( + "value is correct filepath but cannot resolve absolute (%s): %w", + value, err) } - c.value = text + 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..407beda 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/config2/type_bool.go b/internal/config/type_bool.go similarity index 97% rename from internal/config2/type_bool.go rename to internal/config/type_bool.go index 490d610..233a5a6 100644 --- a/internal/config2/type_bool.go +++ b/internal/config/type_bool.go @@ -1,4 +1,4 @@ -package config2 +package config import ( "fmt" diff --git a/internal/config2/type_bool_test.go b/internal/config/type_bool_test.go similarity index 92% rename from internal/config2/type_bool_test.go rename to internal/config/type_bool_test.go index 0fa3b09..2b90285 100644 --- a/internal/config2/type_bool_test.go +++ b/internal/config/type_bool_test.go @@ -1,4 +1,4 @@ -package config2_test +package config_test import ( "encoding/json" @@ -6,13 +6,13 @@ import ( "strconv" "testing" - "github.com/9seconds/mtg/v2/internal/config2" + "github.com/9seconds/mtg/v2/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type typeBoolTestStruct struct { - Value config2.TypeBool `json:"value"` + Value config.TypeBool `json:"value"` } type TypeBoolTestSuite struct { @@ -85,7 +85,7 @@ func (suite *TypeBoolTestSuite) TestMarshalOk() { suite.T().Run(name, func(t *testing.T) { testStruct := typeBoolTestStruct{ - Value: config2.TypeBool{ + Value: config.TypeBool{ Value: v, }, } @@ -98,7 +98,7 @@ func (suite *TypeBoolTestSuite) TestMarshalOk() { } func (suite *TypeBoolTestSuite) TestGet() { - value := config2.TypeBool{} + value := config.TypeBool{} suite.False(value.Get(false)) suite.True(value.Get(true)) diff --git a/internal/config/type_bytes.go b/internal/config/type_bytes.go index 4c7d7d6..412f019 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..08874f4 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/config2/type_concurrency.go b/internal/config/type_concurrency.go similarity index 98% rename from internal/config2/type_concurrency.go rename to internal/config/type_concurrency.go index e54f8ad..1c172d7 100644 --- a/internal/config2/type_concurrency.go +++ b/internal/config/type_concurrency.go @@ -1,4 +1,4 @@ -package config2 +package config import ( "fmt" diff --git a/internal/config2/type_concurrency_test.go b/internal/config/type_concurrency_test.go similarity index 87% rename from internal/config2/type_concurrency_test.go rename to internal/config/type_concurrency_test.go index f7227a5..cf18024 100644 --- a/internal/config2/type_concurrency_test.go +++ b/internal/config/type_concurrency_test.go @@ -1,16 +1,16 @@ -package config2_test +package config_test import ( "encoding/json" "testing" - "github.com/9seconds/mtg/v2/internal/config2" + "github.com/9seconds/mtg/v2/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type typeConcurrencyTestStruct struct { - Value config2.TypeConcurrency `json:"value"` + Value config.TypeConcurrency `json:"value"` } type TypeConcurrencyTestSuite struct { @@ -49,7 +49,7 @@ func (suite *TypeConcurrencyTestSuite) TestUnmarshalOk() { func (suite *TypeConcurrencyTestSuite) TestMarshalOk() { testStruct := &typeConcurrencyTestStruct{ - Value: config2.TypeConcurrency{ + Value: config.TypeConcurrency{ Value: 2, }, } @@ -60,7 +60,7 @@ func (suite *TypeConcurrencyTestSuite) TestMarshalOk() { } func (suite *TypeConcurrencyTestSuite) TestGet() { - value := config2.TypeConcurrency{} + value := config.TypeConcurrency{} suite.EqualValues(1, value.Get(1)) value.Value = 3 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..49d674a 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..cfe6ebc 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) UnmarshalText(data []byte) error { + return t.Set(string(data)) +} + +func (t TypeErrorRate) MarshalText() ([]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..aebe8f0 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,74 @@ 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, + testData := map[string]float64{ + "1": 1.0, + "1.0": 1.0, + "0.5": 0.5, + ".5": 0.5, } - for _, v := range testData { + for k, v := range testData { value := v - data, err := json.Marshal(map[string]float64{ - "value": v, + data, err := json.Marshal(map[string]string{ + "value": k, }) suite.NoError(err) - suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) { + suite.T().Run(k, func(t *testing.T) { testStruct := &typeErrorRateTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.InEpsilon(t, value, testStruct.Value.Value(0), 1e-10) + assert.InEpsilon(t, value, 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..9f006a7 100644 --- a/internal/config/type_hostport.go +++ b/internal/config/type_hostport.go @@ -7,61 +7,53 @@ import ( ) type TypeHostPort struct { - host TypeIP - port TypePort + Value string } -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) + 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..03a2f20 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..fcd951e 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..f53d77e 100644 --- a/internal/config/type_port.go +++ b/internal/config/type_port.go @@ -6,40 +6,40 @@ import ( ) type TypePort struct { - value uint + Value uint16 } -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 = uint16(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 uint16) uint16 { + 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..b28a33f 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/config2/type_proxy_url.go b/internal/config/type_proxy_url.go similarity index 98% rename from internal/config2/type_proxy_url.go rename to internal/config/type_proxy_url.go index 7336968..f27d0af 100644 --- a/internal/config2/type_proxy_url.go +++ b/internal/config/type_proxy_url.go @@ -1,4 +1,4 @@ -package config2 +package config import ( "fmt" diff --git a/internal/config2/type_proxy_url_test.go b/internal/config/type_proxy_url_test.go similarity index 90% rename from internal/config2/type_proxy_url_test.go rename to internal/config/type_proxy_url_test.go index f099231..4bbe042 100644 --- a/internal/config2/type_proxy_url_test.go +++ b/internal/config/type_proxy_url_test.go @@ -1,17 +1,17 @@ -package config2_test +package config_test import ( "encoding/json" "net/url" "testing" - "github.com/9seconds/mtg/v2/internal/config2" + "github.com/9seconds/mtg/v2/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) type typeProxyURLTestStruct struct { - Value config2.TypeProxyURL `json:"value"` + Value config.TypeProxyURL `json:"value"` } type ProxyURLTestSuite struct { @@ -69,20 +69,21 @@ func (suite *ProxyURLTestSuite) TestUnmarshalOk() { func (suite *ProxyURLTestSuite) TestMarshalOk() { parsed, _ := url.Parse("socks5://127.0.0.1:1080?open_threshold=1") testStruct := &typeProxyURLTestStruct{ - Value: config2.TypeProxyURL{ + 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)) + suite.JSONEq(`{"value": "socks5://127.0.0.1:1080?open_threshold=1"}`, + string(encodedJSON)) } func (suite *ProxyURLTestSuite) TestGet() { emptyURL := &url.URL{} - value := config2.TypeProxyURL{} + value := config.TypeProxyURL{} suite.Equal(emptyURL, value.Get(emptyURL)) value.Value = &url.URL{} diff --git a/internal/config/type_statsd_tag_format.go b/internal/config/type_statsd_tag_format.go index 51ed34b..e449b9f 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/config2/config.go b/internal/config2/config.go deleted file mode 100644 index 1b89c78..0000000 --- a/internal/config2/config.go +++ /dev/null @@ -1,81 +0,0 @@ -package config2 - -import ( - "bytes" - "encoding/json" - "fmt" - - "github.com/9seconds/mtg/v2/mtglib" -) - -type Config struct { - 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 TypeBool `json:"enabled"` - MaxSize TypeBytes `json:"maxSize"` - ErrorRate TypeErrorRate `json:"errorRate"` - } `json:"antiReplay"` - Blocklist struct { - Enabled TypeBool `json:"enabled"` - DownloadConcurrency TypeConcurrency `json:"downloadConcurrency"` - URLs []TypeBlocklistURI `json:"urls"` - UpdateEach TypeDuration `json:"updateEach"` - } `json:"blocklist"` - } `json:"defense"` - Network struct { - Timeout struct { - TCP TypeDuration `json:"tcp"` - HTTP TypeDuration `json:"http"` - Idle TypeDuration `json:"idle"` - } `json:"timeout"` - DOHIP TypeIP `json:"dohIp"` - Proxies []TypeProxyURL `json:"proxies"` - } `json:"network"` - Stats struct { - StatsD struct { - Enabled TypeBool `json:"enabled"` - Address TypeHostPort `json:"address"` - MetricPrefix TypeMetricPrefix `json:"metricPrefix"` - TagFormat TypeStatsdTagFormat `json:"tagFormat"` - } `json:"statsd"` - Prometheus struct { - Enabled TypeBool `json:"enabled"` - BindTo TypeHostPort `json:"bindTo"` - HTTPPath TypeHTTPPath `json:"httpPath"` - MetricPrefix TypeMetricPrefix `json:"metricPrefix"` - } `json:"prometheus"` - } `json:"stats"` -} - -func (c *Config) Validate() error { - if !c.Secret.Valid() { - return fmt.Errorf("invalid secret %s", c.Secret.String()) - } - - if c.BindTo.Get("") == "" { - return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String()) - } - - return nil -} - -func (c *Config) String() string { - buf := &bytes.Buffer{} - encoder := json.NewEncoder(buf) - - encoder.SetEscapeHTML(false) - - if err := encoder.Encode(c); err != nil { - panic(err) - } - - return buf.String() -} diff --git a/internal/config2/config_test.go b/internal/config2/config_test.go deleted file mode 100644 index 76d32e7..0000000 --- a/internal/config2/config_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package config2_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/suite" -) - -type ConfigTestSuite struct { - suite.Suite -} - -func (suite *ConfigTestSuite) ReadConfig(filename string) []byte { - data, err := os.ReadFile(filepath.Join("testdata", filename)) - suite.NoError(err) - - return data -} - -func (suite *ConfigTestSuite) TestParseEmpty() { - _, err := config2.Parse([]byte{}) - suite.Error(err) -} - -func (suite *ConfigTestSuite) TestParseBrokenToml() { - _, err := config2.Parse(suite.ReadConfig("broken.toml")) - suite.Error(err) -} - -func (suite *ConfigTestSuite) TestParseOnlySecret() { - _, err := config2.Parse(suite.ReadConfig("only_secret.toml")) - suite.Error(err) -} - -func (suite *ConfigTestSuite) TestParseMinimalConfig() { - conf, err := config2.Parse(suite.ReadConfig("minimal.toml")) - suite.NoError(err) - suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64()) - suite.Equal("0.0.0.0:3128", conf.BindTo.String()) -} - -func (suite *ConfigTestSuite) TestString() { - conf, err := config2.Parse(suite.ReadConfig("minimal.toml")) - suite.NoError(err) - suite.NotEmpty(conf.String()) -} - -func TestConfig(t *testing.T) { - t.Parallel() - suite.Run(t, &ConfigTestSuite{}) -} diff --git a/internal/config2/testdata/broken.toml b/internal/config2/testdata/broken.toml deleted file mode 100644 index d95f791..0000000 --- a/internal/config2/testdata/broken.toml +++ /dev/null @@ -1 +0,0 @@ -s = sdfsdfds diff --git a/internal/config2/testdata/minimal.toml b/internal/config2/testdata/minimal.toml deleted file mode 100644 index 9d0961a..0000000 --- a/internal/config2/testdata/minimal.toml +++ /dev/null @@ -1,2 +0,0 @@ -secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" -bind-to = "0.0.0.0:3128" diff --git a/internal/config2/testdata/only_secret.toml b/internal/config2/testdata/only_secret.toml deleted file mode 100644 index f6b0bee..0000000 --- a/internal/config2/testdata/only_secret.toml +++ /dev/null @@ -1 +0,0 @@ -secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t" diff --git a/internal/config2/type_blocklist_uri.go b/internal/config2/type_blocklist_uri.go deleted file mode 100644 index f73ee3c..0000000 --- a/internal/config2/type_blocklist_uri.go +++ /dev/null @@ -1,77 +0,0 @@ -package config2 - -import ( - "fmt" - "net/url" - "os" - "path/filepath" -) - -type TypeBlocklistURI struct { - Value string -} - -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") - } - - 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(value) - if err != nil { - return fmt.Errorf("incorrect url (%s): %w", value, err) - } - - switch parsedURL.Scheme { - case "http", "https": - default: - return fmt.Errorf("unknown schema %s (%s)", parsedURL.Scheme, value) - } - - if parsedURL.Host == "" { - return fmt.Errorf("incorrect url %s", value) - } - - t.Value = parsedURL.String() - - return nil -} - -func (t TypeBlocklistURI) Get(defaultValue string) string { - if t.Value == "" { - return defaultValue - } - - 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/config2/type_blocklist_uri_test.go b/internal/config2/type_blocklist_uri_test.go deleted file mode 100644 index cfecc8a..0000000 --- a/internal/config2/type_blocklist_uri_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeBlocklistURITestStruct struct { - Value config2.TypeBlocklistURI `json:"value"` -} - -type TypeBlocklistURITestSuite struct { - suite.Suite - - directory string - absDirectory string -} - -func (suite *TypeBlocklistURITestSuite) SetupSuite() { - dir, _ := os.Getwd() - absDir, _ := filepath.Abs(dir) - - suite.directory = dir - suite.absDirectory = absDir -} - -func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() { - testData := []string{ - "gopher://lalala", - "https:///paths", - "h:/=", - filepath.Join(suite.directory, "___"), - filepath.Join(suite.absDirectory, "___"), - suite.directory, - suite.absDirectory, - } - - 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, &typeBlocklistURITestStruct{})) - }) - } -} - -func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() { - testData := []string{ - "http://lalala", - "https://lalala", - "https://lalala/path", - filepath.Join(suite.directory, "config.go"), - filepath.Join(suite.absDirectory, "config.go"), - } - - 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 := &typeBlocklistURITestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, value, testStruct.Value.Get("")) - - if strings.HasPrefix(value, "http") { - assert.True(t, testStruct.Value.IsRemote()) - } else { - assert.False(t, testStruct.Value.IsRemote()) - } - }) - } -} - -func (suite *TypeBlocklistURITestSuite) TestMarshalOk() { - testStruct := &typeBlocklistURITestStruct{ - Value: config2.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 := config2.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/config2/type_bytes.go b/internal/config2/type_bytes.go deleted file mode 100644 index 789ab54..0000000 --- a/internal/config2/type_bytes.go +++ /dev/null @@ -1,55 +0,0 @@ -package config2 - -import ( - "fmt" - "strings" - - "github.com/alecthomas/units" -) - -var typeBytesStringCleaner = strings.NewReplacer(" ", "", "\t", "", "IB", "iB") - -type TypeBytes struct { - Value units.Base2Bytes -} - -func (t *TypeBytes) Set(value string) error { - normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) - - parsedValue, err := units.ParseBase2Bytes(normalizedValue) - if err != nil { - return fmt.Errorf("incorrect bytes value (%v): %w", value, err) - } - - if parsedValue < 0 { - return fmt.Errorf("bytes should be positive (%s)", value) - } - - t.Value = parsedValue - - return nil -} - -func (t TypeBytes) Get(defaultValue uint) uint { - if t.Value == 0 { - return defaultValue - } - - 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/config2/type_bytes_test.go b/internal/config2/type_bytes_test.go deleted file mode 100644 index c153fb2..0000000 --- a/internal/config2/type_bytes_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeBytesTestStruct struct { - Value config2.TypeBytes `json:"value"` -} - -type TypeBytesTestSuite struct { - suite.Suite -} - -func (suite *TypeBytesTestSuite) TestUnmarshalFail() { - testData := []string{ - "1m", - "1", - "-1kb", - "-1kib", - } - - 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, &typeBytesTestStruct{})) - }) - } -} - -func (suite *TypeBytesTestSuite) TestUnmarshalOk() { - testData := map[string]uint{ - "1b": 1, - "1kb": 1024, - "1kib": 1024, - "2mb": 2 * 1024 * 1024, - "2mib": 2 * 1024 * 1024, - } - - 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 := &typeBytesTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.EqualValues(t, value, testStruct.Value.Get(0)) - }) - } -} - -func (suite *TypeBytesTestSuite) TestMarshalOk() { - value := typeBytesTestStruct{} - suite.NoError(value.Value.Set("1kib")) - - data, err := json.Marshal(value) - suite.NoError(err) - suite.JSONEq(`{"value": "1kib"}`, string(data)) -} - -func (suite *TypeBytesTestSuite) TestGet() { - value := config2.TypeBytes{} - suite.EqualValues(1000, value.Get(1000)) - - suite.NoError(value.Set("1mib")) - suite.EqualValues(1048576, value.Get(1000)) -} - -func TestTypeBytes(t *testing.T) { - t.Parallel() - suite.Run(t, &TypeBytesTestSuite{}) -} diff --git a/internal/config2/type_duration.go b/internal/config2/type_duration.go deleted file mode 100644 index 85db148..0000000 --- a/internal/config2/type_duration.go +++ /dev/null @@ -1,53 +0,0 @@ -package config2 - -import ( - "fmt" - "strings" - "time" -) - -var typeDurationStringCleaner = strings.NewReplacer(" ", "", "\t", "") - -type TypeDuration struct { - Value time.Duration -} - -func (t *TypeDuration) Set(value string) error { - parsedValue, err := time.ParseDuration( - typeDurationStringCleaner.Replace(strings.ToLower(value))) - if err != nil { - return fmt.Errorf("incorrect duration (%s): %w", value, err) - } - - if parsedValue < 0 { - return fmt.Errorf("duration has to be a positive: %s", value) - } - - t.Value = parsedValue - - return nil -} - -func (t TypeDuration) Get(defaultValue time.Duration) time.Duration { - if t.Value == 0 { - return defaultValue - } - - 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/config2/type_duration_test.go b/internal/config2/type_duration_test.go deleted file mode 100644 index 3f9b21e..0000000 --- a/internal/config2/type_duration_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - "time" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeDurationTestStruct struct { - Value config2.TypeDuration `json:"value"` -} - -type TypeDurationTestSuite struct { - suite.Suite -} - -func (suite *TypeDurationTestSuite) TestUnmarshalFail() { - testData := []string{ - "-1s", - "1 seconds ago", - "1s ago", - "", - } - - 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, &typeDurationTestStruct{})) - }) - } -} - -func (suite *TypeDurationTestSuite) TestUnmarshalOk() { - testData := map[string]time.Duration{ - "1s": 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 { - value := v - - data, err := json.Marshal(map[string]string{ - "value": k, - }) - suite.NoError(err) - - suite.T().Run(k, func(t *testing.T) { - testStruct := &typeDurationTestStruct{} - - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.Value) - }) - } -} - -func (suite *TypeDurationTestSuite) TestMarshalOk() { - testData := map[string]string{ - "1s": "1s", - "0": "", - "0s": "", - "0ms": "", - "1 H": "1h0m0s", - } - - for k, v := range testData { - value := k - expected := v - - suite.T().Run(value, func(t *testing.T) { - testStruct := &typeDurationTestStruct{} - - assert.NoError(t, testStruct.Value.Set(value)) - - data, err := json.Marshal(testStruct) - assert.NoError(t, err) - - expectedJson, err := json.Marshal(map[string]string{ - "value": expected, - }) - assert.NoError(t, err) - - assert.JSONEq(t, string(expectedJson), string(data)) - }) - } -} - -func (suite *TypeDurationTestSuite) TestGet() { - value := config2.TypeDuration{} - suite.Equal(time.Second, value.Get(time.Second)) - - value.Value = 3 * time.Second - suite.Equal(3*time.Second, value.Get(time.Hour)) -} - -func TestTypeDuration(t *testing.T) { - t.Parallel() - suite.Run(t, &TypeDurationTestSuite{}) -} diff --git a/internal/config2/type_error_rate.go b/internal/config2/type_error_rate.go deleted file mode 100644 index 1ce8ecc..0000000 --- a/internal/config2/type_error_rate.go +++ /dev/null @@ -1,47 +0,0 @@ -package config2 - -import ( - "fmt" - "strconv" -) - -const typeErrorRateIgnoreLess = 1e-8 - -type TypeErrorRate struct { - Value float64 -} - -func (t *TypeErrorRate) Set(value string) error { - parsedValue, err := strconv.ParseFloat(value, 64) - if err != nil { - return fmt.Errorf("Value is not a float (%s): %w", value, err) - } - - if parsedValue <= 0.0 || parsedValue >= 100.0 { - return fmt.Errorf("Value should be 0 < x < 100 (%s)", value) - } - - t.Value = parsedValue - - return nil -} - -func (t TypeErrorRate) Get(defaultValue float64) float64 { - if t.Value < typeErrorRateIgnoreLess { - return defaultValue - } - - return t.Value -} - -func (t *TypeErrorRate) UnmarshalText(data []byte) error { - return t.Set(string(data)) -} - -func (t TypeErrorRate) MarshalText() ([]byte, error) { - return []byte(t.String()), nil -} - -func (t TypeErrorRate) String() string { - return strconv.FormatFloat(t.Value, 'f', -1, 64) -} diff --git a/internal/config2/type_error_rate_test.go b/internal/config2/type_error_rate_test.go deleted file mode 100644 index 5d8590a..0000000 --- a/internal/config2/type_error_rate_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeErrorRateTestStruct struct { - Value config2.TypeErrorRate `json:"value"` -} - -type TypeErrorRateTestSuite struct { - suite.Suite -} - -func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() { - 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]string{ - "value": v, - }) - suite.NoError(err) - - suite.T().Run(v, func(t *testing.T) { - assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{})) - }) - } -} - -func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() { - testData := map[string]float64{ - "1": 1.0, - "1.0": 1.0, - "0.5": 0.5, - ".5": 0.5, - } - - 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 := &typeErrorRateTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.InEpsilon(t, value, testStruct.Value.Value, 1e-10) - }) - } -} - -func (suite *TypeErrorRateTestSuite) TestMarshalOk() { - testStruct := typeErrorRateTestStruct{ - Value: config2.TypeErrorRate{ - Value: 1.01, - }, - } - - encodedJson, err := json.Marshal(testStruct) - suite.NoError(err) - suite.JSONEq(`{"value": "1.01"}`, string(encodedJson)) -} - -func (suite *TypeErrorRateTestSuite) TestGet() { - value := config2.TypeErrorRate{} - suite.InEpsilon(1.0, value.Get(1.0), 1e-10) - - value.Value = 5.0 - suite.InEpsilon(5.0, value.Get(1.0), 1e-10) -} - -func TestTypeErrorRate(t *testing.T) { - t.Parallel() - suite.Run(t, &TypeErrorRateTestSuite{}) -} diff --git a/internal/config2/type_hostport.go b/internal/config2/type_hostport.go deleted file mode 100644 index 73d45c0..0000000 --- a/internal/config2/type_hostport.go +++ /dev/null @@ -1,59 +0,0 @@ -package config2 - -import ( - "fmt" - "net" - "strconv" -) - -type TypeHostPort struct { - Value string -} - -func (t *TypeHostPort) Set(value string) error { - host, port, err := net.SplitHostPort(value) - if err != nil { - return fmt.Errorf("incorrect host:port value (%v): %w", value, err) - } - - portValue, err := strconv.ParseUint(port, 10, 16) - if err != nil { - return fmt.Errorf("incorrect port number (%v): %w", value, err) - } - - if portValue == 0 { - return fmt.Errorf("incorrect port number (%s)", value) - } - - 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) - - return nil -} - -func (t TypeHostPort) Get(defaultValue string) string { - if t.Value == "" { - return defaultValue - } - - 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/config2/type_hostport_test.go b/internal/config2/type_hostport_test.go deleted file mode 100644 index dfaf395..0000000 --- a/internal/config2/type_hostport_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeHostPortTestStruct struct { - Value config2.TypeHostPort `json:"value"` -} - -type TypeHostPortTestSuite struct { - suite.Suite -} - -func (suite *TypeHostPortTestSuite) TestUnmarshalFail() { - testData := []string{ - ":", - ":800", - "127.0.0.1:8000000", - "12...:80", - "", - "localhost", - "google.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, &typeHostPortTestStruct{})) - }) - } -} - -func (suite *TypeHostPortTestSuite) TestUnmarshalOk() { - testData := []string{ - "127.0.0.1:80", - "10.0.0.10:6553", - } - - 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.Value) - }) - } -} - -func (suite *TypeHostPortTestSuite) TestMarshalOk() { - testStruct := typeHostPortTestStruct{ - Value: config2.TypeHostPort{ - Value: "127.0.0.1:8000", - }, - } - - data, err := json.Marshal(testStruct) - suite.NoError(err) - suite.JSONEq(`{"value": "127.0.0.1:8000"}`, string(data)) -} - -func (suite *TypeHostPortTestSuite) TestGet() { - value := config2.TypeHostPort{} - suite.Equal("127.0.0.1:9000", value.Get("127.0.0.1:9000")) - - 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) { - t.Parallel() - suite.Run(t, &TypeHostPortTestSuite{}) -} diff --git a/internal/config2/type_http_path.go b/internal/config2/type_http_path.go deleted file mode 100644 index d31ac57..0000000 --- a/internal/config2/type_http_path.go +++ /dev/null @@ -1,33 +0,0 @@ -package config2 - -import "strings" - -type TypeHTTPPath struct { - Value string -} - -func (t *TypeHTTPPath) Set(value string) error { - t.Value = "/" + strings.Trim(value, "/") - - return nil -} - -func (t TypeHTTPPath) Get(defaultValue string) string { - if t.Value == "" { - return defaultValue - } - - 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/config2/type_http_path_test.go b/internal/config2/type_http_path_test.go deleted file mode 100644 index 014907e..0000000 --- a/internal/config2/type_http_path_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeHTTPPathTestStruct struct { - Value config2.TypeHTTPPath `json:"value"` -} - -type TypeHTTPPathTestSuite struct { - suite.Suite -} - -func (suite *TypeHTTPPathTestSuite) TestUnmarshalOk() { - testData := map[string]string{ - "": "/", - "/": "/", - "/path": "/path", - "path": "/path", - } - - 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 := &typeHTTPPathTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, value, testStruct.Value.Get("")) - }) - } -} - -func (suite *TypeHTTPPathTestSuite) TestMarshalOk() { - value := typeHTTPPathTestStruct{ - Value: config2.TypeHTTPPath{ - Value: "/path", - }, - } - - data, err := json.Marshal(value) - suite.NoError(err) - suite.JSONEq(`{"value": "/path"}`, string(data)) -} - -func (suite *TypeHTTPPathTestSuite) TestGet() { - value := config2.TypeHTTPPath{} - suite.Equal("/hello", value.Get("/hello")) - - suite.NoError(value.Set("/lalala")) - suite.Equal("/lalala", value.Get("/hello")) -} - -func TestTypeHTTPPath(t *testing.T) { - t.Parallel() - suite.Run(t, &TypeHTTPPathTestSuite{}) -} diff --git a/internal/config2/type_ip.go b/internal/config2/type_ip.go deleted file mode 100644 index 207d22e..0000000 --- a/internal/config2/type_ip.go +++ /dev/null @@ -1,45 +0,0 @@ -package config2 - -import ( - "fmt" - "net" -) - -type TypeIP struct { - Value net.IP -} - -func (t *TypeIP) Set(value string) error { - ip := net.ParseIP(value) - if ip == nil { - return fmt.Errorf("incorret ip %s", value) - } - - t.Value = ip - - return nil -} - -func (t *TypeIP) Get(defaultValue net.IP) net.IP { - if len(t.Value) == 0 { - return defaultValue - } - - 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/config2/type_ip_test.go b/internal/config2/type_ip_test.go deleted file mode 100644 index 4659cb0..0000000 --- a/internal/config2/type_ip_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "net" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeIPTestStruct struct { - Value config2.TypeIP `json:"value"` -} - -type TypeIPTestSuite struct { - suite.Suite -} - -func (suite *TypeIPTestSuite) TestUnmarshalFail() { - testData := []string{ - "", - "....", - "0...", - "300.200.200.800", - "[]", - } - - 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, &typeIPTestStruct{})) - }) - } -} - -func (suite *TypeIPTestSuite) TestUnmarshalOk() { - 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 k, v := range testData { - expected := v - - data, err := json.Marshal(map[string]string{ - "value": k, - }) - suite.NoError(err) - - suite.T().Run(k, func(t *testing.T) { - testStruct := &typeIPTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, expected, testStruct.Value.Get(nil).String()) - }) - } -} - -func (suite *TypeIPTestSuite) TestMarshalOk() { - testData := []string{ - "2001:db8:85a3::8a2e:370:7334", - "127.0.0.1", - } - - for _, v := range testData { - value := v - - suite.T().Run(v, func(t *testing.T) { - testStruct := &typeIPTestStruct{ - Value: config2.TypeIP{ - Value: net.ParseIP(value), - }, - } - - encodedJSON, err := json.Marshal(testStruct) - assert.NoError(t, err) - - expectedJSON, err := json.Marshal(map[string]string{ - "value": value, - }) - assert.NoError(t, err) - - assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) - }) - } -} - -func (suite *TypeIPTestSuite) TestGet() { - value := config2.TypeIP{} - suite.Equal("127.0.0.1", value.Get(net.ParseIP("127.0.0.1")).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) { - t.Parallel() - suite.Run(t, &TypeIPTestSuite{}) -} diff --git a/internal/config2/type_metric_prefix.go b/internal/config2/type_metric_prefix.go deleted file mode 100644 index d8507de..0000000 --- a/internal/config2/type_metric_prefix.go +++ /dev/null @@ -1,40 +0,0 @@ -package config2 - -import ( - "fmt" - "regexp" -) - -type TypeMetricPrefix struct { - Value string -} - -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) - } - - t.Value = value - - return nil -} - -func (t TypeMetricPrefix) Get(defaultValue string) string { - if t.Value == "" { - return defaultValue - } - - 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/config2/type_metric_prefix_test.go b/internal/config2/type_metric_prefix_test.go deleted file mode 100644 index bed6497..0000000 --- a/internal/config2/type_metric_prefix_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeMetricPrefixTestStruct struct { - Value config2.TypeMetricPrefix `json:"value"` -} - -type TypeMetricPrefixTestSuite struct { - suite.Suite -} - -func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() { - testData := []string{ - "", - "-", - "hello/world", - "lala*", - "++sdf++", - } - - 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, &typeMetricPrefixTestStruct{})) - }) - } -} - -func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() { - testStruct := &typeMetricPrefixTestStruct{} - suite.NoError(json.Unmarshal([]byte(`{"value": "mtg"}`), testStruct)) - suite.Equal("mtg", testStruct.Value.Get("lalala")) -} - -func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() { - testStruct := &typeMetricPrefixTestStruct{ - Value: config2.TypeMetricPrefix{ - Value: "mtg", - }, - } - - data, err := json.Marshal(testStruct) - suite.NoError(err) - suite.JSONEq(`{"value": "mtg"}`, string(data)) -} - -func (suite *TypeMetricPrefixTestSuite) TestGet() { - value := config2.TypeMetricPrefix{} - suite.Equal("lalala", value.Get("lalala")) - - value.Value = "mtg" - suite.Equal("mtg", value.Get("lalala")) -} - -func TestTypeMetricPrefix(t *testing.T) { - t.Parallel() - suite.Run(t, &TypeMetricPrefixTestSuite{}) -} diff --git a/internal/config2/type_port.go b/internal/config2/type_port.go deleted file mode 100644 index 3a307d9..0000000 --- a/internal/config2/type_port.go +++ /dev/null @@ -1,45 +0,0 @@ -package config2 - -import ( - "fmt" - "strconv" -) - -type TypePort struct { - Value uint16 -} - -func (t *TypePort) Set(value string) error { - portValue, err := strconv.ParseUint(value, 10, 16) - if err != nil { - return fmt.Errorf("incorrect port number (%v): %w", value, err) - } - - if portValue == 0 { - return fmt.Errorf("incorrect port number (%s)", value) - } - - t.Value = uint16(portValue) - - return nil -} - -func (t TypePort) Get(defaultValue uint16) uint16 { - if t.Value == 0 { - return defaultValue - } - - 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/config2/type_port_test.go b/internal/config2/type_port_test.go deleted file mode 100644 index 544a2e9..0000000 --- a/internal/config2/type_port_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typePortTestStruct struct { - Value config2.TypePort `json:"value"` -} - -type TypePortTestSuite struct { - suite.Suite -} - -func (suite *TypePortTestSuite) TestUnmarshalFail() { - testData := []string{ - "", - "port", - "0", - "-1", - "1.5", - "70000", - } - - 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, &typePortTestStruct{})) - }) - } -} - -func (suite *TypePortTestSuite) TestUnmarshalOk() { - testStruct := &typePortTestStruct{} - suite.NoError(json.Unmarshal([]byte(`{"value": 5}`), testStruct)) - suite.EqualValues(5, testStruct.Value.Value) -} - -func (suite *TypePortTestSuite) TestMarshalOk() { - testStruct := &typePortTestStruct{ - Value: config2.TypePort{ - Value: 10, - }, - } - - data, err := json.Marshal(testStruct) - suite.NoError(err) - suite.JSONEq(`{"value":10}`, string(data)) -} - -func (suite *TypePortTestSuite) TestGet() { - value := config2.TypePort{} - suite.EqualValues(10, value.Get(10)) - - value.Value = 100 - suite.EqualValues(100, value.Get(10)) -} - -func TestTypePort(t *testing.T) { - t.Parallel() - suite.Run(t, &TypePortTestSuite{}) -} diff --git a/internal/config2/type_prefer_ip.go b/internal/config2/type_prefer_ip.go deleted file mode 100644 index 3370a3a..0000000 --- a/internal/config2/type_prefer_ip.go +++ /dev/null @@ -1,62 +0,0 @@ -package config2 - -import ( - "fmt" - "strings" -) - -const ( - // TypePreferIPPreferIPv4 states that you prefer to use IPv4 addresses - // but IPv6 is also possible. - TypePreferIPPreferIPv4 = "prefer-ipv4" - - // TypePreferIPPreferIPv6 states that you prefer to use IPv6 addresses - // but IPv4 is also possible. - TypePreferIPPreferIPv6 = "prefer-ipv6" - - // TypePreferOnlyIPv4 states that you prefer to use IPv4 addresses - // only. - TypePreferOnlyIPv4 = "only-ipv4" - - // TypePreferOnlyIPv6 states that you prefer to use IPv6 addresses - // only. - TypePreferOnlyIPv6 = "only-ipv6" -) - -type TypePreferIP struct { - Value string -} - -func (t *TypePreferIP) Set(value string) error { - value = strings.ToLower(value) - - switch value { - case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, - TypePreferOnlyIPv4, TypePreferOnlyIPv6: - t.Value = value - - return nil - default: - return fmt.Errorf("unsupported ip preference: %s", value) - } -} - -func (t *TypePreferIP) Get(defaultValue string) string { - if t.Value == "" { - return defaultValue - } - - 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/config2/type_prefer_ip_test.go b/internal/config2/type_prefer_ip_test.go deleted file mode 100644 index 420ac13..0000000 --- a/internal/config2/type_prefer_ip_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "strings" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typePreferIPTestStruct struct { - Value config2.TypePreferIP `json:"value"` -} - -type TypePreferIPTestSuite struct { - suite.Suite -} - -func (suite *TypePreferIPTestSuite) TestUnmarshalFail() { - testData := []string{ - "", - "prefer", - "preferipv4", - config2.TypePreferIPPreferIPv4 + "_", - } - - 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, &typePreferIPTestStruct{})) - }) - } -} - -func (suite *TypePreferIPTestSuite) TestUnmarshalOk() { - testData := []string{ - config2.TypePreferIPPreferIPv4, - config2.TypePreferIPPreferIPv6, - config2.TypePreferOnlyIPv4, - config2.TypePreferOnlyIPv6, - strings.ToTitle(config2.TypePreferOnlyIPv4), - strings.ToTitle(config2.TypePreferOnlyIPv6), - strings.ToTitle(config2.TypePreferIPPreferIPv4), - strings.ToTitle(config2.TypePreferIPPreferIPv6), - } - - 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{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) - }) - } -} - -func (suite *TypePreferIPTestSuite) TestMarshalOk() { - testData := []string{ - config2.TypePreferIPPreferIPv4, - config2.TypePreferIPPreferIPv6, - config2.TypePreferOnlyIPv4, - config2.TypePreferOnlyIPv6, - } - - for _, v := range testData { - value := v - - suite.T().Run(v, func(t *testing.T) { - testStruct := &typePreferIPTestStruct{ - Value: config2.TypePreferIP{ - Value: value, - }, - } - - encodedJSON, err := json.Marshal(testStruct) - assert.NoError(t, err) - - expectedJSON, err := json.Marshal(map[string]string{ - "value": value, - }) - assert.NoError(t, err) - - assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) - }) - } -} - -func (suite *TypePreferIPTestSuite) TestGet() { - value := config2.TypePreferIP{} - suite.Equal(config2.TypePreferIPPreferIPv4, - value.Get(config2.TypePreferIPPreferIPv4)) - - suite.NoError(value.Set(config2.TypePreferIPPreferIPv6)) - suite.Equal(config2.TypePreferIPPreferIPv6, - value.Get(config2.TypePreferIPPreferIPv4)) -} - -func TestTypePreferIP(t *testing.T) { - t.Parallel() - suite.Run(t, &TypePreferIPTestSuite{}) -} diff --git a/internal/config2/type_statsd_tag_format.go b/internal/config2/type_statsd_tag_format.go deleted file mode 100644 index fb4f267..0000000 --- a/internal/config2/type_statsd_tag_format.go +++ /dev/null @@ -1,58 +0,0 @@ -package config2 - -import ( - "fmt" - "strings" -) - -const ( - // TypeStatsdTagFormatInfluxdb defines a tag format compatible with - // InfluxDB. - TypeStatsdTagFormatInfluxdb = "influxdb" - - // TypeStatsdTagFormatDatadog defines a tag format compatible with - // DataDog. - TypeStatsdTagFormatDatadog = "datadog" - - // TypeStatsdTagFormatGraphite defines a tag format compatible with - // Graphite. - TypeStatsdTagFormatGraphite = "graphite" -) - -type TypeStatsdTagFormat struct { - Value string -} - -func (t *TypeStatsdTagFormat) Set(value string) error { - lowercasedValue := strings.ToLower(value) - - switch lowercasedValue { - case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb, - TypeStatsdTagFormatGraphite: - t.Value = lowercasedValue - - return nil - default: - return fmt.Errorf("unknown tag format %s", value) - } -} - -func (t TypeStatsdTagFormat) Get(defaultValue string) string { - if t.Value == "" { - return defaultValue - } - - 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/config2/type_statsd_tag_format_test.go b/internal/config2/type_statsd_tag_format_test.go deleted file mode 100644 index fb75b26..0000000 --- a/internal/config2/type_statsd_tag_format_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package config2_test - -import ( - "encoding/json" - "strings" - "testing" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type typeStatsdTagFormatTestStruct struct { - Value config2.TypeStatsdTagFormat `json:"value"` -} - -type StatsdTagFormatTestSuite struct { - suite.Suite -} - -func (suite *StatsdTagFormatTestSuite) TestUnmarshalFail() { - testData := []string{ - "", - "dogdog", - } - - 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, &typeStatsdTagFormatTestStruct{})) - }) - } -} - -func (suite *StatsdTagFormatTestSuite) TestUnmarshalOk() { - testData := []string{ - config2.TypeStatsdTagFormatInfluxdb, - config2.TypeStatsdTagFormatGraphite, - config2.TypeStatsdTagFormatDatadog, - strings.ToUpper(config2.TypeStatsdTagFormatInfluxdb), - strings.ToUpper(config2.TypeStatsdTagFormatGraphite), - strings.ToUpper(config2.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{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.Equal(t, strings.ToLower(value), testStruct.Value.Value) - }) - } -} - -func (suite *StatsdTagFormatTestSuite) TestMarshalOk() { - testData := []string{ - config2.TypeStatsdTagFormatInfluxdb, - config2.TypeStatsdTagFormatGraphite, - config2.TypeStatsdTagFormatDatadog, - } - - for _, v := range testData { - value := v - - suite.T().Run(v, func(t *testing.T) { - testStruct := &typeStatsdTagFormatTestStruct{ - Value: config2.TypeStatsdTagFormat{ - Value: value, - }, - } - - encodedJSON, err := json.Marshal(testStruct) - assert.NoError(t, err) - - expectedJSON, err := json.Marshal(map[string]string{ - "value": value, - }) - assert.NoError(t, err) - - assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) - }) - } -} - -func (suite *StatsdTagFormatTestSuite) TestGet() { - value := config2.TypeStatsdTagFormat{} - suite.Equal(config2.TypeStatsdTagFormatDatadog, - value.Get(config2.TypeStatsdTagFormatDatadog)) - - suite.NoError(value.Set(config2.TypeStatsdTagFormatInfluxdb)) - suite.Equal(config2.TypeStatsdTagFormatInfluxdb, - value.Get(config2.TypeStatsdTagFormatDatadog)) -} - -func TestTypeStatsdTagFormat(t *testing.T) { - t.Parallel() - suite.Run(t, &StatsdTagFormatTestSuite{}) -} From 3fd5e9eb19d7e0ad9aca40e08fd7218523abbf49 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Thu, 29 Jul 2021 18:33:58 +0300 Subject: [PATCH 3/8] Rework cli --- internal/cli/access.go | 71 ++++---- internal/cli/access_test.go | 197 ---------------------- internal/cli/base.go | 81 --------- internal/cli/base_internal_test.go | 33 ---- internal/cli/generate_secret_test.go | 51 ------ internal/cli/init_test.go | 37 ----- internal/cli/proxy.go | 155 +----------------- internal/cli/run_proxy.go | 208 ++++++++++++++++++++++++ internal/cli/utils.go | 66 -------- internal/config/type_bool.go | 17 +- internal/config/type_bool_test.go | 52 +++--- internal/config/type_error_rate.go | 4 +- internal/config/type_error_rate_test.go | 29 +--- internal/config/type_hostport.go | 4 + internal/utils/make_qr_code_url.go | 19 +++ internal/utils/read_config.go | 26 +++ mtglib/proxy_opts.go | 2 +- 17 files changed, 333 insertions(+), 719 deletions(-) delete mode 100644 internal/cli/access_test.go delete mode 100644 internal/cli/base.go delete mode 100644 internal/cli/base_internal_test.go delete mode 100644 internal/cli/generate_secret_test.go delete mode 100644 internal/cli/init_test.go create mode 100644 internal/cli/run_proxy.go delete mode 100644 internal/cli/utils.go create mode 100644 internal/utils/make_qr_code_url.go create mode 100644 internal/utils/read_config.go diff --git a/internal/cli/access.go b/internal/cli/access.go index 15e6bfa..6628269 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,8 +37,7 @@ 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 @@ -42,17 +45,19 @@ type Access struct { } func (c *Access) Run(cli *CLI, version string) error { - if err := c.ReadConfig(version); err != nil { + conf, err := utils.ReadConfig(c.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 := c.PublicIPv4 if ip == nil { - ip = c.getIP("tcp4") + ip = c.getIP(ntw, "tcp4") } if ip != nil { ip = ip.To4() } - resp.IPv4 = c.makeURLs(ip, cli) + resp.IPv4 = c.makeURLs(conf, ip) }() go func() { defer wg.Done() - ip := cli.Access.PublicIPv6 + ip := c.PublicIPv6 if ip == nil { - ip = c.getIP("tcp6") + ip = c.getIP(ntw, "tcp6") } if ip != nil { ip = ip.To16() } - resp.IPv6 = c.makeURLs(ip, cli) + resp.IPv6 = c.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 (c *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 (c *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs { if ip == nil { return nil } - portNo := cli.Access.Port + portNo := c.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 c.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/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 index b20b39d..1990185 100644 --- a/internal/cli/proxy.go +++ b/internal/cli/proxy.go @@ -2,166 +2,19 @@ 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 + ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll } func (c *Proxy) Run(cli *CLI, version string) error { - if err := c.ReadConfig(version); err != nil { + conf, err := utils.ReadConfig(c.ConfigPath) + if 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 + return runProxy(conf, version) } diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go new file mode 100644 index 0000000..4716bc1 --- /dev/null +++ b/internal/cli/run_proxy.go @@ -0,0 +1,208 @@ +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/utils.go b/internal/cli/utils.go deleted file mode 100644 index d07f047..0000000 --- a/internal/cli/utils.go +++ /dev/null @@ -1,66 +0,0 @@ -package cli - -import ( - "fmt" - "net" - "net/url" - "os" - - "github.com/9seconds/mtg/v2/internal/config2" - "github.com/9seconds/mtg/v2/mtglib" - "github.com/9seconds/mtg/v2/network" -) - -func readTOMLConfig(path string) (*config2.Config, error) { - content, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("cannot read config file: %w", err) - } - - conf, err := config2.Parse(content) - if err != nil { - return nil, fmt.Errorf("cannot parse config: %w", err) - } - - return conf, nil -} - -func makeNetwork(conf *config2.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 -} diff --git a/internal/config/type_bool.go b/internal/config/type_bool.go index 233a5a6..f1ec9d5 100644 --- a/internal/config/type_bool.go +++ b/internal/config/type_bool.go @@ -3,7 +3,6 @@ package config import ( "fmt" "strconv" - "strings" ) type TypeBool struct { @@ -11,15 +10,13 @@ type TypeBool struct { } func (t *TypeBool) Set(data string) error { - switch strings.ToLower(data) { - case "1", "y", "yes", "enabled", "true": - t.Value = true - case "0", "n", "no", "disabled", "false": - t.Value = false - default: - return fmt.Errorf("incorrect bool value %s", data) + parsed, err := strconv.ParseBool(data) + if err != nil { + return fmt.Errorf("incorrect bool value: %s", data) } + t.Value = parsed + return nil } @@ -27,11 +24,11 @@ func (t TypeBool) Get(defaultValue bool) bool { return t.Value || defaultValue } -func (t *TypeBool) UnmarshalText(data []byte) error { +func (t *TypeBool) UnmarshalJSON(data []byte) error { return t.Set(string(data)) } -func (t TypeBool) MarshalText() ([]byte, error) { +func (t TypeBool) MarshalJSON() ([]byte, error) { return []byte(t.String()), nil } diff --git a/internal/config/type_bool_test.go b/internal/config/type_bool_test.go index 2b90285..99d0488 100644 --- a/internal/config/type_bool_test.go +++ b/internal/config/type_bool_test.go @@ -20,53 +20,41 @@ type TypeBoolTestSuite struct { } func (suite *TypeBoolTestSuite) TestUnmarshalFail() { - testData := []string{ + testData := []interface{}{ "", "np", "нет", + int(10), + []int{}, } for _, v := range testData { - data, err := json.Marshal(map[string]string{ + data, err := json.Marshal(map[string]interface{}{ "value": v, }) suite.NoError(err) - suite.T().Run(v, func(t *testing.T) { + suite.T().Run(fmt.Sprintf("%v", v), func(t *testing.T) { assert.Error(t, json.Unmarshal(data, &typeBoolTestStruct{})) }) } } func (suite *TypeBoolTestSuite) TestUnmarshalOk() { - testData := map[string]bool{ - "0": false, - "N": false, - "nO": false, - "no": false, - "dISAbLEd": false, - "False": false, - "false": false, - - "1": true, - "y": true, - "Yes": true, - "yes": true, - "enABLED": true, - "True": true, - "TRUE": true, - "true": true, + testData := []bool{ + true, + false, } - for k, v := range testData { + for _, v := range testData { value := v - data, err := json.Marshal(map[string]string{ - "value": k, + data, err := json.Marshal(map[string]bool{ + "value": v, }) suite.NoError(err) - suite.T().Run(k, func(t *testing.T) { + suite.T().Run(strconv.FormatBool(v), func(t *testing.T) { testStruct := &typeBoolTestStruct{} assert.NoError(t, json.Unmarshal(data, testStruct)) @@ -81,18 +69,24 @@ func (suite *TypeBoolTestSuite) TestUnmarshalOk() { func (suite *TypeBoolTestSuite) TestMarshalOk() { for _, v := range []bool{true, false} { - name := strconv.FormatBool(v) + value := v - suite.T().Run(name, func(t *testing.T) { + suite.T().Run(strconv.FormatBool(v), func(t *testing.T) { testStruct := typeBoolTestStruct{ Value: config.TypeBool{ - Value: v, + Value: value, }, } - data, err := json.Marshal(testStruct) + encodedJSON, err := json.Marshal(testStruct) assert.NoError(t, err) - assert.JSONEq(t, fmt.Sprintf(`{"value": "%s"}`, name), string(data)) + + expectedJSON, err := json.Marshal(map[string]bool{ + "value": value, + }) + assert.NoError(t, err) + + assert.JSONEq(t, string(expectedJSON), string(encodedJSON)) }) } } diff --git a/internal/config/type_error_rate.go b/internal/config/type_error_rate.go index cfe6ebc..e950424 100644 --- a/internal/config/type_error_rate.go +++ b/internal/config/type_error_rate.go @@ -34,11 +34,11 @@ func (t TypeErrorRate) Get(defaultValue float64) float64 { return t.Value } -func (t *TypeErrorRate) UnmarshalText(data []byte) error { +func (t *TypeErrorRate) UnmarshalJSON(data []byte) error { return t.Set(string(data)) } -func (t TypeErrorRate) MarshalText() ([]byte, error) { +func (t TypeErrorRate) MarshalJSON() ([]byte, error) { return []byte(t.String()), nil } diff --git a/internal/config/type_error_rate_test.go b/internal/config/type_error_rate_test.go index aebe8f0..c45ffa7 100644 --- a/internal/config/type_error_rate_test.go +++ b/internal/config/type_error_rate_test.go @@ -45,27 +45,14 @@ func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() { } func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() { - testData := map[string]float64{ - "1": 1.0, - "1.0": 1.0, - "0.5": 0.5, - ".5": 0.5, - } + data, err := json.Marshal(map[string]float64{ + "value": 1.0, + }) + suite.NoError(err) - 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 := &typeErrorRateTestStruct{} - assert.NoError(t, json.Unmarshal(data, testStruct)) - assert.InEpsilon(t, value, testStruct.Value.Value, 1e-10) - }) - } + testStruct := &typeErrorRateTestStruct{} + suite.NoError(json.Unmarshal(data, testStruct)) + suite.InEpsilon(1.0, testStruct.Value.Value, 1e-10) } func (suite *TypeErrorRateTestSuite) TestMarshalOk() { @@ -77,7 +64,7 @@ func (suite *TypeErrorRateTestSuite) TestMarshalOk() { encodedJson, err := json.Marshal(testStruct) suite.NoError(err) - suite.JSONEq(`{"value": "1.01"}`, string(encodedJson)) + suite.JSONEq(`{"value": 1.01}`, string(encodedJson)) } func (suite *TypeErrorRateTestSuite) TestGet() { diff --git a/internal/config/type_hostport.go b/internal/config/type_hostport.go index 9f006a7..665ba10 100644 --- a/internal/config/type_hostport.go +++ b/internal/config/type_hostport.go @@ -8,6 +8,8 @@ import ( type TypeHostPort struct { Value string + Host string + Port uint } func (t *TypeHostPort) Set(value string) error { @@ -34,6 +36,8 @@ func (t *TypeHostPort) Set(value string) error { } t.Value = net.JoinHostPort(host, port) + t.Port = uint(portValue) + t.Host = host return nil } diff --git a/internal/utils/make_qr_code_url.go b/internal/utils/make_qr_code_url.go new file mode 100644 index 0000000..752986f --- /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/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/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index dc9b57b..d747286 100644 --- a/mtglib/proxy_opts.go +++ b/mtglib/proxy_opts.go @@ -62,7 +62,7 @@ type ProxyOpts struct { // specifies a hostname only. // // This is an optional setting. - DomainFrontingPort uint + DomainFrontingPort uint16 // IdleTimeout is a timeout for relay when we have to break a // stream. From ed91290e4742081ca44fc4394524fdce4ff8161a Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 30 Jul 2021 15:07:35 +0300 Subject: [PATCH 4/8] Add test for reading config --- internal/utils/read_config_test.go | 55 +++++++++++++++++++ internal/utils/testdata/broken.toml | 1 + internal/utils/testdata/empty.toml | 0 internal/{cli => utils}/testdata/minimal.toml | 0 internal/utils/testdata/missed-bindto.toml | 2 + internal/utils/testdata/missed-secret.toml | 1 + 6 files changed, 59 insertions(+) create mode 100644 internal/utils/read_config_test.go create mode 100644 internal/utils/testdata/broken.toml create mode 100644 internal/utils/testdata/empty.toml rename internal/{cli => utils}/testdata/minimal.toml (100%) create mode 100644 internal/utils/testdata/missed-bindto.toml create mode 100644 internal/utils/testdata/missed-secret.toml 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" From c53364d9520fadf544db5ea063c9c83360540964 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 30 Jul 2021 15:18:26 +0300 Subject: [PATCH 5/8] Add test for making QR code url --- internal/utils/make_qr_code_url.go | 2 +- internal/utils/make_qr_code_url_test.go | 31 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 internal/utils/make_qr_code_url_test.go diff --git a/internal/utils/make_qr_code_url.go b/internal/utils/make_qr_code_url.go index 752986f..fd140fb 100644 --- a/internal/utils/make_qr_code_url.go +++ b/internal/utils/make_qr_code_url.go @@ -11,7 +11,7 @@ func MakeQRCodeURL(data string) string { rv := url.URL{ Scheme: "https", Host: "api.qrserver.com", - Path: "v1/create-qr-code", + Path: "/v1/create-qr-code", RawQuery: values.Encode(), } 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{}) +} From c85c88efd67acd0689872c3d706c9b8b944c582a Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 30 Jul 2021 16:14:58 +0300 Subject: [PATCH 6/8] Add simple-run command --- internal/cli/access.go | 24 ++++----- internal/cli/cli.go | 3 +- internal/cli/generate_secret.go | 4 +- internal/cli/{proxy.go => run.go} | 6 +-- internal/cli/simple_run.go | 84 +++++++++++++++++++++++++++++++ mtglib/secret.go | 5 +- 6 files changed, 107 insertions(+), 19 deletions(-) rename internal/cli/{proxy.go => run.go} (73%) create mode 100644 internal/cli/simple_run.go diff --git a/internal/cli/access.go b/internal/cli/access.go index 6628269..23a47ce 100644 --- a/internal/cli/access.go +++ b/internal/cli/access.go @@ -44,8 +44,8 @@ type Access struct { Hex bool `kong:"help='Print secret in hex encoding.',short='x'"` } -func (c *Access) Run(cli *CLI, version string) error { - conf, err := utils.ReadConfig(c.ConfigPath) +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) } @@ -65,31 +65,31 @@ func (c *Access) Run(cli *CLI, version string) error { go func() { defer wg.Done() - ip := c.PublicIPv4 + ip := a.PublicIPv4 if ip == nil { - ip = c.getIP(ntw, "tcp4") + ip = a.getIP(ntw, "tcp4") } if ip != nil { ip = ip.To4() } - resp.IPv4 = c.makeURLs(conf, ip) + resp.IPv4 = a.makeURLs(conf, ip) }() go func() { defer wg.Done() - ip := c.PublicIPv6 + ip := a.PublicIPv6 if ip == nil { - ip = c.getIP(ntw, "tcp6") + ip = a.getIP(ntw, "tcp6") } if ip != nil { ip = ip.To16() } - resp.IPv6 = c.makeURLs(conf, ip) + resp.IPv6 = a.makeURLs(conf, ip) }() wg.Wait() @@ -105,7 +105,7 @@ func (c *Access) Run(cli *CLI, version string) error { return nil } -func (c *Access) getIP(ntw mtglib.Network, protocol string) net.IP { +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 }) @@ -139,12 +139,12 @@ func (c *Access) getIP(ntw mtglib.Network, protocol string) net.IP { return net.ParseIP(strings.TrimSpace(string(data))) } -func (c *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs { +func (a *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs { if ip == nil { return nil } - portNo := c.Port + portNo := a.Port if portNo == 0 { portNo = conf.BindTo.Port } @@ -153,7 +153,7 @@ func (c *Access) makeURLs(conf *config.Config, ip net.IP) *accessResponseURLs { values.Set("server", ip.String()) values.Set("port", strconv.Itoa(int(portNo))) - if c.Hex { + if a.Hex { values.Set("secret", conf.Secret.Hex()) } else { values.Set("secret", conf.Secret.Base64()) 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/proxy.go b/internal/cli/run.go similarity index 73% rename from internal/cli/proxy.go rename to internal/cli/run.go index 1990185..644f349 100644 --- a/internal/cli/proxy.go +++ b/internal/cli/run.go @@ -6,12 +6,12 @@ import ( "github.com/9seconds/mtg/v2/internal/utils" ) -type Proxy struct { +type Run struct { ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll } -func (c *Proxy) Run(cli *CLI, version string) error { - conf, err := utils.ReadConfig(c.ConfigPath) +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) } diff --git a/internal/cli/simple_run.go b/internal/cli/simple_run.go new file mode 100644 index 0000000..167b9c4 --- /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.'"` + Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` + TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Size of TCP buffer to use.'"` + PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` + DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` + DOHIP net.IP `kong:"name='doh-ip',short='d',default='9.9.9.9',help='IP address of DNS-over-HTTP to use.'"` + Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` + AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` +} + +func (s *SimpleRun) Run(cli *CLI, version string) error { + 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/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 } From 1050ca0b97af9c2ae7d59be79d3826c8bb623e90 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 30 Jul 2021 16:27:28 +0300 Subject: [PATCH 7/8] Fix lint issues --- internal/cli/run.go | 2 +- internal/cli/run_proxy.go | 1 + internal/cli/simple_run.go | 18 +++++++++--------- internal/config/config.go | 2 +- internal/config/config_test.go | 2 +- internal/config/type_blocklist_uri.go | 12 ++++++------ internal/config/type_blocklist_uri_test.go | 6 +++--- internal/config/type_bytes.go | 2 +- internal/config/type_bytes_test.go | 18 +++++++++--------- internal/config/type_concurrency.go | 4 ++-- internal/config/type_duration_test.go | 4 ++-- internal/config/type_error_rate.go | 4 ++-- internal/config/type_error_rate_test.go | 4 ++-- internal/config/type_ip.go | 2 +- internal/config/type_metric_prefix.go | 4 ++-- internal/config/type_port.go | 6 +++--- internal/config/type_prefer_ip.go | 2 +- internal/config/type_proxy_url.go | 2 +- internal/config/type_statsd_tag_format.go | 2 +- mtglib/proxy_opts.go | 18 +++++++++--------- 20 files changed, 58 insertions(+), 57 deletions(-) diff --git a/internal/cli/run.go b/internal/cli/run.go index 644f349..390f30a 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -16,5 +16,5 @@ func (r *Run) Run(cli *CLI, version string) error { return fmt.Errorf("cannot init config: %w", err) } - return runProxy(conf, version) + return runProxy(conf, version) } diff --git a/internal/cli/run_proxy.go b/internal/cli/run_proxy.go index 4716bc1..745319f 100644 --- a/internal/cli/run_proxy.go +++ b/internal/cli/run_proxy.go @@ -51,6 +51,7 @@ func makeNetwork(conf *config.Config, version string) (mtglib.Network, error) { } 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) diff --git a/internal/cli/simple_run.go b/internal/cli/simple_run.go index 167b9c4..4d1726a 100644 --- a/internal/cli/simple_run.go +++ b/internal/cli/simple_run.go @@ -13,17 +13,17 @@ 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.'"` - Concurrency uint64 `kong:"name='concurrency',short='c',default='8192',help='Max number of concurrent connection to proxy.'"` - TCPBuffer string `kong:"name='tcp-buffer',short='b',default='4KB',help='Size of TCP buffer to use.'"` - PreferIP string `kong:"name='prefer-ip',short='i',default='prefer-ipv6',help='IP preference. By default we prefer IPv6 with fallback to IPv4.'"` - DomainFrontingPort uint64 `kong:"name='domain-fronting-port',short='p',default='443',help='A port to access for domain fronting.'"` - DOHIP net.IP `kong:"name='doh-ip',short='d',default='9.9.9.9',help='IP address of DNS-over-HTTP to use.'"` - Timeout time.Duration `kong:"name='timeout',short='t',default='10s',help='Network timeout to use'"` - AntiReplayCacheSize string `kong:"name='antireplay-cache-size',short='a',default='1MB',help='A size of anti-replay cache to use.'"` + 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 { +func (s *SimpleRun) Run(cli *CLI, version string) error { // nolint: cyclop conf := &config.Config{} if err := conf.BindTo.Set(s.BindTo); err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index c05d33f..6230758 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,7 +60,7 @@ func (c *Config) Validate() error { return fmt.Errorf("invalid secret %s", c.Secret.String()) } - if c.BindTo.Get("") == "" { + if c.BindTo.Get("") == "" { return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String()) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f191221..e6b19e0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,7 +10,7 @@ import ( ) type ConfigTestSuite struct { - suite.Suite + suite.Suite } func (suite *ConfigTestSuite) ReadConfig(filename string) []byte { diff --git a/internal/config/type_blocklist_uri.go b/internal/config/type_blocklist_uri.go index 8bd3677..620ea51 100644 --- a/internal/config/type_blocklist_uri.go +++ b/internal/config/type_blocklist_uri.go @@ -13,12 +13,12 @@ type TypeBlocklistURI struct { 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") - } + 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") + } value, err = filepath.Abs(value) if err != nil { diff --git a/internal/config/type_blocklist_uri_test.go b/internal/config/type_blocklist_uri_test.go index 407beda..3cf76bc 100644 --- a/internal/config/type_blocklist_uri_test.go +++ b/internal/config/type_blocklist_uri_test.go @@ -102,9 +102,9 @@ 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("")) + 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) { diff --git a/internal/config/type_bytes.go b/internal/config/type_bytes.go index 412f019..254d556 100644 --- a/internal/config/type_bytes.go +++ b/internal/config/type_bytes.go @@ -14,7 +14,7 @@ type TypeBytes struct { } func (t *TypeBytes) Set(value string) error { - normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) + normalizedValue := typeBytesStringCleaner.Replace(strings.ToUpper(value)) parsedValue, err := units.ParseBase2Bytes(normalizedValue) if err != nil { diff --git a/internal/config/type_bytes_test.go b/internal/config/type_bytes_test.go index 08874f4..bee0922 100644 --- a/internal/config/type_bytes_test.go +++ b/internal/config/type_bytes_test.go @@ -64,20 +64,20 @@ func (suite *TypeBytesTestSuite) TestUnmarshalOk() { } func (suite *TypeBytesTestSuite) TestMarshalOk() { - value := typeBytesTestStruct{} - suite.NoError(value.Value.Set("1kib")) + value := typeBytesTestStruct{} + suite.NoError(value.Value.Set("1kib")) - data, err := json.Marshal(value) - suite.NoError(err) - suite.JSONEq(`{"value": "1kib"}`, string(data)) + data, err := json.Marshal(value) + suite.NoError(err) + suite.JSONEq(`{"value": "1kib"}`, string(data)) } func (suite *TypeBytesTestSuite) TestGet() { - value := config.TypeBytes{} - suite.EqualValues(1000, value.Get(1000)) + value := config.TypeBytes{} + suite.EqualValues(1000, value.Get(1000)) - suite.NoError(value.Set("1mib")) - suite.EqualValues(1048576, value.Get(1000)) + 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 index 1c172d7..4f39b3b 100644 --- a/internal/config/type_concurrency.go +++ b/internal/config/type_concurrency.go @@ -12,11 +12,11 @@ type TypeConcurrency struct { 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) + return fmt.Errorf("value is not uint (%s): %w", value, err) } if concurrencyValue == 0 { - return fmt.Errorf("Value should be >0 (%s)", value) + return fmt.Errorf("value should be >0 (%s)", value) } t.Value = uint(concurrencyValue) diff --git a/internal/config/type_duration_test.go b/internal/config/type_duration_test.go index 49d674a..5d0e204 100644 --- a/internal/config/type_duration_test.go +++ b/internal/config/type_duration_test.go @@ -86,12 +86,12 @@ func (suite *TypeDurationTestSuite) TestMarshalOk() { data, err := json.Marshal(testStruct) assert.NoError(t, err) - expectedJson, err := json.Marshal(map[string]string{ + expectedJSON, err := json.Marshal(map[string]string{ "value": expected, }) assert.NoError(t, err) - assert.JSONEq(t, string(expectedJson), string(data)) + assert.JSONEq(t, string(expectedJSON), string(data)) }) } } diff --git a/internal/config/type_error_rate.go b/internal/config/type_error_rate.go index e950424..4e30e09 100644 --- a/internal/config/type_error_rate.go +++ b/internal/config/type_error_rate.go @@ -14,11 +14,11 @@ type TypeErrorRate struct { func (t *TypeErrorRate) Set(value string) error { parsedValue, err := strconv.ParseFloat(value, 64) if err != nil { - return fmt.Errorf("Value is not a float (%s): %w", value, err) + return fmt.Errorf("value is not a float (%s): %w", value, err) } if parsedValue <= 0.0 || parsedValue >= 100.0 { - return fmt.Errorf("Value should be 0 < x < 100 (%s)", value) + return fmt.Errorf("value should be 0 < x < 100 (%s)", value) } t.Value = parsedValue diff --git a/internal/config/type_error_rate_test.go b/internal/config/type_error_rate_test.go index c45ffa7..44d17c2 100644 --- a/internal/config/type_error_rate_test.go +++ b/internal/config/type_error_rate_test.go @@ -62,9 +62,9 @@ func (suite *TypeErrorRateTestSuite) TestMarshalOk() { }, } - encodedJson, err := json.Marshal(testStruct) + encodedJSON, err := json.Marshal(testStruct) suite.NoError(err) - suite.JSONEq(`{"value": 1.01}`, string(encodedJson)) + suite.JSONEq(`{"value": 1.01}`, string(encodedJSON)) } func (suite *TypeErrorRateTestSuite) TestGet() { diff --git a/internal/config/type_ip.go b/internal/config/type_ip.go index 03a2f20..c637181 100644 --- a/internal/config/type_ip.go +++ b/internal/config/type_ip.go @@ -15,7 +15,7 @@ func (t *TypeIP) Set(value string) error { return fmt.Errorf("incorret ip %s", value) } - t.Value = ip + t.Value = ip return nil } diff --git a/internal/config/type_metric_prefix.go b/internal/config/type_metric_prefix.go index fcd951e..e55438c 100644 --- a/internal/config/type_metric_prefix.go +++ b/internal/config/type_metric_prefix.go @@ -11,10 +11,10 @@ type TypeMetricPrefix struct { 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) + return fmt.Errorf("incorrect metric prefix %s: %w", value, err) } - t.Value = value + t.Value = value return nil } diff --git a/internal/config/type_port.go b/internal/config/type_port.go index f53d77e..cdab9ee 100644 --- a/internal/config/type_port.go +++ b/internal/config/type_port.go @@ -6,7 +6,7 @@ import ( ) type TypePort struct { - Value uint16 + Value uint } func (t *TypePort) Set(value string) error { @@ -19,12 +19,12 @@ func (t *TypePort) Set(value string) error { return fmt.Errorf("incorrect port number (%s)", value) } - t.Value = uint16(portValue) + t.Value = uint(portValue) return nil } -func (t TypePort) Get(defaultValue uint16) uint16 { +func (t TypePort) Get(defaultValue uint) uint { if t.Value == 0 { return defaultValue } diff --git a/internal/config/type_prefer_ip.go b/internal/config/type_prefer_ip.go index b28a33f..8a07721 100644 --- a/internal/config/type_prefer_ip.go +++ b/internal/config/type_prefer_ip.go @@ -33,7 +33,7 @@ func (t *TypePreferIP) Set(value string) error { switch value { case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, TypePreferOnlyIPv4, TypePreferOnlyIPv6: - t.Value = value + t.Value = value return nil default: diff --git a/internal/config/type_proxy_url.go b/internal/config/type_proxy_url.go index f27d0af..f3d0231 100644 --- a/internal/config/type_proxy_url.go +++ b/internal/config/type_proxy_url.go @@ -15,7 +15,7 @@ type TypeProxyURL struct { 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) + return fmt.Errorf("value is not corect URL (%s): %w", value, err) } if parsedURL.Host == "" { diff --git a/internal/config/type_statsd_tag_format.go b/internal/config/type_statsd_tag_format.go index e449b9f..5301903 100644 --- a/internal/config/type_statsd_tag_format.go +++ b/internal/config/type_statsd_tag_format.go @@ -29,7 +29,7 @@ func (t *TypeStatsdTagFormat) Set(value string) error { switch lowercasedValue { case TypeStatsdTagFormatDatadog, TypeStatsdTagFormatInfluxdb, TypeStatsdTagFormatGraphite: - t.Value = lowercasedValue + t.Value = lowercasedValue return nil default: diff --git a/mtglib/proxy_opts.go b/mtglib/proxy_opts.go index d747286..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 uint16 - // 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. // From dcbbb4960790c15d47585cf85efc360a0a18814d Mon Sep 17 00:00:00 2001 From: 9seconds Date: Fri, 30 Jul 2021 16:34:09 +0300 Subject: [PATCH 8/8] Update README --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) 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