Ctrl K
ComunidadGuías…

tests/gateway_tls_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/shellhub-io/shellhub/pkg/uuid"
	"github.com/shellhub-io/shellhub/tests/environment"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const acmeDomain = "shellhub.example.com"

func upGatewayStack(t *testing.T, ctx context.Context, cfg environment.Config) (*environment.Stack, string) {
	t.Helper()

	cfg.Edition = environment.EditionCommunity
	cfg.Name = uuid.Generate()
	cfg.Run = run

	stack, err := environment.Up(ctx, cfg)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, stack.Down(context.WithoutCancel(ctx))) })

	return stack, cfg.Name
}

func requireAttachedDownRemoves(t *testing.T, ctx context.Context, stack *environment.Stack, name, dir string) {
	t.Helper()

	require.DirExists(t, dir)

	attached, err := environment.Attach(ctx, name, stack.Files(), stack.Envs())
	require.NoError(t, err)
	require.NoError(t, attached.Down(ctx))

	assert.NoDirExists(t, dir)
}

func servedCertificate(ctx context.Context, address string, config *tls.Config) (*x509.Certificate, error) {
	dialer := &tls.Dialer{NetDialer: &net.Dialer{Timeout: 10 * time.Second}, Config: config}

	conn, err := dialer.DialContext(ctx, "tcp", address)
	if err != nil {
		return nil, err
	}

	defer func() { _ = conn.Close() }()

	tlsConn, ok := conn.(*tls.Conn)
	if !ok {
		return nil, fmt.Errorf("dialing %s returned a %T, not a TLS connection", address, conn)
	}

	return tlsConn.ConnectionState().PeerCertificates[0], nil
}

// TestGatewaySuppliedCertificate runs the gateway with automatic HTTPS on and a certificate it is
// given, which is how TLS runs on a name no public CA signs.
func TestGatewaySuppliedCertificate(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	certificate, err := environment.SelfSignedCertificate("localhost")
	require.NoError(t, err)

	block, _ := pem.Decode(certificate.CertificatePEM)
	require.NotNil(t, block)

	supplied, err := x509.ParseCertificate(block.Bytes)
	require.NoError(t, err)

	roots := x509.NewCertPool()
	roots.AddCert(supplied)

	compose, name := upGatewayStack(t, ctx, environment.Config{SuppliedCertificate: &certificate})

	t.Run("the gateway serves the supplied certificate", func(t *testing.T) {
		served, err := servedCertificate(t.Context(), compose.HTTPSAddress(), &tls.Config{ServerName: "localhost", RootCAs: roots, MinVersion: tls.VersionTLS12})
		require.NoError(t, err)
		assert.Equal(t, supplied.Raw, served.Raw)

		client := &http.Client{
			Transport:     &http.Transport{TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}},
			CheckRedirect: withoutRedirects.CheckRedirect,
		}

		info := fetch(t, client, "https://"+compose.HTTPSAddress()+"/info", "", nil)
		assert.Equal(t, http.StatusOK, info.status, info.body)
	})

	t.Run("plain HTTP to the domain is redirected to HTTPS", func(t *testing.T) {
		plain := fetch(t, withoutRedirects, compose.BaseURL()+"/devices?page=2", "", nil)

		assert.Equal(t, http.StatusPermanentRedirect, plain.status)
		assert.Equal(t, "https://localhost/devices?page=2", plain.header.Get("Location"))
	})

	t.Run("plain HTTP for a name the gateway does not serve is answered 404", func(t *testing.T) {
		unknown := fetch(t, withoutRedirects, compose.BaseURL()+"/info", "unknown.example", nil)

		assert.Equal(t, http.StatusNotFound, unknown.status)
		assert.Empty(t, unknown.header.Get("Location"))
	})

	t.Run("tearing the stack down from another process removes the certificate it wrote", func(t *testing.T) {
		requireAttachedDownRemoves(t, ctx, compose, name, compose.Envs()["SHELLHUB_TEST_TLS_DIR"])
	})
}

// TestGatewayACME runs the gateway with automatic HTTPS on and its ACME CA pointed at a Pebble
// server in the stack, in place of Let's Encrypt's production directory.
func TestGatewayACME(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	compose, name := upGatewayStack(t, ctx, environment.Config{ACME: true, Envs: map[string]string{"SHELLHUB_DOMAIN": acmeDomain}})

	t.Run("the gateway serves a certificate the overriding CA issued", func(t *testing.T) {
		var served *x509.Certificate

		require.EventuallyWithT(t, func(tt *assert.CollectT) {
			var err error

			served, err = servedCertificate(t.Context(), compose.HTTPSAddress(), &tls.Config{
				ServerName:         acmeDomain,
				InsecureSkipVerify: true, //nolint:gosec // Pebble's root is generated when it starts; the issuer is what the case reads
				MinVersion:         tls.VersionTLS12,
			})
			assert.NoError(tt, err, "the gateway holds no certificate for %s yet", acmeDomain)
		}, 2*time.Minute, 2*time.Second)

		assert.True(t, strings.HasPrefix(served.Issuer.CommonName, "Pebble Intermediate CA"), "issued by %q", served.Issuer.CommonName)
		assert.NoError(t, served.VerifyHostname(acmeDomain))
	})

	t.Run("tearing the stack down from another process removes the Pebble files it wrote", func(t *testing.T) {
		requireAttachedDownRemoves(t, ctx, compose, name, compose.Envs()["SHELLHUB_TEST_ACME_DIR"])
	})
}