From 039e5160191195b1b5d099642b5fbc4ed2c17534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Wed, 10 Jan 2024 13:32:50 +0100 Subject: [PATCH 1/4] command/grpctest: add gRPC test server Run the Martian h2 test service as a standalone server. --- command/grpctest/grpctest.go | 125 +++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 command/grpctest/grpctest.go diff --git a/command/grpctest/grpctest.go b/command/grpctest/grpctest.go new file mode 100644 index 00000000..be6ce1c3 --- /dev/null +++ b/command/grpctest/grpctest.go @@ -0,0 +1,125 @@ +// Copyright 2023 Sauce Labs Inc., all rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package grpctest + +import ( + "context" + "crypto/tls" + "net" + + "github.com/saucelabs/forwarder" + "github.com/saucelabs/forwarder/bind" + ts "github.com/saucelabs/forwarder/internal/martian/h2/testing" + tspb "github.com/saucelabs/forwarder/internal/martian/h2/testservice" + "github.com/saucelabs/forwarder/log" + "github.com/saucelabs/forwarder/log/stdlog" + "github.com/saucelabs/forwarder/runctx" + "github.com/saucelabs/forwarder/utils/cobrautil" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + _ "google.golang.org/grpc/encoding/gzip" // register gzip encoding +) + +type command struct { + addr string + tlsServerConfig *forwarder.TLSServerConfig + logConfig *log.Config +} + +func (c *command) runE(cmd *cobra.Command, _ []string) (cmdErr error) { + if f := c.logConfig.File; f != nil { + defer f.Close() + } + logger := stdlog.New(c.logConfig) + + defer func() { + if cmdErr != nil { + logger.Errorf("fatal error exiting: %s", cmdErr) + cmd.SilenceErrors = true + } + }() + + { + var ( + cfg []byte + err error + ) + + d := cobrautil.FlagsDescriber{ + Format: cobrautil.Plain, + } + cfg, err = d.DescribeFlags(cmd.Flags()) + if err != nil { + return err + } + logger.Infof("configuration\n%s", cfg) + + d.ShowNotChanged = true + cfg, err = d.DescribeFlags(cmd.Flags()) + if err != nil { + return err + } + logger.Debugf("all configuration\n%s\n\n", cfg) + } + + g := runctx.NewGroup() + + { + tlsCfg := new(tls.Config) + if err := c.tlsServerConfig.ConfigureTLSConfig(tlsCfg); err != nil { + return err + } + gs := grpc.NewServer( + grpc.Creds(credentials.NewServerTLSFromCert(&tlsCfg.Certificates[0])), + ) + tspb.RegisterTestServiceServer(gs, &ts.Server{}) + defer gs.Stop() + + l, err := net.Listen("tcp", c.addr) + if err != nil { + return err + } + defer l.Close() + + g.Add(func(ctx context.Context) error { + logger.Named("grpc").Infof("server listen address=%s", l.Addr()) + go func() { + <-ctx.Done() + gs.GracefulStop() + }() + return gs.Serve(l) + }) + } + + return g.Run() +} + +func Command() *cobra.Command { + c := command{ + addr: "localhost:1443", + tlsServerConfig: new(forwarder.TLSServerConfig), + logConfig: log.DefaultConfig(), + } + + cmd := &cobra.Command{ + Use: "grpctest [--address ] [flags]", + Short: "Start HTTP/2 gRPC server for testing", + RunE: c.runE, + Hidden: true, + } + + fs := cmd.Flags() + fs.StringVar(&c.addr, "address", c.addr, ""+ + "Address to listen on. "+ + "If the host is empty, the server will listen on all available interfaces. ") + bind.TLSServerConfig(fs, c.tlsServerConfig, "") + bind.LogConfig(fs, c.logConfig) + bind.AutoMarkFlagFilename(cmd) + + return cmd +} From 59b4b23b2caba61d8596e55fc7615a12d30e9a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Wed, 10 Jan 2024 13:33:08 +0100 Subject: [PATCH 2/4] command/forwarder: add gRPC test server to root command --- command/forwarder/root.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/command/forwarder/root.go b/command/forwarder/root.go index 46b11829..fc39224e 100644 --- a/command/forwarder/root.go +++ b/command/forwarder/root.go @@ -8,6 +8,7 @@ package forwarder import ( "github.com/saucelabs/forwarder/bind" + "github.com/saucelabs/forwarder/command/grpctest" "github.com/saucelabs/forwarder/command/httpbin" "github.com/saucelabs/forwarder/command/pac" "github.com/saucelabs/forwarder/command/ready" @@ -107,7 +108,8 @@ func Command() *cobra.Command { // Add other commands. cmd.AddCommand( - httpbin.Command(), // hidden + grpctest.Command(), // hidden + httpbin.Command(), // hidden version.Command(), ) From 9384942b86e1d044afb5eb23cd061dfecd3646ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Wed, 10 Jan 2024 15:06:27 +0100 Subject: [PATCH 3/4] command: move httpbin and grpctest under test command Add test command and rename grpctest to "test grpc", httpbin is now "test httpbin". The commands are not hidden anymore and the help shows: Other Commands: completion Generate the autocompletion script for the specified shell test Run test servers for various protocols version Print version information --- command/forwarder/root.go | 21 ++++++++++++------- .../grpctest.go => test/grpc/grpc.go} | 9 ++++---- command/{ => test}/httpbin/httpbin.go | 7 +++---- e2e/forwarder/service.go | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) rename command/{grpctest/grpctest.go => test/grpc/grpc.go} (94%) rename command/{ => test}/httpbin/httpbin.go (93%) diff --git a/command/forwarder/root.go b/command/forwarder/root.go index fc39224e..fc1c1265 100644 --- a/command/forwarder/root.go +++ b/command/forwarder/root.go @@ -8,11 +8,11 @@ package forwarder import ( "github.com/saucelabs/forwarder/bind" - "github.com/saucelabs/forwarder/command/grpctest" - "github.com/saucelabs/forwarder/command/httpbin" "github.com/saucelabs/forwarder/command/pac" "github.com/saucelabs/forwarder/command/ready" "github.com/saucelabs/forwarder/command/run" + "github.com/saucelabs/forwarder/command/test/grpc" + "github.com/saucelabs/forwarder/command/test/httpbin" "github.com/saucelabs/forwarder/command/version" "github.com/saucelabs/forwarder/utils/cobrautil" "github.com/saucelabs/forwarder/utils/cobrautil/templates" @@ -106,12 +106,19 @@ func Command() *cobra.Command { templates.ActsAsRootCommand(cmd, nil, cg, FlagGroups(), EnvPrefix) - // Add other commands. - cmd.AddCommand( - grpctest.Command(), // hidden - httpbin.Command(), // hidden - version.Command(), + // Add test commands. + test := &cobra.Command{ + Use: "test", + Short: "Run test servers for various protocols", + } + test.AddCommand( + grpc.Command(), + httpbin.Command(), ) + cmd.AddCommand(test) + + // Add version command. + cmd.AddCommand(version.Command()) // Add config-file command to all commands. cobrautil.AddConfigFileForEachCommand(cmd, FlagGroups(), ConfigFileFlagName) diff --git a/command/grpctest/grpctest.go b/command/test/grpc/grpc.go similarity index 94% rename from command/grpctest/grpctest.go rename to command/test/grpc/grpc.go index be6ce1c3..be08fb0a 100644 --- a/command/grpctest/grpctest.go +++ b/command/test/grpc/grpc.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -package grpctest +package grpc import ( "context" @@ -107,10 +107,9 @@ func Command() *cobra.Command { } cmd := &cobra.Command{ - Use: "grpctest [--address ] [flags]", - Short: "Start HTTP/2 gRPC server for testing", - RunE: c.runE, - Hidden: true, + Use: "grpc [--address ] [flags]", + Short: "Start HTTP/2 gRPC server for testing", + RunE: c.runE, } fs := cmd.Flags() diff --git a/command/httpbin/httpbin.go b/command/test/httpbin/httpbin.go similarity index 93% rename from command/httpbin/httpbin.go rename to command/test/httpbin/httpbin.go index 10bcccb0..967e6748 100644 --- a/command/httpbin/httpbin.go +++ b/command/test/httpbin/httpbin.go @@ -87,10 +87,9 @@ func Command() *cobra.Command { c.apiServerConfig.Addr = "localhost:10000" cmd := &cobra.Command{ - Use: "httpbin [--protocol ] [--address ] [flags]", - Short: "Start HTTP(S) server that serves httpbin.org API", - RunE: c.runE, - Hidden: true, + Use: "httpbin [--protocol ] [--address ] [flags]", + Short: "Start HTTP(S) server that serves httpbin.org API", + RunE: c.runE, } fs := cmd.Flags() diff --git a/e2e/forwarder/service.go b/e2e/forwarder/service.go index 4b199044..e5376b62 100644 --- a/e2e/forwarder/service.go +++ b/e2e/forwarder/service.go @@ -59,7 +59,7 @@ func HttpbinService() *Service { return &Service{ Name: HttpbinServiceName, Image: Image, - Command: "httpbin", + Command: "test httpbin", Environment: map[string]string{ "FORWARDER_API_ADDRESS": ":10000", }, From 776cbff925d230d53374f0821111f703dd29386f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Wed, 10 Jan 2024 13:44:15 +0100 Subject: [PATCH 4/4] e2e: add gRPC tests The tests are ported from Martian h2 tests. The client is called fixture for compatibility reasons. --- e2e/certs/gen.sh | 1 + e2e/forwarder/service.go | 17 +++++ e2e/setups.go | 14 ++++ e2e/tests/grpc_test.go | 146 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 e2e/tests/grpc_test.go diff --git a/e2e/certs/gen.sh b/e2e/certs/gen.sh index dc47eae8..a7c61a64 100755 --- a/e2e/certs/gen.sh +++ b/e2e/certs/gen.sh @@ -56,5 +56,6 @@ EOF generate_certificate "proxy" generate_certificate "upstream-proxy" generate_certificate "httpbin" +generate_certificate "grpctest" chmod 644 *.key *.crt diff --git a/e2e/forwarder/service.go b/e2e/forwarder/service.go index e5376b62..122f872f 100644 --- a/e2e/forwarder/service.go +++ b/e2e/forwarder/service.go @@ -21,6 +21,7 @@ const ( ProxyServiceName = "proxy" UpstreamProxyServiceName = "upstream-proxy" HttpbinServiceName = "httpbin" + GRPCTestServiceName = "grpctest" ) const enabled = "true" @@ -70,6 +71,22 @@ func HttpbinService() *Service { } } +func GRPCTestService() *Service { + s := &Service{ + Name: GRPCTestServiceName, + Image: Image, + Command: "test grpc", + Environment: map[string]string{ + "FORWARDER_ADDRESS": ":1443", + }, + Ports: []string{ + "1443:1443", + "10003:10000", + }, + } + return s.WithProtocol("h2") +} + func (s *Service) WithProtocol(protocol string) *Service { s.Environment["FORWARDER_PROTOCOL"] = protocol diff --git a/e2e/setups.go b/e2e/setups.go index 99d1c95f..0eed6fd2 100644 --- a/e2e/setups.go +++ b/e2e/setups.go @@ -33,6 +33,7 @@ func AllSetups() []setup.Setup { SetupDefaults(l) SetupAuth(l) SetupPac(l) + SetupGRPC(l) SetupFlagProxyLocalhost(l) SetupFlagHeader(l) SetupFlagResponseHeader(l) @@ -164,6 +165,19 @@ func SetupPac(l *setupList) { ) } +func SetupGRPC(l *setupList) { + l.Add( + setup.Setup{ + Name: "grpc", + Compose: compose.NewBuilder(). + AddService(forwarder.ProxyService()). + AddService(forwarder.GRPCTestService()). + MustBuild(), + Run: "^TestGRPC", + }, + ) +} + func SetupFlagProxyLocalhost(l *setupList) { for _, mode := range []string{"deny", "allow"} { l.Add(setup.Setup{ diff --git a/e2e/tests/grpc_test.go b/e2e/tests/grpc_test.go new file mode 100644 index 00000000..fd922b23 --- /dev/null +++ b/e2e/tests/grpc_test.go @@ -0,0 +1,146 @@ +// Copyright 2023 Sauce Labs Inc., all rights reserved. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//go:build e2e + +package tests + +import ( + "context" + "crypto/rand" + "crypto/tls" + "encoding/base64" + "errors" + "io" + "sync" + "testing" + + "github.com/saucelabs/forwarder/e2e/forwarder" + tspb "github.com/saucelabs/forwarder/internal/martian/h2/testservice" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/encoding/gzip" +) + +func TestGRPC(t *testing.T) { + tlsCfg := &tls.Config{ + InsecureSkipVerify: true, + } + + t.Setenv("HTTPS_PROXY", proxy) // set proxy for grpc.Dial + conn, err := grpc.Dial(forwarder.GRPCTestServiceName+":1443", grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + fixture := tspb.NewTestServiceClient(conn) + + t.Run("Echo", func(t *testing.T) { + ctx := context.Background() + req := &tspb.EchoRequest{ + Payload: "Hello", + } + resp, err := fixture.Echo(ctx, req) + if err != nil { + t.Fatalf("fixture.Echo(...) = _, %v, want _, nil", err) + } + if got, want := resp.GetPayload(), req.GetPayload(); got != want { + t.Errorf("resp.GetPayload() = %s, want = %s", got, want) + } + }) + + t.Run("LargeEcho", func(t *testing.T) { + // Sends a >128KB payload through the proxy. Since the standard gRPC frame size is only 16KB, + // this exercises frame merging, splitting and flow control code. + payload := make([]byte, 128*1024) + rand.Read(payload) + req := &tspb.EchoRequest{ + Payload: base64.StdEncoding.EncodeToString(payload), + } + + // This test also covers using gzip compression. Ideally, we would test more compression types + // but the golang gRPC implementation only provides a gzip compressor. + tests := []struct { + name string + useCompression bool + }{ + {"RawData", false}, + {"Gzip", true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + var resp *tspb.EchoResponse + if tc.useCompression { + resp, err = fixture.Echo(ctx, req, grpc.UseCompressor(gzip.Name)) + } else { + resp, err = fixture.Echo(ctx, req) + } + if err != nil { + t.Fatalf("fixture.Echo(...) = _, %v, want _, nil", err) + } + if got, want := resp.GetPayload(), req.GetPayload(); got != want { + t.Errorf("resp.GetPayload() = %s, want = %s", got, want) + } + }) + } + }) + + t.Run("Stream", func(t *testing.T) { + ctx := context.Background() + stream, err := fixture.DoubleEcho(ctx) + if err != nil { + t.Fatalf("fixture.DoubleEcho(ctx) = _, %v, want _, nil", err) + } + + var received []*tspb.EchoResponse + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + resp, err := stream.Recv() + if errors.Is(err, io.EOF) { + return + } + if err != nil { + t.Errorf("stream.Recv() = %v, want nil", err) + return + } + received = append(received, resp) + } + }() + + var sent []*tspb.EchoRequest + for i := 0; i < 5; i++ { + payload := make([]byte, 20*1024) + rand.Read(payload) + req := &tspb.EchoRequest{ + Payload: base64.StdEncoding.EncodeToString(payload), + } + if err := stream.Send(req); err != nil { + t.Fatalf("stream.Send(req) = %v, want nil", err) + } + sent = append(sent, req) + } + if err := stream.CloseSend(); err != nil { + t.Fatalf("stream.CloseSend() = %v, want nil", err) + } + wg.Wait() + + for i, req := range sent { + want := req.GetPayload() + if got := received[2*i].GetPayload(); got != want { + t.Errorf("received[2*i].GetPayload() = %s, want %s", got, want) + } + if got := received[2*i+1].GetPayload(); got != want { + t.Errorf("received[2*i+1].GetPayload() = %s, want %s", got, want) + } + } + }) +}