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.