From 1a02511afe4b305dcc85339f7726a6d3f7d084c9 Mon Sep 17 00:00:00 2001 From: 9seconds Date: Thu, 11 Mar 2021 05:37:18 +0300 Subject: [PATCH] Move config into separate package --- cli.go | 17 +- cli_access.go | 26 +- config.go | 529 ----------------------------------- config/config.go | 151 ++++++++++ config/type_bytes.go | 47 ++++ config/type_duration.go | 46 +++ config/type_float.go | 41 +++ config/type_hostport.go | 56 ++++ config/type_http_path.go | 31 ++ config/type_ip.go | 45 +++ config/type_metric_prefix.go | 42 +++ config/type_port.go | 45 +++ config/type_prefer_ip.go | 43 +++ config/type_url.go | 45 +++ utils.go | 3 +- 15 files changed, 615 insertions(+), 552 deletions(-) delete mode 100644 config.go create mode 100644 config/config.go create mode 100644 config/type_bytes.go create mode 100644 config/type_duration.go create mode 100644 config/type_float.go create mode 100644 config/type_hostport.go create mode 100644 config/type_http_path.go create mode 100644 config/type_ip.go create mode 100644 config/type_metric_prefix.go create mode 100644 config/type_port.go create mode 100644 config/type_prefer_ip.go create mode 100644 config/type_url.go diff --git a/cli.go b/cli.go index d57b159..78373d3 100644 --- a/cli.go +++ b/cli.go @@ -2,25 +2,24 @@ package main import ( "fmt" - "os" + "io/ioutil" + "github.com/9seconds/mtg/v2/config" "github.com/9seconds/mtg/v2/mtglib/network" ) type cli struct { network network.Network - conf *config + conf *config.Config } func (c *cli) ReadConfig(path string) error { - filefp, err := os.Open(path) - if err != nil { - return fmt.Errorf("cannot open config file: %w", err) - } + content, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("cannot read config file: %w", err) + } - defer filefp.Close() - - conf, err := parseConfig(filefp) + conf, err := config.Parse(content) if err != nil { return fmt.Errorf("cannot parse config: %w", err) } diff --git a/cli_access.go b/cli_access.go index e0eea15..7036231 100644 --- a/cli_access.go +++ b/cli_access.go @@ -13,16 +13,16 @@ import ( "strings" ) -type runAccessResponse struct { - IPv4 *runAccessResponseURLs `json:"ipv4,omitempty"` - IPv6 *runAccessResponseURLs `json:"ipv6,omitempty"` +type accessResponse struct { + IPv4 *accessResponseURLs `json:"ipv4,omitempty"` + IPv6 *accessResponseURLs `json:"ipv6,omitempty"` Secret struct { Hex string `json:"hex"` Base64 string `json:"base64"` } `json:"secret"` } -type runAccessResponseURLs struct { +type accessResponseURLs struct { IP net.IP `json:"ip"` TgURL string `json:"tg_url"` TgQrCode string `json:"tg_qrcode"` @@ -53,9 +53,9 @@ func (c *cliCommandAccess) Run(cli *CLI) error { ipv6 = c.getIP("tcp6") } - resp := runAccessResponse{ - IPv4: c.makeResponseURLs(ipv4, cli), - IPv6: c.makeResponseURLs(ipv6, cli), + resp := accessResponse{ + IPv4: c.makeURLs(ipv4, cli), + IPv6: c.makeURLs(ipv6, cli), } resp.Secret.Base64 = c.conf.Secret.Base64() resp.Secret.Hex = c.conf.Secret.Hex() @@ -101,7 +101,7 @@ func (c *cliCommandAccess) getIP(protocol string) net.IP { return net.ParseIP(strings.TrimSpace(string(data))) } -func (c *cliCommandAccess) makeResponseURLs(ip net.IP, cli *CLI) *runAccessResponseURLs { +func (c *cliCommandAccess) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs { if ip == nil { return nil } @@ -109,7 +109,7 @@ func (c *cliCommandAccess) makeResponseURLs(ip net.IP, cli *CLI) *runAccessRespo values := url.Values{} values.Set("server", ip.String()) - values.Set("port", strconv.Itoa(int(c.conf.BindTo.port.Value(0)))) + values.Set("port", strconv.Itoa(int(c.conf.BindTo.PortValue(0)))) if cli.Access.Hex { values.Set("secret", c.conf.Secret.Hex()) @@ -119,7 +119,7 @@ func (c *cliCommandAccess) makeResponseURLs(ip net.IP, cli *CLI) *runAccessRespo urlQuery := values.Encode() - rv := &runAccessResponseURLs{ + rv := &accessResponseURLs{ IP: ip, TgURL: (&url.URL{ Scheme: "tg", @@ -134,13 +134,13 @@ func (c *cliCommandAccess) makeResponseURLs(ip net.IP, cli *CLI) *runAccessRespo }).String(), } - rv.TgQrCode = c.makeResponseQRCode(rv.TgURL) - rv.TmeQrCode = c.makeResponseQRCode(rv.TmeURL) + rv.TgQrCode = c.makeQRCode(rv.TgURL) + rv.TmeQrCode = c.makeQRCode(rv.TmeURL) return rv } -func (c *cliCommandAccess) makeResponseQRCode(data string) string { +func (c *cliCommandAccess) makeQRCode(data string) string { values := url.Values{} values.Set("qzone", "4") diff --git a/config.go b/config.go deleted file mode 100644 index b0c309a..0000000 --- a/config.go +++ /dev/null @@ -1,529 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net" - "net/url" - "regexp" - "strconv" - "strings" - "time" - - "github.com/9seconds/mtg/v2/mtglib" - "github.com/alecthomas/units" - "github.com/pelletier/go-toml" -) - -type configTypeHostPort struct { - host configTypeIP - port configTypePort -} - -func (c *configTypeHostPort) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - host, port, err := net.SplitHostPort(string(data)) - if err != nil { - return fmt.Errorf("incorrect host:port syntax: %w", err) - } - - if err := c.port.UnmarshalJSON([]byte(port)); err != nil { - return fmt.Errorf("incorrect port in host:port: %w", err) - } - - if err := c.host.UnmarshalText([]byte(host)); err != nil { - return fmt.Errorf("incorrect host: %w", err) - } - - return nil -} - -func (c configTypeHostPort) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeHostPort) String() string { - return c.Value(net.IP{}, 0) -} - -func (c configTypeHostPort) Value(defaultHostValue net.IP, defaultPortValue uint) string { - return net.JoinHostPort(c.host.Value(defaultHostValue).String(), - strconv.Itoa(int(c.port.Value(defaultPortValue)))) -} - -type configTypePort struct { - value uint -} - -func (c *configTypePort) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - return nil - } - - intValue, err := strconv.ParseUint(string(data), 10, 16) - if err != nil { - return fmt.Errorf("port number is not a number: %w", err) - } - - if intValue == 0 || intValue > 65536 { - return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue) - } - - c.value = uint(intValue) - - return nil -} - -func (c *configTypePort) MarshalJSON() ([]byte, error) { - return json.Marshal(c.value) -} - -func (c configTypePort) String() string { - return strconv.Itoa(int(c.value)) -} - -func (c configTypePort) Value(defaultValue uint) uint { - if c.value == 0 { - return defaultValue - } - - return c.value -} - -type configTypeBytes struct { - value uint -} - -func (c *configTypeBytes) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - value, err := units.ParseStrictBytes(strings.ToUpper(string(data))) - if err != nil { - return fmt.Errorf("incorrect bytes value: %w", err) - } - - if value < 0 { - return fmt.Errorf("%d should be positive number", value) - } - - c.value = uint(value) - - return nil -} - -func (c configTypeBytes) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeBytes) String() string { - return units.ToString(int64(c.value), 1024, "ib", "b") -} - -func (c configTypeBytes) Value(defaultValue uint) uint { - if c.value == 0 { - return defaultValue - } - - return c.value -} - -type configTypePreferIP struct { - value string -} - -func (c *configTypePreferIP) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - text := strings.ToLower(string(data)) - - switch text { - case "prefer-ipv4", "prefer-ipv6", "only-ipv4", "only-ipv6": - c.value = text - default: - return fmt.Errorf("incorrect prefer-ip value: %s", string(data)) - } - - return nil -} - -func (c configTypePreferIP) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.value), nil -} - -func (c *configTypePreferIP) String() string { - return c.value -} - -func (c *configTypePreferIP) Value(defaultValue string) string { - if c.value == "" { - return defaultValue - } - - return c.value -} - -type configTypeDuration struct { - value time.Duration -} - -func (c *configTypeDuration) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - dur, err := time.ParseDuration(strings.ToLower(string(data))) - if err != nil { - return fmt.Errorf("incorrect duration: %w", err) - } - - if dur < 0 { - return fmt.Errorf("%s should be positive duration", dur) - } - - c.value = dur - - return nil -} - -func (c configTypeDuration) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.value.String()), nil -} - -func (c configTypeDuration) String() string { - return c.value.String() -} - -func (c configTypeDuration) Value(defaultValue time.Duration) time.Duration { - if c.value == 0 { - return defaultValue - } - - return c.value -} - -type configTypeFloat struct { - value float64 -} - -func (c *configTypeFloat) UnmarshalJSON(data []byte) error { - value, err := strconv.ParseFloat(string(data), 64) - if err != nil { - return fmt.Errorf("incorrect float value: %w", err) - } - - if value < 0 { - return fmt.Errorf("%f should be positive", value) - } - - c.value = value - - return nil -} - -func (c *configTypeFloat) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeFloat) String() string { - return strconv.FormatFloat(c.value, 'f', -1, 64) -} - -func (c configTypeFloat) Value(defaultValue float64) float64 { - if c.value < 0.00001 { - return defaultValue - } - - return c.value -} - -type configTypeIP struct { - value net.IP -} - -func (c *configTypeIP) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - ip := net.ParseIP(string(data)) - if ip == nil { - return fmt.Errorf("incorrect ip address: %s", string(data)) - } - - c.value = ip - - return nil -} - -func (c *configTypeIP) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeIP) String() string { - if c.value == nil { - return "" - } - - return c.value.String() -} - -func (c configTypeIP) Value(defaultValue net.IP) net.IP { - if c.value == nil { - return defaultValue - } - - return c.value -} - -type configTypeURL struct { - value *url.URL -} - -func (c *configTypeURL) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - value, err := url.Parse(string(data)) - if err != nil { - return fmt.Errorf("incorrect URL: %w", err) - } - - c.value = value - - return nil -} - -func (c *configTypeURL) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeURL) String() string { - if c.value == nil { - return "" - } - - return c.value.String() -} - -func (c configTypeURL) Value(defaultValue *url.URL) *url.URL { - if c.value == nil { - return defaultValue - } - - return c.value -} - -type configTypeMetricPrefix struct { - value string -} - -func (c *configTypeMetricPrefix) UnmarshalText(data []byte) error { - if len(data) == 0 { - return nil - } - - 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 - - return nil -} - -func (c configTypeMetricPrefix) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeMetricPrefix) String() string { - return c.value -} - -func (c configTypeMetricPrefix) Value(defaultValue string) string { - if c.value == "" { - return defaultValue - } - - return c.value -} - -type configTypeHTTPPath struct { - value string -} - -func (c *configTypeHTTPPath) UnmarshalText(data []byte) error { // nolint: unparam - if len(data) > 0 { - c.value = "/" + strings.Trim(string(data), "/") - } - - return nil -} - -func (c configTypeHTTPPath) MarshalText() ([]byte, error) { // nolint: unparam - return []byte(c.String()), nil -} - -func (c configTypeHTTPPath) String() string { - return c.value -} - -func (c configTypeHTTPPath) Value(defaultValue string) string { - if c.value == "" { - return defaultValue - } - - return c.value -} - -type config struct { - Debug bool `json:"debug"` - Secret mtglib.Secret `json:"secret"` - BindTo configTypeHostPort `json:"bind-to"` - TCPBuffer configTypeBytes `json:"tcp-buffer"` - PreferIP configTypePreferIP `json:"prefer-ip"` - CloakPort configTypePort `json:"cloak-port"` - Probes struct { - Time struct { - Enabled bool `json:"enabled"` - AllowSkewness configTypeDuration `json:"allow-skewness"` - } `json:"time"` - AntiReplay struct { - Enabled bool `json:"enabled"` - MaxSize configTypeBytes `json:"max-size"` - ErrorRate configTypeFloat `json:"error-rate"` - } `json:"anti-replay"` - } `json:"probes"` - Network struct { - PublicIP struct { - IPv4 configTypeIP `json:"ipv4"` - IPv6 configTypeIP `json:"ipv6"` - } `json:"public-ip"` - Timeout struct { - TCP configTypeDuration `json:"tcp"` - Idle configTypeDuration `json:"idle"` - } `json:"timeout"` - DOHIP configTypeIP `json:"doh-ip"` - Proxies []configTypeURL `json:"proxies"` - } `json:"network"` - Stats struct { - StatsD struct { - Enabled bool `json:"enabled"` - Address configTypeHostPort `json:"address"` - MetricPrefix configTypeMetricPrefix `json:"metric-prefix"` - } `json:"statsd"` - Prometheus struct { - Enabled bool `json:"enabled"` - BindTo configTypeHostPort `json:"bind-to"` - HTTPPath configTypeHTTPPath `json:"http-path"` - MetricPrefix configTypeMetricPrefix `json:"metric-prefix"` - } `json:"prometheus"` - } `json:"stats"` -} - -func (c *config) Validate() error { - if len(c.Secret.Key) == 0 || c.Secret.Host == "" { - return fmt.Errorf("incorrect secret %s", c.Secret.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() -} - -type configRaw struct { - Debug bool `toml:"debug" json:"debug"` - Secret string `toml:"secret" json:"secret"` - BindTo string `toml:"bind-to" json:"bind-to"` - TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer"` - PreferIP string `toml:"prefer-ip" json:"prefer-ip"` - CloakPort uint `toml:"cloak-port" json:"cloak-port"` - Probes struct { - Time struct { - Enabled bool `toml:"enabled" json:"enabled"` - AllowSkewness string `toml:"allow-skewness" json:"allow-skewness"` - } `toml:"time" json:"time"` - AntiReplay struct { - Enabled bool `toml:"enabled" json:"enabled"` - MaxSize string `toml:"max-size" json:"max-size"` - ErrorRate float64 `toml:"error-rate" json:"error-rate"` - } `toml:"anti-replay" json:"anti-replay"` - } `toml:"probes" json:"probes"` - Network struct { - PublicIP struct { - IPv4 string `toml:"ipv4" json:"ipv4"` - IPv6 string `toml:"ipv6" json:"ipv6"` - } `toml:"public-ip" json:"public-ip"` - Timeout struct { - TCP string `toml:"tcp" json:"tcp"` - Idle string `toml:"idle" json:"idle"` - } `toml:"timeout" json:"timeout"` - DOHIP string `toml:"doh-ip" json:"doh-ip"` - Proxies []string `toml:"proxies" json:"proxies"` - } `toml:"network" json:"network"` - Stats struct { - StatsD struct { - Enabled bool `toml:"enabled" json:"enabled"` - Address string `toml:"address" json:"address"` - MetricPrefix string `toml:"metric-prefix" json:"metric-prefix"` - } `toml:"statsd" json:"statsd"` - Prometheus struct { - Enabled bool `toml:"enabled" json:"enabled"` - BindTo string `toml:"bind-to" json:"bind-to"` - HTTPPath string `toml:"http-path" json:"http-path"` - MetricPrefix string `toml:"metric-prefix" json:"metric-prefix"` - } `toml:"prometheus" json:"prometheus"` - } `toml:"stats" json:"stats"` -} - -func parseConfig(reader io.Reader) (*config, error) { - rawConf := &configRaw{} - - if err := toml.NewDecoder(reader).Decode(rawConf); err != nil { - return nil, fmt.Errorf("cannot parse toml config: %w", err) - } - - jsonBuf := &bytes.Buffer{} - jsonEncoder := json.NewEncoder(jsonBuf) - - jsonEncoder.SetEscapeHTML(false) - jsonEncoder.SetIndent("", "") - - if err := jsonEncoder.Encode(rawConf); err != nil { - return nil, fmt.Errorf("cannot dump into interim format: %w", err) - } - - conf := &config{} - - if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil { - return nil, fmt.Errorf("cannot parse final config: %w", err) - } - - if err := conf.Validate(); err != nil { - return nil, fmt.Errorf("cannot validate config: %w", err) - } - - return conf, nil -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..ab4f5fe --- /dev/null +++ b/config/config.go @@ -0,0 +1,151 @@ +package config + +import ( + "bytes" + "encoding/json" + "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:"bind-to"` + TCPBuffer TypeBytes `json:"tcp-buffer"` + PreferIP TypePreferIP `json:"prefer-ip"` + CloakPort TypePort `json:"cloak-port"` + Probes struct { + Time struct { + Enabled bool `json:"enabled"` + AllowSkewness TypeDuration `json:"allow-skewness"` + } `json:"time"` + AntiReplay struct { + Enabled bool `json:"enabled"` + MaxSize TypeBytes `json:"max-size"` + ErrorRate TypeFloat `json:"error-rate"` + } `json:"anti-replay"` + } `json:"probes"` + Network struct { + PublicIP struct { + IPv4 TypeIP `json:"ipv4"` + IPv6 TypeIP `json:"ipv6"` + } `json:"public-ip"` + Timeout struct { + TCP TypeDuration `json:"tcp"` + Idle TypeDuration `json:"idle"` + } `json:"timeout"` + DOHIP TypeIP `json:"doh-ip"` + Proxies []TypeURL `json:"proxies"` + } `json:"network"` + Stats struct { + StatsD struct { + Enabled bool `json:"enabled"` + Address TypeHostPort `json:"address"` + MetricPrefix TypeMetricPrefix `json:"metric-prefix"` + } `json:"statsd"` + Prometheus struct { + Enabled bool `json:"enabled"` + BindTo TypeHostPort `json:"bind-to"` + HTTPPath TypeHTTPPath `json:"http-path"` + MetricPrefix TypeMetricPrefix `json:"metric-prefix"` + } `json:"prometheus"` + } `json:"stats"` +} + +func (c *Config) Validate() error { + if len(c.Secret.Key) == 0 || c.Secret.Host == "" { + return fmt.Errorf("incorrect secret %s", c.Secret.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() +} + +type configRaw struct { + Debug bool `toml:"debug" json:"debug"` + Secret string `toml:"secret" json:"secret"` + BindTo string `toml:"bind-to" json:"bind-to"` + TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer"` + PreferIP string `toml:"prefer-ip" json:"prefer-ip"` + CloakPort uint `toml:"cloak-port" json:"cloak-port"` + Probes struct { + Time struct { + Enabled bool `toml:"enabled" json:"enabled"` + AllowSkewness string `toml:"allow-skewness" json:"allow-skewness"` + } `toml:"time" json:"time"` + AntiReplay struct { + Enabled bool `toml:"enabled" json:"enabled"` + MaxSize string `toml:"max-size" json:"max-size"` + ErrorRate float64 `toml:"error-rate" json:"error-rate"` + } `toml:"anti-replay" json:"anti-replay"` + } `toml:"probes" json:"probes"` + Network struct { + PublicIP struct { + IPv4 string `toml:"ipv4" json:"ipv4"` + IPv6 string `toml:"ipv6" json:"ipv6"` + } `toml:"public-ip" json:"public-ip"` + Timeout struct { + TCP string `toml:"tcp" json:"tcp"` + Idle string `toml:"idle" json:"idle"` + } `toml:"timeout" json:"timeout"` + DOHIP string `toml:"doh-ip" json:"doh-ip"` + Proxies []string `toml:"proxies" json:"proxies"` + } `toml:"network" json:"network"` + Stats struct { + StatsD struct { + Enabled bool `toml:"enabled" json:"enabled"` + Address string `toml:"address" json:"address"` + MetricPrefix string `toml:"metric-prefix" json:"metric-prefix"` + } `toml:"statsd" json:"statsd"` + Prometheus struct { + Enabled bool `toml:"enabled" json:"enabled"` + BindTo string `toml:"bind-to" json:"bind-to"` + HTTPPath string `toml:"http-path" json:"http-path"` + MetricPrefix string `toml:"metric-prefix" json:"metric-prefix"` + } `toml:"prometheus" json:"prometheus"` + } `toml:"stats" json:"stats"` +} + +func Parse(rawData []byte) (*Config, error) { + rawConf := &configRaw{} + + if err := toml.Unmarshal(rawData, rawConf); err != nil { + return nil, fmt.Errorf("cannot parse toml config: %w", err) + } + + jsonBuf := &bytes.Buffer{} + jsonEncoder := json.NewEncoder(jsonBuf) + + jsonEncoder.SetEscapeHTML(false) + jsonEncoder.SetIndent("", "") + + if err := jsonEncoder.Encode(rawConf); err != nil { + return nil, fmt.Errorf("cannot dump into interim format: %w", err) + } + + conf := &Config{} + + if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil { + return nil, fmt.Errorf("cannot parse final config: %w", err) + } + + if err := conf.Validate(); err != nil { + return nil, fmt.Errorf("cannot validate config: %w", err) + } + + return conf, nil +} diff --git a/config/type_bytes.go b/config/type_bytes.go new file mode 100644 index 0000000..585ee86 --- /dev/null +++ b/config/type_bytes.go @@ -0,0 +1,47 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/alecthomas/units" +) + +type TypeBytes struct { + value uint +} + +func (c *TypeBytes) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + value, err := units.ParseStrictBytes(strings.ToUpper(string(data))) + if err != nil { + return fmt.Errorf("incorrect bytes value: %w", err) + } + + if value < 0 { + return fmt.Errorf("%d should be positive number", value) + } + + c.value = uint(value) + + return nil +} + +func (c TypeBytes) MarshalText() ([]byte, error) { // nolint: unparam + return []byte(c.String()), nil +} + +func (c TypeBytes) String() string { + return units.ToString(int64(c.value), 1024, "ib", "b") +} + +func (c TypeBytes) Value(defaultValue uint) uint { + if c.value == 0 { + return defaultValue + } + + return c.value +} diff --git a/config/type_duration.go b/config/type_duration.go new file mode 100644 index 0000000..f5dbbab --- /dev/null +++ b/config/type_duration.go @@ -0,0 +1,46 @@ +package config + +import ( + "fmt" + "strings" + "time" +) + +type TypeDuration struct { + value time.Duration +} + +func (c *TypeDuration) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + dur, err := time.ParseDuration(strings.ToLower(string(data))) + if err != nil { + return fmt.Errorf("incorrect duration: %w", err) + } + + if dur < 0 { + return fmt.Errorf("%s should be positive duration", dur) + } + + c.value = dur + + return nil +} + +func (c TypeDuration) MarshalText() ([]byte, error) { // nolint: unparam + 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 { + return defaultValue + } + + return c.value +} diff --git a/config/type_float.go b/config/type_float.go new file mode 100644 index 0000000..bf5aff6 --- /dev/null +++ b/config/type_float.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "strconv" +) + +type TypeFloat struct { + value float64 +} + +func (c *TypeFloat) UnmarshalJSON(data []byte) error { + value, err := strconv.ParseFloat(string(data), 64) + if err != nil { + return fmt.Errorf("incorrect float value: %w", err) + } + + if value < 0 { + return fmt.Errorf("%f should be positive", value) + } + + c.value = value + + return nil +} + +func (c *TypeFloat) MarshalText() ([]byte, error) { // nolint: unparam + return []byte(c.String()), nil +} + +func (c TypeFloat) String() string { + return strconv.FormatFloat(c.value, 'f', -1, 64) +} + +func (c TypeFloat) Value(defaultValue float64) float64 { + if c.value < 0.00001 { + return defaultValue + } + + return c.value +} diff --git a/config/type_hostport.go b/config/type_hostport.go new file mode 100644 index 0000000..42a9fc7 --- /dev/null +++ b/config/type_hostport.go @@ -0,0 +1,56 @@ +package config + +import ( + "fmt" + "net" + "strconv" +) + +type TypeHostPort struct { + host TypeIP + port TypePort +} + +func (c *TypeHostPort) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + host, port, err := net.SplitHostPort(string(data)) + if err != nil { + return fmt.Errorf("incorrect host:port syntax: %w", err) + } + + if err := c.port.UnmarshalJSON([]byte(port)); err != nil { + return fmt.Errorf("incorrect port in host:port: %w", err) + } + + if err := c.host.UnmarshalText([]byte(host)); err != nil { + return fmt.Errorf("incorrect host: %w", err) + } + + return nil +} + +func (c TypeHostPort) MarshalText() ([]byte, error) { // nolint: unparam + 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) + + return net.JoinHostPort(host.String(), strconv.Itoa(int(port))) +} diff --git a/config/type_http_path.go b/config/type_http_path.go new file mode 100644 index 0000000..79d311a --- /dev/null +++ b/config/type_http_path.go @@ -0,0 +1,31 @@ +package config + +import "strings" + +type TypeHTTPPath struct { + value string +} + +func (c *TypeHTTPPath) UnmarshalText(data []byte) error { // nolint: unparam + if len(data) > 0 { + c.value = "/" + strings.Trim(string(data), "/") + } + + return nil +} + +func (c TypeHTTPPath) MarshalText() ([]byte, error) { // nolint: unparam + return []byte(c.String()), nil +} + +func (c TypeHTTPPath) String() string { + return c.value +} + +func (c TypeHTTPPath) Value(defaultValue string) string { + if c.value == "" { + return defaultValue + } + + return c.value +} diff --git a/config/type_ip.go b/config/type_ip.go new file mode 100644 index 0000000..e249a94 --- /dev/null +++ b/config/type_ip.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + "net" +) + +type TypeIP struct { + value net.IP +} + +func (c *TypeIP) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + ip := net.ParseIP(string(data)) + if ip == nil { + return fmt.Errorf("incorrect ip address: %s", string(data)) + } + + c.value = ip + + return nil +} + +func (c *TypeIP) MarshalText() ([]byte, error) { // nolint: unparam + return []byte(c.String()), nil +} + +func (c TypeIP) String() string { + if c.value == nil { + return "" + } + + return c.value.String() +} + +func (c TypeIP) Value(defaultValue net.IP) net.IP { + if c.value == nil { + return defaultValue + } + + return c.value +} diff --git a/config/type_metric_prefix.go b/config/type_metric_prefix.go new file mode 100644 index 0000000..adc1029 --- /dev/null +++ b/config/type_metric_prefix.go @@ -0,0 +1,42 @@ +package config + +import ( + "fmt" + "regexp" +) + +type TypeMetricPrefix struct { + value string +} + +func (c *TypeMetricPrefix) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + 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 + + return nil +} + +func (c TypeMetricPrefix) MarshalText() ([]byte, error) { // nolint: unparam + return []byte(c.String()), nil +} + +func (c TypeMetricPrefix) String() string { + return c.value +} + +func (c TypeMetricPrefix) Value(defaultValue string) string { + if c.value == "" { + return defaultValue + } + + return c.value +} diff --git a/config/type_port.go b/config/type_port.go new file mode 100644 index 0000000..41963e5 --- /dev/null +++ b/config/type_port.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + "strconv" +) + +type TypePort struct { + value uint +} + +func (c *TypePort) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + + intValue, err := strconv.ParseUint(string(data), 10, 16) + if err != nil { + return fmt.Errorf("port number is not a number: %w", err) + } + + if intValue == 0 || intValue > 65536 { + return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue) + } + + c.value = uint(intValue) + + return nil +} + +func (c *TypePort) MarshalJSON() ([]byte, error) { // nolint: unparam + 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 { + return defaultValue + } + + return c.value +} diff --git a/config/type_prefer_ip.go b/config/type_prefer_ip.go new file mode 100644 index 0000000..fc5e034 --- /dev/null +++ b/config/type_prefer_ip.go @@ -0,0 +1,43 @@ +package config + +import ( + "fmt" + "strings" +) + +type TypePreferIP struct { + value string +} + +func (c *TypePreferIP) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + text := strings.ToLower(string(data)) + + switch text { + case "prefer-ipv4", "prefer-ipv6", "only-ipv4", "only-ipv6": + c.value = text + default: + return fmt.Errorf("incorrect prefer-ip value: %s", string(data)) + } + + return nil +} + +func (c TypePreferIP) MarshalText() ([]byte, error) { // nolint: unparam + return []byte(c.value), nil +} + +func (c *TypePreferIP) String() string { + return c.value +} + +func (c *TypePreferIP) Value(defaultValue string) string { + if c.value == "" { + return defaultValue + } + + return c.value +} diff --git a/config/type_url.go b/config/type_url.go new file mode 100644 index 0000000..38378e2 --- /dev/null +++ b/config/type_url.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + "net/url" +) + +type TypeURL struct { + value *url.URL +} + +func (c *TypeURL) UnmarshalText(data []byte) error { + if len(data) == 0 { + return nil + } + + value, err := url.Parse(string(data)) + if err != nil { + return fmt.Errorf("incorrect URL: %w", err) + } + + c.value = value + + return nil +} + +func (c *TypeURL) MarshalText() ([]byte, error) { // nolint: unparam + 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/utils.go b/utils.go index 1411635..84b008d 100644 --- a/utils.go +++ b/utils.go @@ -8,10 +8,11 @@ import ( "net/http" "net/url" + "github.com/9seconds/mtg/v2/config" "github.com/9seconds/mtg/v2/mtglib/network" ) -func makeNetwork(conf *config) (network.Network, error) { +func makeNetwork(conf *config.Config) (network.Network, error) { tcpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultTimeout) idleTimeout := conf.Network.Timeout.Idle.Value(network.DefaultIdleTimeout) dohIP := conf.Network.DOHIP.Value(net.ParseIP(network.DefaultDOHHostname)).String()