diff --git a/cli/access.go b/cli/access.go index 6dc98b6..9ae7af5 100644 --- a/cli/access.go +++ b/cli/access.go @@ -26,6 +26,7 @@ type accessResponse struct { type accessResponseURLs struct { IP net.IP `json:"ip"` + Port uint `json:"port"` TgURL string `json:"tg_url"` TgQrCode string `json:"tg_qrcode"` TmeURL string `json:"tme_url"` @@ -33,10 +34,11 @@ type accessResponseURLs struct { } type Access struct { - base + base `kong:"-"` - ConfigPath string `arg required type:"existingfile" help:"Path to the configuration file." name:"config-path"` // nolint: lll, govet - Hex bool `help:"Print secret in hex encoding."` + ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll + Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint'"` + Hex bool `kong:"help='Print secret in hex encoding.'"` } func (c *Access) Run(cli *CLI, version string) error { @@ -44,9 +46,13 @@ func (c *Access) Run(cli *CLI, version string) error { 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.conf.Secret.Base64() - resp.Secret.Hex = c.conf.Secret.Hex() + resp.Secret.Base64 = c.Config.Secret.Base64() + resp.Secret.Hex = c.Config.Secret.Hex() wg := &sync.WaitGroup{} wg.Add(2) // nolint: gomnd @@ -54,7 +60,7 @@ func (c *Access) Run(cli *CLI, version string) error { go func() { defer wg.Done() - ip := c.conf.Network.PublicIP.IPv4.Value(nil) + ip := c.Config.Network.PublicIP.IPv4.Value(nil) if ip == nil { ip = c.getIP("tcp4") } @@ -69,7 +75,7 @@ func (c *Access) Run(cli *CLI, version string) error { go func() { defer wg.Done() - ip := c.conf.Network.PublicIP.IPv4.Value(nil) + ip := c.Config.Network.PublicIP.IPv6.Value(nil) if ip == nil { ip = c.getIP("tcp6") } @@ -95,8 +101,8 @@ func (c *Access) Run(cli *CLI, version string) error { } 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) + client := c.Network.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) { + return c.Network.DialContext(ctx, protocol, address) }) req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx @@ -133,20 +139,26 @@ func (c *Access) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs { return nil } + portNo := cli.Access.Port + if portNo == 0 { + portNo = c.Config.BindTo.PortValue(0) + } + values := url.Values{} values.Set("server", ip.String()) - values.Set("port", strconv.Itoa(int(c.conf.BindTo.PortValue(0)))) + values.Set("port", strconv.Itoa(int(portNo))) if cli.Access.Hex { - values.Set("secret", c.conf.Secret.Hex()) + values.Set("secret", c.Config.Secret.Hex()) } else { - values.Set("secret", c.conf.Secret.Base64()) + values.Set("secret", c.Config.Secret.Base64()) } urlQuery := values.Encode() rv := &accessResponseURLs{ - IP: ip, + IP: ip, + Port: portNo, TgURL: (&url.URL{ Scheme: "tg", Host: "proxy", diff --git a/cli/access_test.go b/cli/access_test.go new file mode 100644 index 0000000..1a06351 --- /dev/null +++ b/cli/access_test.go @@ -0,0 +1,205 @@ +package cli_test + +import ( + "net/http" + "testing" + + "github.com/9seconds/mtg/v2/config" + "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.NoError( + suite.cli.Access.Config.Network.PublicIP.IPv4.UnmarshalText( + []byte("10.0.0.10"))) + suite.NoError( + suite.cli.Access.Config.Network.PublicIP.IPv6.UnmarshalText( + []byte("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))) + + output := suite.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.NoError( + suite.cli.Access.Config.Network.PublicIP.IPv6.UnmarshalText( + []byte("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))) + + httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co", + httpmock.NewStringResponder(http.StatusOK, "10.11.12.13")) + + output := suite.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.NoError( + suite.cli.Access.Config.Network.PublicIP.IPv6.UnmarshalText( + []byte("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))) + + httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co", + httpmock.NewStringResponder(http.StatusForbidden, "")) + + output := suite.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) { + t.Parallel() + suite.Run(t, &AccessTestSuite{}) +} diff --git a/cli/base.go b/cli/base.go index 4cf9d6d..40348c9 100644 --- a/cli/base.go +++ b/cli/base.go @@ -11,8 +11,8 @@ import ( ) type base struct { - network network.Network - conf *config.Config + Network network.Network + Config *config.Config } func (b *base) ReadConfig(path, version string) error { @@ -31,8 +31,8 @@ func (b *base) ReadConfig(path, version string) error { return fmt.Errorf("cannot build a network: %w", err) } - b.conf = conf - b.network = ntw + b.Config = conf + b.Network = ntw return nil } diff --git a/cli/cli.go b/cli/cli.go index 9892043..19ff668 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -3,7 +3,7 @@ package cli import "github.com/alecthomas/kong" type CLI struct { - GenerateSecret GenerateSecret `cmd help:"Generate new proxy secret"` // nolint: govet - Access Access `cmd help:"Print access information."` // nolint: govet - Version kong.VersionFlag `help:"Print version."` + GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"` // nolint: govet + Access Access `kong:"cmd,help='Print access information.'"` // nolint: govet + Version kong.VersionFlag `kong:"help='Print version.'"` } diff --git a/cli/generate_secret.go b/cli/generate_secret.go index f0f8f69..8a57251 100644 --- a/cli/generate_secret.go +++ b/cli/generate_secret.go @@ -7,10 +7,10 @@ import ( ) type GenerateSecret struct { - base + base `kong:"-"` - HostName string `arg optional help:"Hostname to use for domain fronting. Default is '${domain_front}'." name:"hostname" default:"${domain_front}"` // nolint: lll, govet - Hex bool `help:"Print secret in hex encoding."` + HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"` // nolint: lll, govet + Hex bool `kong:"help='Print secret in hex encoding.'"` } func (c *GenerateSecret) Run(cli *CLI, _ string) error { diff --git a/go.mod b/go.mod index 6486b6b..c59047b 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,15 @@ require ( github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 - github.com/jarcoal/httpmock v1.0.8 // indirect + github.com/jarcoal/httpmock v1.0.8 github.com/kr/pretty v0.1.0 // indirect github.com/libp2p/go-reuseport v0.0.2 github.com/mccutchen/go-httpbin v1.1.1 github.com/pelletier/go-toml v1.8.1 github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/testify v1.7.0 + github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 - golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 17aca99..dafe96e 100644 --- a/go.sum +++ b/go.sum @@ -36,12 +36,18 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index fc51371..e74a760 100644 --- a/main.go +++ b/main.go @@ -15,8 +15,7 @@ func main() { cli := &cli.CLI{} ctx := kong.Parse(cli, kong.Vars{ - "domain_front": "amazonaws.com", - "version": version, + "version": version, }) ctx.FatalIfErrorf(ctx.Run(cli, version))