Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTPS support to bucket server #3098

Merged
merged 1 commit into from Dec 1, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Add HTTPS support to bucket server
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
commit babd91574b99b310b84aeec9f8f895bd18acb967
57 changes: 28 additions & 29 deletions cmd/gitops-bucket-server/main.go
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
@@ -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
@@ -0,0 +1,115 @@
package http_test
chanwit marked this conversation as resolved.
Show resolved Hide resolved

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
@@ -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
@@ -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-----