diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 5b7f54b..e93b21c 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -10,7 +10,6 @@ import ( "net" "net/http" "path" - "strings" "testing" "time" @@ -19,6 +18,7 @@ import ( proxytest "github.com/lightninglabs/aperture/proxy/testdata" "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/macaroons" + "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -35,6 +35,12 @@ const ( testHTTPResponseBody = "HTTP Hello" ) +type testCase struct { + name string + auth auth.Level + authWhitelist []string +} + // helloServer is a simple server that implements the GreeterServer interface. type helloServer struct{} @@ -51,7 +57,7 @@ func (s *helloServer) SayHello(_ context.Context, // SayHello returns a simple string that also contains a string from the // request. This RPC method should be whitelisted to be called without any // authentication required. -func (s *helloServer) SayHelloNoAuth(ctx context.Context, +func (s *helloServer) SayHelloNoAuth(_ context.Context, req *proxytest.HelloRequest) (*proxytest.HelloReply, error) { return &proxytest.HelloReply{ @@ -62,19 +68,40 @@ func (s *helloServer) SayHelloNoAuth(ctx context.Context, // TestProxyHTTP tests that the proxy can forward HTTP requests to a backend // service and handle LSAT authentication correctly. func TestProxyHTTP(t *testing.T) { + testCases := []*testCase{{ + name: "no whitelist", + auth: "on", + }, { + name: "with whitelist", + auth: "on", + authWhitelist: []string{"^/http/white.*$"}, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + runHTTPTest(t, tc) + }) + } +} + +// TestProxyHTTP tests that the proxy can forward HTTP requests to a backend +// service and handle LSAT authentication correctly. +func runHTTPTest(t *testing.T, tc *testCase) { // Create a list of services to proxy between. services := []*proxy.Service{{ - Address: testTargetServiceAddress, - HostRegexp: testHostRegexp, - PathRegexp: testPathRegexpHTTP, - Protocol: "http", + Address: testTargetServiceAddress, + HostRegexp: testHostRegexp, + PathRegexp: testPathRegexpHTTP, + Protocol: "http", + Auth: tc.auth, + AuthWhitelistPaths: tc.authWhitelist, }} mockAuth := auth.NewMockAuthenticator() p, err := proxy.New(mockAuth, services, true, "static") - if err != nil { - t.Fatalf("failed to create new proxy: %v", err) - } + require.NoError(t, err) // Start server that gives requests to the proxy. server := &http.Server{ @@ -101,82 +128,103 @@ func TestProxyHTTP(t *testing.T) { client := &http.Client{} url := fmt.Sprintf("http://%s/http/test", testProxyAddr) resp, err := client.Get(url) - if err != nil { - t.Fatalf("errored making http request: %v", err) - } + require.NoError(t, err) - if resp.Status != "402 Payment Required" { - t.Fatalf("expected 402 status code, got: %v", resp.Status) - } + require.Equal(t, "402 Payment Required", resp.Status) authHeader := resp.Header.Get("Www-Authenticate") - if !strings.Contains(authHeader, "LSAT") { - t.Fatalf("expected partial LSAT in response header, got: %v", - authHeader) - } + require.Contains(t, authHeader, "LSAT") _ = resp.Body.Close() + // Make sure that if we query an URL that is on the whitelist, we don't + // get the 402 response. + if len(tc.authWhitelist) > 0 { + url = fmt.Sprintf("http://%s/http/white", testProxyAddr) + req, err := http.NewRequest("GET", url, nil) + require.NoError(t, err) + resp, err = client.Do(req) + require.NoError(t, err) + + require.Equal(t, "200 OK", resp.Status) + + // Ensure that we got the response body we expect. + defer closeOrFail(t, resp.Body) + bodyBytes, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + require.Equal(t, testHTTPResponseBody, string(bodyBytes)) + } + // Make sure that if the Auth header is set, the client's request is // proxied to the backend service. req, err := http.NewRequest("GET", url, nil) - if err != nil { - t.Fatalf("error creating request: %v", err) - } + require.NoError(t, err) req.Header.Add("Authorization", "foobar") resp, err = client.Do(req) - if err != nil { - t.Fatalf("errored making http request: %v", err) - } + require.NoError(t, err) - if resp.Status != "200 OK" { - t.Fatalf("expected 200 OK status code, got: %v", resp.Status) - } + require.Equal(t, "200 OK", resp.Status) // Ensure that we got the response body we expect. defer closeOrFail(t, resp.Body) bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf("failed to read response body: %v", err) - } + require.NoError(t, err) - if string(bodyBytes) != testHTTPResponseBody { - t.Fatalf("expected response body %v, got %v", - testHTTPResponseBody, string(bodyBytes)) - } + require.Equal(t, testHTTPResponseBody, string(bodyBytes)) } // TestProxyHTTP tests that the proxy can forward gRPC requests to a backend // service and handle LSAT authentication correctly. func TestProxyGRPC(t *testing.T) { + testCases := []*testCase{{ + name: "no whitelist", + auth: "on", + }, { + name: "with whitelist", + auth: "on", + authWhitelist: []string{ + "^/proxy_test\\.Greeter/SayHelloNoAuth.*$", + }, + }} + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + runGRPCTest(t, tc) + }) + } +} + +// TestProxyHTTP tests that the proxy can forward gRPC requests to a backend +// service and handle LSAT authentication correctly. +func runGRPCTest(t *testing.T, tc *testCase) { // Since gRPC only really works over TLS, we need to generate a // certificate and key pair first. tempDirName, err := ioutil.TempDir("", "proxytest") - if err != nil { - t.Fatalf("unable to create temp dir: %v", err) - } + require.NoError(t, err) certFile := path.Join(tempDirName, "proxy.cert") keyFile := path.Join(tempDirName, "proxy.key") certPool, creds, certData, err := genCertPair(certFile, keyFile) - if err != nil { - t.Fatalf("unable to create cert pair: %v", err) - } + require.NoError(t, err) + opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} // Create a list of services to proxy between. services := []*proxy.Service{{ - Address: testTargetServiceAddress, - HostRegexp: testHostRegexp, - PathRegexp: testPathRegexpGRPC, - Protocol: "https", - TLSCertPath: certFile, + Address: testTargetServiceAddress, + HostRegexp: testHostRegexp, + PathRegexp: testPathRegexpGRPC, + Protocol: "https", + TLSCertPath: certFile, + Auth: tc.auth, + AuthWhitelistPaths: tc.authWhitelist, }} // Create the proxy server and start serving on TLS. mockAuth := auth.NewMockAuthenticator() p, err := proxy.New(mockAuth, services, true, "static") - if err != nil { - t.Fatalf("failed to create new proxy: %v", err) - } + require.NoError(t, err) server := &http.Server{ Addr: testProxyAddr, Handler: http.HandlerFunc(p.ServeHTTP), @@ -198,11 +246,8 @@ func TestProxyGRPC(t *testing.T) { defer backendService.Stop() // Dial to the proxy now, without any authentication. - opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} conn, err := grpc.Dial(testProxyAddr, opts...) - if err != nil { - t.Fatalf("unable to connect to RPC server: %v", err) - } + require.NoError(t, err) client := proxytest.NewGreeterClient(conn) // Make request without authentication. We expect an error that can @@ -211,29 +256,31 @@ func TestProxyGRPC(t *testing.T) { _, err = client.SayHello( context.Background(), req, grpc.WaitForReady(true), ) - if err == nil { - t.Fatalf("expected error to be returned without auth") - } + require.Error(t, err) statusErr, ok := status.FromError(err) - if !ok { - t.Fatalf("expected error to be status.Status") - } - if statusErr.Code() != codes.Internal { - t.Fatalf("unexpected code. wanted %d, got %d", - codes.Internal, statusErr.Code()) - } - if statusErr.Message() != "payment required" { - t.Fatalf("invalid error. expected [%s] got [%s]", - "payment required", err.Error()) + require.True(t, ok) + require.Equal(t, codes.Internal, statusErr.Code()) + require.Equal(t, "payment required", statusErr.Message()) + + // Make sure that if we query an URL that is on the whitelist, we don't + // get the 402 response. + if len(tc.authWhitelist) > 0 { + conn, err = grpc.Dial(testProxyAddr, opts...) + require.NoError(t, err) + client = proxytest.NewGreeterClient(conn) + + // Make the request. This time no error should be returned. + req = &proxytest.HelloRequest{Name: "foo"} + res, err := client.SayHelloNoAuth(context.Background(), req) + require.NoError(t, err) + require.Equal(t, "Hello foo", res.Message) } // Dial to the proxy again, this time with a dummy macaroon. dummyMac, err := macaroon.New( []byte("key"), []byte("id"), "loc", macaroon.LatestVersion, ) - if err != nil { - t.Fatalf("unable to create dummy macaroon: %v", err) - } + require.NoError(t, err) opts = []grpc.DialOption{ grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(macaroons.NewMacaroonCredential( @@ -241,208 +288,14 @@ func TestProxyGRPC(t *testing.T) { )), } conn, err = grpc.Dial(testProxyAddr, opts...) - if err != nil { - t.Fatalf("unable to connect to RPC server: %v", err) - } + require.NoError(t, err) client = proxytest.NewGreeterClient(conn) // Make the request. This time no error should be returned. req = &proxytest.HelloRequest{Name: "foo"} res, err := client.SayHello(context.Background(), req) - if err != nil { - t.Fatalf("unable to call service: %v", err) - } - if res.Message != "Hello foo" { - t.Fatalf("unexpected reply, wanted %s, got %s", - "Hello foo", res.Message) - } -} - -// TestWhitelistHTTP verifies that a white list entry for a service allows an -// authentication exception to be configured. -func TestWhitelistHTTP(t *testing.T) { - // Create a service with authentication on by default, with one - // exception configured as whitelist. - services := []*proxy.Service{{ - Address: testTargetServiceAddress, - HostRegexp: testHostRegexp, - PathRegexp: testPathRegexpHTTP, - Protocol: "http", - Auth: "on", - AuthWhitelistPaths: []string{"^/http/white.*$"}, - }} - - mockAuth := auth.NewMockAuthenticator() - p, err := proxy.New(mockAuth, services, true, "static") - if err != nil { - t.Fatalf("failed to create new proxy: %v", err) - } - - // Start server that gives requests to the proxy. - server := &http.Server{ - Addr: testProxyAddr, - Handler: http.HandlerFunc(p.ServeHTTP), - } - go func() { _ = server.ListenAndServe() }() - defer closeOrFail(t, server) - - // Start the target backend service. - backendService := &http.Server{Addr: testTargetServiceAddress} - go func() { _ = startBackendHTTP(backendService) }() - defer closeOrFail(t, backendService) - - // Wait for servers to start. - time.Sleep(100 * time.Millisecond) - - // Test making a request to the backend service to an URL where - // authentication is enabled. - client := &http.Client{} - url := fmt.Sprintf("http://%s/http/black", testProxyAddr) - resp, err := client.Get(url) - if err != nil { - t.Fatalf("errored making http request: %v", err) - } - if resp.Status != "402 Payment Required" { - t.Fatalf("expected 402 status code, got: %v", resp.Status) - } - authHeader := resp.Header.Get("Www-Authenticate") - if !strings.Contains(authHeader, "LSAT") { - t.Fatalf("expected partial LSAT in response header, got: %v", - authHeader) - } - _ = resp.Body.Close() - - // Make sure that if we query an URL that is on the whitelist, we don't - // get the 402 response. - url = fmt.Sprintf("http://%s/http/white", testProxyAddr) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - t.Fatalf("error creating request: %v", err) - } - resp, err = client.Do(req) - if err != nil { - t.Fatalf("errored making http request: %v", err) - } - if resp.Status != "200 OK" { - t.Fatalf("expected 200 OK status code, got: %v", resp.Status) - } - - // Ensure that we got the response body we expect. - defer closeOrFail(t, resp.Body) - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf("failed to read response body: %v", err) - } - - if string(bodyBytes) != testHTTPResponseBody { - t.Fatalf("expected response body %v, got %v", - testHTTPResponseBody, string(bodyBytes)) - } -} - -// TestWhitelistGRPC verifies that a white list entry for a service allows an -// authentication exception to be configured. -func TestWhitelistGRPC(t *testing.T) { - // Since gRPC only really works over TLS, we need to generate a - // certificate and key pair first. - tempDirName, err := ioutil.TempDir("", "proxytest") - if err != nil { - t.Fatalf("unable to create temp dir: %v", err) - } - certFile := path.Join(tempDirName, "proxy.cert") - keyFile := path.Join(tempDirName, "proxy.key") - certPool, creds, certData, err := genCertPair(certFile, keyFile) - if err != nil { - t.Fatalf("unable to create cert pair: %v", err) - } - opts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} - - // Create a list of services to proxy between. - services := []*proxy.Service{{ - Address: testTargetServiceAddress, - HostRegexp: testHostRegexp, - PathRegexp: testPathRegexpGRPC, - Protocol: "https", - TLSCertPath: certFile, - Auth: "on", - AuthWhitelistPaths: []string{ - "^/proxy_test\\.Greeter/SayHelloNoAuth.*$", - }, - }} - - // Create the proxy server and start serving on TLS. - mockAuth := auth.NewMockAuthenticator() - p, err := proxy.New(mockAuth, services, true, "static") - if err != nil { - t.Fatalf("failed to create new proxy: %v", err) - } - server := &http.Server{ - Addr: testProxyAddr, - Handler: http.HandlerFunc(p.ServeHTTP), - TLSConfig: &tls.Config{ - RootCAs: certPool, - InsecureSkipVerify: true, - }, - } - go func() { _ = server.ListenAndServeTLS(certFile, keyFile) }() - defer closeOrFail(t, server) - - // Start the target backend service also on TLS. - tlsConf := cert.TLSConfFromCert(certData) - serverOpts := []grpc.ServerOption{ - grpc.Creds(credentials.NewTLS(tlsConf)), - } - backendService := grpc.NewServer(serverOpts...) - go func() { _ = startBackendGRPC(backendService) }() - defer backendService.Stop() - - // Dial to the proxy now, without any authentication. - conn, err := grpc.Dial(testProxyAddr, opts...) - if err != nil { - t.Fatalf("unable to connect to RPC server: %v", err) - } - client := proxytest.NewGreeterClient(conn) - - // Test making a request to the backend service to an URL where - // authentication is enabled. - req := &proxytest.HelloRequest{Name: "foo"} - _, err = client.SayHello( - context.Background(), req, grpc.WaitForReady(true), - ) - if err == nil { - t.Fatalf("expected error to be returned without auth") - } - statusErr, ok := status.FromError(err) - if !ok { - t.Fatalf("expected error to be status.Status") - } - if statusErr.Code() != codes.Internal { - t.Fatalf("unexpected code. wanted %d, got %d", - codes.Internal, statusErr.Code()) - } - if statusErr.Message() != "payment required" { - t.Fatalf("invalid error. expected [%s] got [%s]", - "payment required", err.Error()) - } - - // Make sure that if we query an URL that is on the whitelist, we don't - // get the 402 response. - conn, err = grpc.Dial(testProxyAddr, opts...) - if err != nil { - t.Fatalf("unable to connect to RPC server: %v", err) - } - client = proxytest.NewGreeterClient(conn) - - // Make the request. This time no error should be returned. - req = &proxytest.HelloRequest{Name: "foo"} - res, err := client.SayHelloNoAuth(context.Background(), req) - if err != nil { - t.Fatalf("unable to call service: %v", err) - } - if res.Message != "Hello foo" { - t.Fatalf("unexpected reply, wanted %s, got %s", - "Hello foo", res.Message) - } + require.NoError(t, err) + require.Equal(t, "Hello foo", res.Message) } // startBackendHTTP starts the given HTTP server and blocks until the server