Skip to content

Commit babd915

Browse files
committed
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.
1 parent f5617dd commit babd915

File tree

5 files changed

+282
-29
lines changed

5 files changed

+282
-29
lines changed

cmd/gitops-bucket-server/main.go

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,59 @@ package main
22

33
import (
44
"context"
5+
"flag"
56
"log"
6-
"net"
77
"os"
88
"os/signal"
9-
"strconv"
109
"syscall"
1110

1211
"github.com/johannesboyne/gofakes3"
1312
"github.com/johannesboyne/gofakes3/backend/s3mem"
14-
"net/http/httptest"
13+
"github.com/weaveworks/weave-gitops/pkg/http"
1514
)
1615

1716
func main() {
1817
ctx, cancel := signal.NotifyContext(
1918
context.Background(),
2019
syscall.SIGINT,
21-
syscall.SIGTERM,
22-
syscall.SIGKILL)
20+
syscall.SIGTERM)
2321
defer cancel()
2422

2523
logger := log.New(os.Stdout, "", 0)
2624
backend := s3mem.New()
2725
s3 := gofakes3.New(backend,
2826
gofakes3.WithAutoBucket(true),
2927
gofakes3.WithLogger(gofakes3.StdLog(logger, gofakes3.LogErr, gofakes3.LogWarn, gofakes3.LogInfo)))
28+
s3Server := s3.Server()
3029

31-
port := "9000"
32-
// check args
33-
if len(os.Args) > 1 {
34-
port = os.Args[1]
35-
// part string to integer
36-
_, err := strconv.Atoi(port)
37-
if err != nil {
38-
log.Fatalf("Invalid port number: %s", port)
39-
}
40-
}
30+
var (
31+
httpPort, httpsPort int
32+
certFile, keyFile string
33+
)
4134

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

48-
ts := httptest.NewUnstartedServer(s3.Server())
49-
if err := ts.Listener.Close(); err != nil {
50-
log.Fatal(err)
41+
if certFile == "" {
42+
logger.Fatalf("please specify the path to the HTTPS server certificate file")
5143
}
5244

53-
ts.Listener = listener
54-
// Start the server.
55-
ts.Start()
56-
defer ts.Close()
45+
if keyFile == "" {
46+
logger.Fatalf("please specify the path to the HTTPS server certificate key file")
47+
}
5748

58-
logger.Println(ts.URL)
49+
srv := http.MultiServer{
50+
HTTPPort: httpPort,
51+
HTTPSPort: httpsPort,
52+
CertFile: certFile,
53+
KeyFile: keyFile,
54+
Logger: logger,
55+
}
5956

60-
<-ctx.Done()
57+
if err := srv.Start(ctx, s3Server); err != nil {
58+
logger.Fatalf("server exited unexpectedly: %s", err)
59+
}
6160
}

pkg/http/server.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"log"
8+
"net"
9+
"net/http"
10+
"sync"
11+
)
12+
13+
// 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.
14+
type MultiServer struct {
15+
HTTPPort int
16+
HTTPSPort int
17+
CertFile string
18+
KeyFile string
19+
Logger *log.Logger
20+
}
21+
22+
// Start creates listeners for HTTP and HTTPS and starts serving requests using the provided handler. The function blocks until both servers
23+
// are properly shut down. A shutdown can be initiated by cancelling the given context.
24+
func (srv MultiServer) Start(ctx context.Context, handler http.Handler) error {
25+
var wg sync.WaitGroup
26+
27+
tlsListener, err := createTLSListener(srv.HTTPSPort, srv.CertFile, srv.KeyFile)
28+
if err != nil {
29+
return fmt.Errorf("failed to create TLS listener: %w", err)
30+
}
31+
32+
wg.Add(1)
33+
34+
go func() {
35+
defer wg.Done()
36+
startServer(ctx, handler, tlsListener, srv.Logger)
37+
}()
38+
39+
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", srv.HTTPPort))
40+
if err != nil {
41+
return fmt.Errorf("failed to create TCP listener: %w", err)
42+
}
43+
44+
wg.Add(1)
45+
46+
go func() {
47+
defer wg.Done()
48+
startServer(ctx, handler, listener, srv.Logger)
49+
}()
50+
51+
wg.Wait()
52+
53+
return nil
54+
}
55+
56+
func createTLSListener(port int, certFile, keyFile string) (net.Listener, error) {
57+
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
58+
if err != nil {
59+
return nil, fmt.Errorf("unable to load TLS key pair: %w", err)
60+
}
61+
62+
listener, err := tls.Listen("tcp", fmt.Sprintf(":%d", port), &tls.Config{Certificates: []tls.Certificate{cert}})
63+
if err != nil {
64+
return nil, fmt.Errorf("unable to start TLS listener: %w", err)
65+
}
66+
67+
return listener, nil
68+
}
69+
70+
func startServer(ctx context.Context, hndlr http.Handler, listener net.Listener, logger *log.Logger) {
71+
srv := http.Server{
72+
Addr: listener.Addr().String(),
73+
Handler: hndlr,
74+
}
75+
logger.Printf("https://" + srv.Addr)
76+
77+
go func() {
78+
if err := srv.Serve(listener); err != http.ErrServerClosed {
79+
logger.Fatalf("server quit unexpectedly: %s", err)
80+
}
81+
}()
82+
<-ctx.Done()
83+
logger.Printf("shutting down %s", listener.Addr())
84+
85+
if err := srv.Shutdown(ctx); err != nil && err != context.Canceled {
86+
logger.Printf("error shutting down %s: %s", listener.Addr(), err)
87+
}
88+
}

pkg/http/server_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package http_test
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"fmt"
8+
"io"
9+
"log"
10+
"math/rand"
11+
"net/http"
12+
"os"
13+
"testing"
14+
15+
. "github.com/onsi/gomega"
16+
17+
wegohttp "github.com/weaveworks/weave-gitops/pkg/http"
18+
)
19+
20+
func TestMultiServerStartReturnsImmediatelyWithClosedContext(t *testing.T) {
21+
g := NewGomegaWithT(t)
22+
srv := wegohttp.MultiServer{
23+
CertFile: "testdata/localhost.crt",
24+
KeyFile: "testdata/localhost.key",
25+
Logger: log.Default(),
26+
}
27+
ctx, cancel := context.WithCancel(context.Background())
28+
cancel()
29+
g.Expect(srv.Start(ctx, nil)).To(Succeed())
30+
}
31+
32+
func TestMultiServerWithoutTLSConfigFailsToStart(t *testing.T) {
33+
g := NewGomegaWithT(t)
34+
srv := wegohttp.MultiServer{}
35+
ctx, cancel := context.WithCancel(context.Background())
36+
cancel()
37+
38+
err := srv.Start(ctx, nil)
39+
g.Expect(err).To(HaveOccurred())
40+
g.Expect(err.Error()).To(HavePrefix("failed to create TLS listener"))
41+
}
42+
43+
func TestMultiServerServesOverBothProtocols(t *testing.T) {
44+
g := NewGomegaWithT(t)
45+
46+
httpPort := rand.Intn(49151-1024) + 1024
47+
httpsPort := rand.Intn(49151-1024) + 1024
48+
49+
for httpPort == httpsPort {
50+
httpsPort = rand.Intn(49151-1024) + 1024
51+
}
52+
53+
srv := wegohttp.MultiServer{
54+
HTTPPort: httpPort,
55+
HTTPSPort: httpsPort,
56+
CertFile: "testdata/localhost.crt",
57+
KeyFile: "testdata/localhost.key",
58+
Logger: log.Default(),
59+
}
60+
ctx, cancel := context.WithCancel(context.Background())
61+
62+
exitChan := make(chan struct{})
63+
go func(exitChan chan<- struct{}) {
64+
hndlr := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
65+
fmt.Fprintf(rw, "success")
66+
})
67+
g.Expect(srv.Start(ctx, hndlr)).To(Succeed())
68+
close(exitChan)
69+
}(exitChan)
70+
71+
// test HTTP
72+
73+
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", httpPort))
74+
g.Expect(err).NotTo(HaveOccurred())
75+
g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
76+
body, err := io.ReadAll(resp.Body)
77+
g.Expect(err).NotTo(HaveOccurred())
78+
g.Expect(string(body)).To(Equal("success"))
79+
80+
// test HTTPS
81+
82+
certBytes, err := os.ReadFile("testdata/localhost.crt")
83+
g.Expect(err).NotTo(HaveOccurred())
84+
85+
rootCAs := x509.NewCertPool()
86+
rootCAs.AppendCertsFromPEM(certBytes)
87+
88+
tr := &http.Transport{
89+
TLSClientConfig: &tls.Config{
90+
RootCAs: rootCAs,
91+
},
92+
}
93+
c := http.Client{
94+
Transport: tr,
95+
}
96+
resp, err = c.Get(fmt.Sprintf("https://localhost:%d/", httpsPort))
97+
g.Expect(err).NotTo(HaveOccurred())
98+
g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
99+
body, err = io.ReadAll(resp.Body)
100+
g.Expect(err).NotTo(HaveOccurred())
101+
g.Expect(string(body)).To(Equal("success"))
102+
103+
cancel()
104+
g.Eventually(exitChan, "3s").Should(BeClosed())
105+
106+
// ensure both ports are freed up
107+
108+
_, err = c.Get(fmt.Sprintf("https://localhost:%d/", httpsPort))
109+
g.Expect(err).To(HaveOccurred())
110+
g.Expect(err.Error()).To(ContainSubstring("connection refused"))
111+
112+
_, err = http.Get(fmt.Sprintf("http://localhost:%d/", httpPort))
113+
g.Expect(err).To(HaveOccurred())
114+
g.Expect(err.Error()).To(ContainSubstring("connection refused"))
115+
}

pkg/http/testdata/localhost.crt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIEBTCCAu2gAwIBAgIUX5xBltyah5x8qA6RrJ11nuTKNq8wDQYJKoZIhvcNAQEL
3+
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEyMDExNjAxMjRaFw0zMjEx
5+
MjgxNjAxMjRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
6+
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
7+
AQUAA4IBDwAwggEKAoIBAQDBiUW6/gQwlU8bbQjt76pY59tgxANlGeuU8DyG7QwB
8+
RWSenrzQvvHhAy/+mexaAf4VheAU+efmYHtACgzzeL7c9sS4j5OiJVOgJ9DKg/AI
9+
6fz+mSFWJ6/ZT7YASG3LprGnoWHfTgGWMah+5rDwys+j/7M3f7RsUUB26hVuSgZJ
10+
d6KU70Fge80QMxJyu+twZpMKBrsm+FGM6f+JHj7fKNiHK/LeuTee9cCEJGRPtIHI
11+
T2NYEF9u+MR8b8MEzGL0v4HpEClhFVIb4WH0Gr5K6yFbVdi3CXYep4fJ7ggMwxJQ
12+
PxqU/mn15UpMapkPsfDtTEhH4kBbtaBimUayMKez9x25AgMBAAGjgewwgekwHQYD
13+
VR0OBBYEFEU59EKP2m+ZEayH7jmRfZlhJCAEMIGABgNVHSMEeTB3gBRFOfRCj9pv
14+
mRGsh+45kX2ZYSQgBKFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUt
15+
U3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIUX5xBltya
16+
h5x8qA6RrJ11nuTKNq8wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAvwwFAYDVR0R
17+
BA0wC4IJbG9jYWxob3N0MBQGA1UdEgQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0B
18+
AQsFAAOCAQEAhAvQfrfGr3cKoAEijjYtQ7hSAnTwtxmDUNXUP8O+sMaETEo/GMPI
19+
BGqR7oMTcvWJVbEYNifk68JrnXeNdggRSbM+wV2bCG1/Km+hhHxQp/z/U3uvn54U
20+
cF4INCBvoOk77UteMt77OGex+gasw2Wwnas+X+/m1ezveoxYGxJ9RnRpuFcU7csp
21+
N7cZizrRjGbpg8H+QIrq5Nf86Zo9kbBzyjPMV8Yw68eeiwJzNy3qbgAF1J1YjwXw
22+
Mp4mDIJCY8UB+We35y4V1BOZhFJDXuqD/R4HbKn9HZo3PmFeLo15bUkmzw9n9JaC
23+
Da8Nw7zO1EK8ifcViclb9Ubq3yyUR620zg==
24+
-----END CERTIFICATE-----

pkg/http/testdata/localhost.key

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEAwYlFuv4EMJVPG20I7e+qWOfbYMQDZRnrlPA8hu0MAUVknp68
3+
0L7x4QMv/pnsWgH+FYXgFPnn5mB7QAoM83i+3PbEuI+ToiVToCfQyoPwCOn8/pkh
4+
Viev2U+2AEhty6axp6Fh304BljGofuaw8MrPo/+zN3+0bFFAduoVbkoGSXeilO9B
5+
YHvNEDMScrvrcGaTCga7JvhRjOn/iR4+3yjYhyvy3rk3nvXAhCRkT7SByE9jWBBf
6+
bvjEfG/DBMxi9L+B6RApYRVSG+Fh9Bq+SushW1XYtwl2HqeHye4IDMMSUD8alP5p
7+
9eVKTGqZD7Hw7UxIR+JAW7WgYplGsjCns/cduQIDAQABAoIBAQCljAFsmToGQMGB
8+
KTxZIwfosrNxy1lIEurz5KcxlvUM5UnTcN78BEkseyiDtTB6MXgg+voZl0bpRiBH
9+
QBGh9ef1ZNQTNyVGrn0g4s3zXPZm+ZfiRCRC6QG/djKtfUcFy5ntVNs+QyCSU/nY
10+
SwaRgjopA2FOmNtBSCNHVKZuR72m+sIasBC4lzZ6UJXiN8ccN6Wl2ey8Pq+Dt5Vw
11+
4j45naYACxdwnrTeAwaRGKCg6zSOb4SFM4/CSZ0/pzD2u79/StpJk43pSr9n8EON
12+
OZCH9GpIunsXEVLR/xR5k+Cr30ZAtvKY59rZQhKt3Q6LAE9yK4NkglV08yY314zG
13+
ELd/qLaNAoGBAPucQLxhQhwzSMRQiaq9cz6fyLPc1xqrD1piCTP36NOXyra76aIW
14+
/AbdVvSz8rfgGITFSRJ9XEaDvre4Po21JvXIXfk5oemp4Vmoo2lRS/oD56DbEzlP
15+
vDVid0Mk/PsK1m9ulxE7ta0B46fYCwTg+4zY+Mf6/nX4jw/voWfnuT2LAoGBAMTp
16+
pcugN5lbLWf7RaHH4DgU2Te8bPnFGiqCa6lkmrNKOR6ZtOdZE6pfAVdl9obBsc/l
17+
xGx8caLxCl1GVL/Nq79SdY4NuC/r0O9Eeosa+yf+b/fRrQtXWhjEEG9ITZ5PhLhI
18+
xkml1ma5kyNJF7X9qUkSRAWXCrFinG8mfRXPhQJLAoGAOsRNDnK86S9FQKz66okj
19+
QK47R18+Unk/tcGOGrg9hiY+751GPViW9td9ttvMxguuTlxx68Kh6cpdojWDTr/P
20+
4Loy0MIYQiYufy13NWMKltOQpy5j+A/airF734/lEpF+cjpnSFwk28rELHC2aiZO
21+
OqB2wuapxk4OxA8ZKNajmm8CgYEAqm1W9AB9XpvNltuhjr5B0AgrYNQStbLkTLqI
22+
mBnc0ySAf32lVz5/iMuli5FSZ5upXDiPYx3p9I8O22AN5dwKtBKYcBRrv/4n3Y61
23+
SURW8GyFWEX/sXsvHZREbSx1EXndcup5xDBmeo5PTRDsFrWvGPFYMkZiGNkyb/kt
24+
9fygMDUCgYBiHmq2MBSgV8LTJrhvZqvW+ROj7jgPogsTix30PYIXoH1NnSH/YTVb
25+
D4Ede+YfVD/lDEz10WX8F2dgzupaPjk7KtMU+JRKX6Ran5pAZEgFxZExPuqK2g7h
26+
Hbpt2kO1W/nuz1c2DU7dxamLO3vH6OrqDVtr6PjfhKPwFgT7zigi2A==
27+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)