func configFromCert(crt *tls.Certificate, certPool *x509.CertPool) *tls.Config {
	tlsConf := cert.TLSConfFromCert(*crt)
	tlsConf.CipherSuites = []uint16{
		tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	}
	tlsConf.PreferServerCipherSuites = true
	tlsConf.RootCAs = certPool
	tlsConf.InsecureSkipVerify = true
	haveNPN := false
	for _, p := range tlsConf.NextProtos {
		if p == "h2" {
			haveNPN = true
			break
		}
	}
	if !haveNPN {
		tlsConf.NextProtos = append(tlsConf.NextProtos, "h2")
	}
	tlsConf.NextProtos = append(tlsConf.NextProtos, "h2-14")
	tlsConf.NextProtos = append(tlsConf.NextProtos, "http/1.1")
	return tlsConf
}
configFromCert creates a new TLS configuration from a certificate and a cert
pool. These configs shouldn't be shared among different server instances to
avoid data races.
package proxy_test
import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"path"
	"strings"
	"testing"
	"time"
	"github.com/lightninglabs/aperture/auth"
	"github.com/lightninglabs/aperture/proxy"
	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"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"gopkg.in/macaroon.v2"
)
const (
	testProxyAddr            = "localhost:10019"
	testHostRegexp           = "^localhost:.*$"
	testPathRegexpHTTP       = "^/http/.*$"
	testPathRegexpGRPC       = "^/proxy_test\\.Greeter/.*$"
	testTargetServiceAddress = "localhost:8082"
	testHTTPResponseBody     = "HTTP Hello"
)
var (
	errBackend = fmt.Errorf("this is the error you wanted")
)
type testCase struct {
	name           string
	auth           auth.Level
	authWhitelist  []string
	wantBackendErr bool
}
type helloServer struct{}
func (s *helloServer) SayHello(_ context.Context,
	req *proxytest.HelloRequest) (*proxytest.HelloReply, error) {
	if req.ReturnError {
		return nil, errBackend
	}
	return &proxytest.HelloReply{
		Message: fmt.Sprintf("Hello %s", req.Name),
	}, nil
}
func (s *helloServer) SayHelloNoAuth(_ context.Context,
	req *proxytest.HelloRequest) (*proxytest.HelloReply, error) {
	if req.ReturnError {
		return nil, errBackend
	}
	return &proxytest.HelloReply{
		Message: fmt.Sprintf("Hello %s", req.Name),
	}, nil
}
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)
		})
	}
}
func runHTTPTest(t *testing.T, tc *testCase) {
	services := []*proxy.Service{{
		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")
	require.NoError(t, err)
	server := &http.Server{
		Addr:    testProxyAddr,
		Handler: http.HandlerFunc(p.ServeHTTP),
	}
	go func() {
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			t.Errorf("Error serving on %s: %v", testProxyAddr, err)
		}
	}()
	defer closeOrFail(t, server)
	backendService := &http.Server{Addr: testTargetServiceAddress}
	go func() { _ = startBackendHTTP(backendService) }()
	defer closeOrFail(t, backendService)
	time.Sleep(100 * time.Millisecond)
	client := &http.Client{}
	url := fmt.Sprintf("http://%s/http/test", testProxyAddr)
	resp, err := client.Get(url)
	require.NoError(t, err)
	require.Equal(t, "402 Payment Required", resp.Status)
	authHeader := resp.Header.Get("Www-Authenticate")
	require.Contains(t, authHeader, "LSAT")
	_ = resp.Body.Close()
	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)
		defer closeOrFail(t, resp.Body)
		bodyBytes, err := ioutil.ReadAll(resp.Body)
		require.NoError(t, err)
		require.Equal(t, testHTTPResponseBody, string(bodyBytes))
	}
	req, err := http.NewRequest("GET", url, nil)
	require.NoError(t, err)
	req.Header.Add("Authorization", "foobar")
	resp, err = client.Do(req)
	require.NoError(t, err)
	require.Equal(t, "200 OK", resp.Status)
	defer closeOrFail(t, resp.Body)
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, testHTTPResponseBody, string(bodyBytes))
}
func TestProxyGRPC(t *testing.T) {
	testCases := []*testCase{{
		name: "no whitelist",
		auth: "on",
	}, {
		name:           "no whitelist expect err",
		auth:           "on",
		wantBackendErr: true,
	}, {
		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)
		})
	}
	for i := 0; i < 20; i++ {
		name := fmt.Sprintf("stream closed w/o trailers repro %d", i)
		t.Run(name, func(t *testing.T) {
			runGRPCTest(t, &testCase{
				name:           name,
				auth:           "on",
				wantBackendErr: true,
			})
		})
	}
}
func runGRPCTest(t *testing.T, tc *testCase) {
	tempDirName, err := ioutil.TempDir("", "proxytest")
	require.NoError(t, err)
	certFile := path.Join(tempDirName, "proxy.cert")
	keyFile := path.Join(tempDirName, "proxy.key")
	certPool, creds, certData, err := genCertPair(certFile, keyFile)
	require.NoError(t, err)
	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(creds),
	}
	httpListener, err := net.Listen("tcp", testProxyAddr)
	if err != nil {
		t.Errorf("Error listening on %s: %v", testProxyAddr, err)
	}
	tlsListener := tls.NewListener(
		httpListener, configFromCert(&certData, certPool),
	)
	defer closeOrFail(t, tlsListener)
	services := []*proxy.Service{{
		Address:            testTargetServiceAddress,
		HostRegexp:         testHostRegexp,
		PathRegexp:         testPathRegexpGRPC,
		Protocol:           "https",
		TLSCertPath:        certFile,
		Auth:               tc.auth,
		AuthWhitelistPaths: tc.authWhitelist,
	}}
	mockAuth := auth.NewMockAuthenticator()
	p, err := proxy.New(mockAuth, services, true, "static")
	require.NoError(t, err)
	server := &http.Server{
		Addr:      testProxyAddr,
		Handler:   http.HandlerFunc(p.ServeHTTP),
		TLSConfig: configFromCert(&certData, certPool),
	}
	go func() {
		err := server.Serve(tlsListener)
		if !isClosedErr(err) {
			t.Errorf("Error serving on %s: %v", testProxyAddr, err)
		}
	}()
	serverOpts := []grpc.ServerOption{
		grpc.Creds(credentials.NewTLS(configFromCert(
			&certData, certPool,
		))),
	}
	backendService := grpc.NewServer(serverOpts...)
	go func() { _ = startBackendGRPC(backendService) }()
	defer backendService.Stop()
	conn, err := grpc.Dial(testProxyAddr, opts...)
	require.NoError(t, err)
	client := proxytest.NewGreeterClient(conn)
	req := &proxytest.HelloRequest{Name: "foo"}
	captureMetadata := metadata.MD{}
	_, err = client.SayHello(
		context.Background(), req, grpc.WaitForReady(true),
		grpc.Trailer(&captureMetadata),
	)
	require.Error(t, err)
	statusErr, ok := status.FromError(err)
	require.True(t, ok)
	require.Equal(t, "payment required", statusErr.Message())
	require.Equal(t, codes.Internal, statusErr.Code())
	expectedHeaderContent, _ := mockAuth.FreshChallengeHeader(&http.Request{
		Header: map[string][]string{},
	}, "", 0)
	capturedHeader := captureMetadata.Get("WWW-Authenticate")
	require.Len(t, capturedHeader, 1)
	require.Equal(
		t, expectedHeaderContent.Get("WWW-Authenticate"),
		capturedHeader[0],
	)
	if len(tc.authWhitelist) > 0 {
		conn, err = grpc.Dial(testProxyAddr, opts...)
		require.NoError(t, err)
		client = proxytest.NewGreeterClient(conn)
		req = &proxytest.HelloRequest{Name: "foo"}
		res, err := client.SayHelloNoAuth(context.Background(), req)
		require.NoError(t, err)
		require.Equal(t, "Hello foo", res.Message)
	}
	dummyMac, err := macaroon.New(
		[]byte("key"), []byte("id"), "loc", macaroon.LatestVersion,
	)
	require.NoError(t, err)
	opts = []grpc.DialOption{
		grpc.WithTransportCredentials(creds),
		grpc.WithPerRPCCredentials(macaroons.NewMacaroonCredential(
			dummyMac,
		)),
	}
	conn, err = grpc.Dial(testProxyAddr, opts...)
	require.NoError(t, err)
	client = proxytest.NewGreeterClient(conn)
	req = &proxytest.HelloRequest{
		Name: "foo", ReturnError: tc.wantBackendErr,
	}
	res, err := client.SayHello(context.Background(), req)
	if tc.wantBackendErr {
		require.Error(t, err)
		statusErr, ok := status.FromError(err)
		require.True(t, ok)
		require.Equal(t, errBackend.Error(), statusErr.Message())
		require.Equal(t, codes.Unknown, statusErr.Code())
	} else {
		require.NoError(t, err)
		require.Equal(t, "Hello foo", res.Message)
	}
}
func startBackendHTTP(server *http.Server) error {
	sayHello := func(w http.ResponseWriter, r *http.Request) {
		_, err := w.Write([]byte(testHTTPResponseBody))
		if err != nil {
			panic(err)
		}
	}
	server.Handler = http.HandlerFunc(sayHello)
	return server.ListenAndServe()
}
func startBackendGRPC(grpcServer *grpc.Server) error {
	server := helloServer{}
	proxytest.RegisterGreeterServer(grpcServer, &server)
	grpcListener, err := net.Listen("tcp", testTargetServiceAddress)
	if err != nil {
		return fmt.Errorf("RPC server unable to listen on %s",
			testTargetServiceAddress)
	}
	return grpcServer.Serve(grpcListener)
}
func genCertPair(certFile, keyFile string) (*x509.CertPool,
	credentials.TransportCredentials, tls.Certificate, error) {
	crt := tls.Certificate{}
	err := cert.GenCertPair(
		"aperture autogenerated cert", certFile, keyFile, nil, nil,
		false, cert.DefaultAutogenValidity,
	)
	if err != nil {
		return nil, nil, crt, fmt.Errorf("unable to generate cert "+
			"pair: %v", err)
	}
	crt, x509Cert, err := cert.LoadCert(certFile, keyFile)
	if err != nil {
		return nil, nil, crt, fmt.Errorf("unable to load cert: %v", err)
	}
	cp := x509.NewCertPool()
	cp.AddCert(x509Cert)
	creds, err := credentials.NewClientTLSFromFile(certFile, "")
	if err != nil {
		return nil, nil, crt, fmt.Errorf("unable to load cert file: "+
			"%v", err)
	}
	return cp, creds, crt, nil
}