Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,7 @@ admin/Cargo.lock
/target
.envrc

# locally generated certs for testing TLS
*.crt
*.pem
*.csr
*.srl
*.ext
# TODO(v1): remove when removing the legacy e2e tests.
/certs/

*.test
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ COSIGN_FLAGS ?= -y -a GIT_HASH=$(GIT_COMMIT) -a GIT_VERSION=$(VERSION) -a BUILD_
## Tool Binaries
CONTROLLER_GEN ?= go tool controller-gen

# TODO(v1): remove DOMAINS, ABC_DOMAINS, and the cert targets below when removing the legacy e2e tests.
define DOMAINS
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
Expand Down Expand Up @@ -95,6 +96,7 @@ ko-build: ko-build-operator ko-build-interceptor ko-build-scaler
# Testing #
##################################################

# TODO(v1): remove cert targets below when removing the legacy e2e tests.
rootca-test-certs:
mkdir -p certs
openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout certs/RootCA.key -out certs/RootCA.pem -subj "/C=US/CN=Keda-Root-CA"
Expand All @@ -111,7 +113,8 @@ test-certs: rootca-test-certs
clean-test-certs:
rm -rf certs

test: test-certs
.PHONY: test
test:
go test ./...

e2e-test-legacy:
Expand Down
55 changes: 7 additions & 48 deletions interceptor/tls_config_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"path/filepath"
"slices"
"testing"
"time"

"github.com/go-logr/logr"

"github.com/kedacore/http-add-on/pkg/testutil"
)

func TestBuildTLSConfig_CertificatePath(t *testing.T) {
Expand Down Expand Up @@ -101,7 +95,7 @@ func TestBuildTLSConfig_NoDefaultCert(t *testing.T) {

func TestBuildTLSConfig_MissingKeyFile(t *testing.T) {
dir := t.TempDir()
certPEM, _ := generateCertAndKeyPEM(t, []string{"example.com"}, nil)
certPEM, _ := testutil.GenerateCertPEM(t, []string{"example.com"}, nil)
writeFile(t, filepath.Join(dir, "server.crt"), certPEM)

opts := TLSOptions{CertStorePaths: dir}
Expand All @@ -114,7 +108,7 @@ func TestBuildTLSConfig_MissingKeyFile(t *testing.T) {

func TestBuildTLSConfig_PemFormat(t *testing.T) {
dir := t.TempDir()
certPEM, keyPEM := generateCertAndKeyPEM(t, []string{"pem.example.com"}, nil)
certPEM, keyPEM := testutil.GenerateCertPEM(t, []string{"pem.example.com"}, nil)
writeFile(t, filepath.Join(dir, "server.pem"), certPEM)
writeFile(t, filepath.Join(dir, "server-key.pem"), keyPEM)

Expand All @@ -130,7 +124,7 @@ func TestBuildTLSConfig_PemFormat(t *testing.T) {

func TestBuildTLSConfig_IPAddressSAN(t *testing.T) {
dir := t.TempDir()
certPEM, keyPEM := generateCertAndKeyPEM(t, nil, []net.IP{net.ParseIP("192.168.1.100")})
certPEM, keyPEM := testutil.GenerateCertPEM(t, nil, []net.IP{net.ParseIP("192.168.1.100")})
writeFile(t, filepath.Join(dir, "ip.crt"), certPEM)
writeFile(t, filepath.Join(dir, "ip.key"), keyPEM)

Expand All @@ -156,7 +150,7 @@ func TestBuildTLSConfig_InvalidContent(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
dir := t.TempDir()
certPEM, keyPEM := generateCertAndKeyPEM(t, []string{"example.com"}, nil)
certPEM, keyPEM := testutil.GenerateCertPEM(t, []string{"example.com"}, nil)

if tt.invalidCert {
certPEM = []byte("not a valid certificate")
Expand Down Expand Up @@ -294,7 +288,7 @@ func requireCertForHost(t *testing.T, cfg *tls.Config, host string) {

func writeCert(t *testing.T, dir, name, dnsName string) {
t.Helper()
certPEM, keyPEM := generateCertAndKeyPEM(t, []string{dnsName}, nil)
certPEM, keyPEM := testutil.GenerateCertPEM(t, []string{dnsName}, nil)
writeFile(t, filepath.Join(dir, name+".crt"), certPEM)
writeFile(t, filepath.Join(dir, name+".key"), keyPEM)
}
Expand All @@ -305,38 +299,3 @@ func writeFile(t *testing.T, path string, data []byte) {
t.Fatalf("writing %s: %v", path, err)
}
}

func generateCertAndKeyPEM(t *testing.T, dnsNames []string, ipAddresses []net.IP) (certPEM, keyPEM []byte) {
t.Helper()

key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generating key: %v", err)
}

template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{Organization: []string{"Test"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}

certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
t.Fatalf("creating certificate: %v", err)
}

keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatalf("marshaling key: %v", err)
}

certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
return
}
17 changes: 13 additions & 4 deletions pkg/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ package http
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"

"github.com/kedacore/http-add-on/pkg/util"
)

// ServeContext creates a TCP listener on addr and serves HTTP(S) requests until ctx is cancelled.
func ServeContext(ctx context.Context, addr string, hdl http.Handler, tlsConfig *tls.Config) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("listening on %s: %w", addr, err)
}
return serve(ctx, ln, hdl, tlsConfig)
}

func serve(ctx context.Context, ln net.Listener, hdl http.Handler, tlsConfig *tls.Config) error {
srv := &http.Server{
Handler: hdl,
Addr: addr,
TLSConfig: tlsConfig,
ReadHeaderTimeout: time.Minute, // mitigate Slowloris attacks
}
Expand All @@ -27,8 +37,7 @@ func ServeContext(ctx context.Context, addr string, hdl http.Handler, tlsConfig
}()

if tlsConfig != nil {
return srv.ListenAndServeTLS("", "")
return srv.ServeTLS(ln, "", "")
}

return srv.ListenAndServe()
return srv.Serve(ln)
}
127 changes: 52 additions & 75 deletions pkg/http/server_test.go
Original file line number Diff line number Diff line change
@@ -1,91 +1,68 @@
package http

import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/kedacore/http-add-on/pkg/testutil"
)

func TestServeContext(t *testing.T) {
r := require.New(t)
ctx, done := context.WithCancel(
context.Background(),
)
defer done()
hdl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
if err := rc.EnableFullDuplex(); err != nil {
t.Fatalf("error enabling full duplex: %v", err)
}
func TestServe(t *testing.T) {
cert, caPool := testutil.GenerateCert(t, []string{"localhost"}, []net.IP{net.IPv4(127, 0, 0, 1)})

w.Header().Set("foo", "bar")
_, err := w.Write([]byte("hello world"))
if err != nil {
t.Fatalf("error writing message to client from handler")
}
})
addr := "localhost:1234"
const waitDur = 100 * time.Millisecond
const cancelDur = 400 * time.Millisecond
go func() {
time.Sleep(waitDur)
tests := map[string]struct {
serverTLS *tls.Config
clientTLS *tls.Config
}{
"plain HTTP": {},
"TLS": {
serverTLS: &tls.Config{
Certificates: []tls.Certificate{cert},
},
clientTLS: &tls.Config{
RootCAs: caPool,
},
},
}

// send a request so the handler runs
resp, err := http.Get("http://" + addr)
if err != nil {
panic(fmt.Sprintf("error sending request to the server: %v", err))
}
defer resp.Body.Close()
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// Create a listener on a free port
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("listen: %v", err)
}

time.Sleep(cancelDur)
done()
}()
start := time.Now()
err := ServeContext(ctx, addr, hdl, nil)
elapsed := time.Since(start)
// Launch the server
hdl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
go func() {
_ = serve(t.Context(), ln, hdl, tc.serverTLS)
}()

r.Error(err)
r.ErrorIs(err, http.ErrServerClosed, "error is not a http.ErrServerClosed (%w)", err)
r.Greater(elapsed, cancelDur)
r.Less(elapsed, cancelDur*4)
}
// Send a test request
scheme := "http"
if tc.clientTLS != nil {
scheme = "https"
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tc.clientTLS}}

func TestServeContextWithTLS(t *testing.T) {
r := require.New(t)
ctx, done := context.WithCancel(
context.Background(),
)
defer done()
hdl := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("foo", "bar")
_, err := w.Write([]byte("hello world"))
if err != nil {
t.Fatalf("error writing message to client from handler")
}
})
addr := "localhost:1234"
const cancelDur = 500 * time.Millisecond
go func() {
time.Sleep(cancelDur)
done()
}()
start := time.Now()
tlsConfig := tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair("../../certs/tls.crt", "../../certs/tls.key")
return &cert, err
},
}
err := ServeContext(ctx, addr, hdl, &tlsConfig)
elapsed := time.Since(start)
resp, err := client.Get(scheme + "://" + ln.Addr().String())
if err != nil {
t.Fatalf("request: %v", err)
}
_ = resp.Body.Close()

r.Error(err)
r.ErrorIs(err, http.ErrServerClosed, "error is not a http.ErrServerClosed (%w)", err)
r.Greater(elapsed, cancelDur)
r.Less(elapsed, cancelDur*4)
// Verify response
if got, want := resp.StatusCode, http.StatusOK; got != want {
t.Fatalf("status = %d, want %d", got, want)
}
if tc.serverTLS != nil && resp.TLS == nil {
t.Fatal("expected TLS connection, but resp.TLS is nil")
}
})
}
}
Loading
Loading