Skip to content

Commit

Permalink
Add HTTPS support to bucket server
Browse files Browse the repository at this point in the history
The bucket server used by `gitops beta run` now serves over HTTP as
well as HTTPS. HTTP is still necessary as Flux's Bucket resource
doesn't have a field for providing a custom CA certificate.

This is a backwards-incompatible change as the ports the server is
listening on have to provided through flags. Also, providing TLS cert
and key is mandatory.
  • Loading branch information
makkes committed Dec 1, 2022
1 parent f5617dd commit babd915
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 29 deletions.
57 changes: 28 additions & 29 deletions cmd/gitops-bucket-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,59 @@ package main

import (
"context"
"flag"
"log"
"net"
"os"
"os/signal"
"strconv"
"syscall"

"github.com/johannesboyne/gofakes3"
"github.com/johannesboyne/gofakes3/backend/s3mem"
"net/http/httptest"
"github.com/weaveworks/weave-gitops/pkg/http"
)

func main() {
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGKILL)
syscall.SIGTERM)
defer cancel()

logger := log.New(os.Stdout, "", 0)
backend := s3mem.New()
s3 := gofakes3.New(backend,
gofakes3.WithAutoBucket(true),
gofakes3.WithLogger(gofakes3.StdLog(logger, gofakes3.LogErr, gofakes3.LogWarn, gofakes3.LogInfo)))
s3Server := s3.Server()

port := "9000"
// check args
if len(os.Args) > 1 {
port = os.Args[1]
// part string to integer
_, err := strconv.Atoi(port)
if err != nil {
log.Fatalf("Invalid port number: %s", port)
}
}
var (
httpPort, httpsPort int
certFile, keyFile string
)

// create a listener with the desired port.
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatal(err)
}
flag.IntVar(&httpPort, "http-port", 9000, "TCP port to listen on for HTTP connections")
flag.IntVar(&httpsPort, "https-port", 9443, "TCP port to listen on for HTTPS connections")
flag.StringVar(&certFile, "cert-file", "", "Path to the HTTPS server certificate file")
flag.StringVar(&keyFile, "key-file", "", "Path to the HTTPS server certificate key file")
flag.Parse()

ts := httptest.NewUnstartedServer(s3.Server())
if err := ts.Listener.Close(); err != nil {
log.Fatal(err)
if certFile == "" {
logger.Fatalf("please specify the path to the HTTPS server certificate file")
}

ts.Listener = listener
// Start the server.
ts.Start()
defer ts.Close()
if keyFile == "" {
logger.Fatalf("please specify the path to the HTTPS server certificate key file")
}

logger.Println(ts.URL)
srv := http.MultiServer{
HTTPPort: httpPort,
HTTPSPort: httpsPort,
CertFile: certFile,
KeyFile: keyFile,
Logger: logger,
}

<-ctx.Done()
if err := srv.Start(ctx, s3Server); err != nil {
logger.Fatalf("server exited unexpectedly: %s", err)
}
}
88 changes: 88 additions & 0 deletions pkg/http/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package http

import (
"context"
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"sync"
)

// MultiServer lets you create and run an HTTP server that serves over both, HTTP and HTTPS. It is a convenience wrapper around net/http and crypto/tls.
type MultiServer struct {
HTTPPort int
HTTPSPort int
CertFile string
KeyFile string
Logger *log.Logger
}

// Start creates listeners for HTTP and HTTPS and starts serving requests using the provided handler. The function blocks until both servers
// are properly shut down. A shutdown can be initiated by cancelling the given context.
func (srv MultiServer) Start(ctx context.Context, handler http.Handler) error {
var wg sync.WaitGroup

tlsListener, err := createTLSListener(srv.HTTPSPort, srv.CertFile, srv.KeyFile)
if err != nil {
return fmt.Errorf("failed to create TLS listener: %w", err)
}

wg.Add(1)

go func() {
defer wg.Done()
startServer(ctx, handler, tlsListener, srv.Logger)
}()

listener, err := net.Listen("tcp", fmt.Sprintf(":%d", srv.HTTPPort))
if err != nil {
return fmt.Errorf("failed to create TCP listener: %w", err)
}

wg.Add(1)

go func() {
defer wg.Done()
startServer(ctx, handler, listener, srv.Logger)
}()

wg.Wait()

return nil
}

func createTLSListener(port int, certFile, keyFile string) (net.Listener, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("unable to load TLS key pair: %w", err)
}

listener, err := tls.Listen("tcp", fmt.Sprintf(":%d", port), &tls.Config{Certificates: []tls.Certificate{cert}})
if err != nil {
return nil, fmt.Errorf("unable to start TLS listener: %w", err)
}

return listener, nil
}

func startServer(ctx context.Context, hndlr http.Handler, listener net.Listener, logger *log.Logger) {
srv := http.Server{
Addr: listener.Addr().String(),
Handler: hndlr,
}
logger.Printf("https://" + srv.Addr)

go func() {
if err := srv.Serve(listener); err != http.ErrServerClosed {
logger.Fatalf("server quit unexpectedly: %s", err)
}
}()
<-ctx.Done()
logger.Printf("shutting down %s", listener.Addr())

if err := srv.Shutdown(ctx); err != nil && err != context.Canceled {
logger.Printf("error shutting down %s: %s", listener.Addr(), err)
}
}
115 changes: 115 additions & 0 deletions pkg/http/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package http_test

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"testing"

. "github.com/onsi/gomega"

wegohttp "github.com/weaveworks/weave-gitops/pkg/http"
)

func TestMultiServerStartReturnsImmediatelyWithClosedContext(t *testing.T) {
g := NewGomegaWithT(t)
srv := wegohttp.MultiServer{
CertFile: "testdata/localhost.crt",
KeyFile: "testdata/localhost.key",
Logger: log.Default(),
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
g.Expect(srv.Start(ctx, nil)).To(Succeed())
}

func TestMultiServerWithoutTLSConfigFailsToStart(t *testing.T) {
g := NewGomegaWithT(t)
srv := wegohttp.MultiServer{}
ctx, cancel := context.WithCancel(context.Background())
cancel()

err := srv.Start(ctx, nil)
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(HavePrefix("failed to create TLS listener"))
}

func TestMultiServerServesOverBothProtocols(t *testing.T) {
g := NewGomegaWithT(t)

httpPort := rand.Intn(49151-1024) + 1024
httpsPort := rand.Intn(49151-1024) + 1024

for httpPort == httpsPort {
httpsPort = rand.Intn(49151-1024) + 1024
}

srv := wegohttp.MultiServer{
HTTPPort: httpPort,
HTTPSPort: httpsPort,
CertFile: "testdata/localhost.crt",
KeyFile: "testdata/localhost.key",
Logger: log.Default(),
}
ctx, cancel := context.WithCancel(context.Background())

exitChan := make(chan struct{})
go func(exitChan chan<- struct{}) {
hndlr := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
fmt.Fprintf(rw, "success")
})
g.Expect(srv.Start(ctx, hndlr)).To(Succeed())
close(exitChan)
}(exitChan)

// test HTTP

resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", httpPort))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
body, err := io.ReadAll(resp.Body)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(string(body)).To(Equal("success"))

// test HTTPS

certBytes, err := os.ReadFile("testdata/localhost.crt")
g.Expect(err).NotTo(HaveOccurred())

rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(certBytes)

tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
},
}
c := http.Client{
Transport: tr,
}
resp, err = c.Get(fmt.Sprintf("https://localhost:%d/", httpsPort))
g.Expect(err).NotTo(HaveOccurred())
g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
body, err = io.ReadAll(resp.Body)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(string(body)).To(Equal("success"))

cancel()
g.Eventually(exitChan, "3s").Should(BeClosed())

// ensure both ports are freed up

_, err = c.Get(fmt.Sprintf("https://localhost:%d/", httpsPort))
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("connection refused"))

_, err = http.Get(fmt.Sprintf("http://localhost:%d/", httpPort))
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("connection refused"))
}
24 changes: 24 additions & 0 deletions pkg/http/testdata/localhost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEBTCCAu2gAwIBAgIUX5xBltyah5x8qA6RrJ11nuTKNq8wDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEyMDExNjAxMjRaFw0zMjEx
MjgxNjAxMjRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDBiUW6/gQwlU8bbQjt76pY59tgxANlGeuU8DyG7QwB
RWSenrzQvvHhAy/+mexaAf4VheAU+efmYHtACgzzeL7c9sS4j5OiJVOgJ9DKg/AI
6fz+mSFWJ6/ZT7YASG3LprGnoWHfTgGWMah+5rDwys+j/7M3f7RsUUB26hVuSgZJ
d6KU70Fge80QMxJyu+twZpMKBrsm+FGM6f+JHj7fKNiHK/LeuTee9cCEJGRPtIHI
T2NYEF9u+MR8b8MEzGL0v4HpEClhFVIb4WH0Gr5K6yFbVdi3CXYep4fJ7ggMwxJQ
PxqU/mn15UpMapkPsfDtTEhH4kBbtaBimUayMKez9x25AgMBAAGjgewwgekwHQYD
VR0OBBYEFEU59EKP2m+ZEayH7jmRfZlhJCAEMIGABgNVHSMEeTB3gBRFOfRCj9pv
mRGsh+45kX2ZYSQgBKFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUt
U3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIUX5xBltya
h5x8qA6RrJ11nuTKNq8wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAvwwFAYDVR0R
BA0wC4IJbG9jYWxob3N0MBQGA1UdEgQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0B
AQsFAAOCAQEAhAvQfrfGr3cKoAEijjYtQ7hSAnTwtxmDUNXUP8O+sMaETEo/GMPI
BGqR7oMTcvWJVbEYNifk68JrnXeNdggRSbM+wV2bCG1/Km+hhHxQp/z/U3uvn54U
cF4INCBvoOk77UteMt77OGex+gasw2Wwnas+X+/m1ezveoxYGxJ9RnRpuFcU7csp
N7cZizrRjGbpg8H+QIrq5Nf86Zo9kbBzyjPMV8Yw68eeiwJzNy3qbgAF1J1YjwXw
Mp4mDIJCY8UB+We35y4V1BOZhFJDXuqD/R4HbKn9HZo3PmFeLo15bUkmzw9n9JaC
Da8Nw7zO1EK8ifcViclb9Ubq3yyUR620zg==
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions pkg/http/testdata/localhost.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwYlFuv4EMJVPG20I7e+qWOfbYMQDZRnrlPA8hu0MAUVknp68
0L7x4QMv/pnsWgH+FYXgFPnn5mB7QAoM83i+3PbEuI+ToiVToCfQyoPwCOn8/pkh
Viev2U+2AEhty6axp6Fh304BljGofuaw8MrPo/+zN3+0bFFAduoVbkoGSXeilO9B
YHvNEDMScrvrcGaTCga7JvhRjOn/iR4+3yjYhyvy3rk3nvXAhCRkT7SByE9jWBBf
bvjEfG/DBMxi9L+B6RApYRVSG+Fh9Bq+SushW1XYtwl2HqeHye4IDMMSUD8alP5p
9eVKTGqZD7Hw7UxIR+JAW7WgYplGsjCns/cduQIDAQABAoIBAQCljAFsmToGQMGB
KTxZIwfosrNxy1lIEurz5KcxlvUM5UnTcN78BEkseyiDtTB6MXgg+voZl0bpRiBH
QBGh9ef1ZNQTNyVGrn0g4s3zXPZm+ZfiRCRC6QG/djKtfUcFy5ntVNs+QyCSU/nY
SwaRgjopA2FOmNtBSCNHVKZuR72m+sIasBC4lzZ6UJXiN8ccN6Wl2ey8Pq+Dt5Vw
4j45naYACxdwnrTeAwaRGKCg6zSOb4SFM4/CSZ0/pzD2u79/StpJk43pSr9n8EON
OZCH9GpIunsXEVLR/xR5k+Cr30ZAtvKY59rZQhKt3Q6LAE9yK4NkglV08yY314zG
ELd/qLaNAoGBAPucQLxhQhwzSMRQiaq9cz6fyLPc1xqrD1piCTP36NOXyra76aIW
/AbdVvSz8rfgGITFSRJ9XEaDvre4Po21JvXIXfk5oemp4Vmoo2lRS/oD56DbEzlP
vDVid0Mk/PsK1m9ulxE7ta0B46fYCwTg+4zY+Mf6/nX4jw/voWfnuT2LAoGBAMTp
pcugN5lbLWf7RaHH4DgU2Te8bPnFGiqCa6lkmrNKOR6ZtOdZE6pfAVdl9obBsc/l
xGx8caLxCl1GVL/Nq79SdY4NuC/r0O9Eeosa+yf+b/fRrQtXWhjEEG9ITZ5PhLhI
xkml1ma5kyNJF7X9qUkSRAWXCrFinG8mfRXPhQJLAoGAOsRNDnK86S9FQKz66okj
QK47R18+Unk/tcGOGrg9hiY+751GPViW9td9ttvMxguuTlxx68Kh6cpdojWDTr/P
4Loy0MIYQiYufy13NWMKltOQpy5j+A/airF734/lEpF+cjpnSFwk28rELHC2aiZO
OqB2wuapxk4OxA8ZKNajmm8CgYEAqm1W9AB9XpvNltuhjr5B0AgrYNQStbLkTLqI
mBnc0ySAf32lVz5/iMuli5FSZ5upXDiPYx3p9I8O22AN5dwKtBKYcBRrv/4n3Y61
SURW8GyFWEX/sXsvHZREbSx1EXndcup5xDBmeo5PTRDsFrWvGPFYMkZiGNkyb/kt
9fygMDUCgYBiHmq2MBSgV8LTJrhvZqvW+ROj7jgPogsTix30PYIXoH1NnSH/YTVb
D4Ede+YfVD/lDEz10WX8F2dgzupaPjk7KtMU+JRKX6Ran5pAZEgFxZExPuqK2g7h
Hbpt2kO1W/nuz1c2DU7dxamLO3vH6OrqDVtr6PjfhKPwFgT7zigi2A==
-----END RSA PRIVATE KEY-----

0 comments on commit babd915

Please sign in to comment.