Skip to content

Commit 79d2c63

Browse files
authored
Add RunTLS (#120)
* added run tls * setup run tls bool flag * options update for tls config and files; server adjustments for tls * serve updated after rebase * removed RunTLS to simplify API and updated tests * chore: typo fix for WithoutStartupMessages * functional test for tls servers * graceful shutdown for http server too * moved TestServer_Run at the end of file above TestServer_RunTLS * removed options add RunTLS back in; updated log messages with proto * acme-tls example with certmagic (#1) * certmagic poc * add missing flag parse * add with tls config from acme client * listen on http too * manage sync + https redir * updated acme tls example * acme tls update with comments * remove debug log from acme-tls example * acme-tls RunTLS * chore: acme-tls comment update * tls test without logger * removed acme-tls example
1 parent 28e33da commit 79d2c63

File tree

4 files changed

+223
-32
lines changed

4 files changed

+223
-32
lines changed

openapi.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,12 @@ func (s *Server) registerOpenAPIRoutes(jsonSpec []byte) {
120120
Path: s.OpenAPIConfig.SwaggerUrl + "/",
121121
}, s.OpenAPIConfig.UIHandler(s.OpenAPIConfig.JsonUrl))
122122

123-
s.printOpenAPIMessage(fmt.Sprintf("JSON spec: http://%s%s", s.Server.Addr, s.OpenAPIConfig.JsonUrl))
124-
s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: http://%s%s/index.html", s.Server.Addr, s.OpenAPIConfig.SwaggerUrl))
123+
proto := "http"
124+
if s.isTLS {
125+
proto = "https"
126+
}
127+
s.printOpenAPIMessage(fmt.Sprintf("JSON spec: %s://%s%s", proto, s.Server.Addr, s.OpenAPIConfig.JsonUrl))
128+
s.printOpenAPIMessage(fmt.Sprintf("OpenAPI UI: %s://%s%s/index.html", proto, s.Server.Addr, s.OpenAPIConfig.SwaggerUrl))
125129
}
126130

127131
func (s *Server) printOpenAPIMessage(msg string) {

options.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type Server struct {
4242
// [http.ServeMux.Handle] can also be used to register routes.
4343
Mux *http.ServeMux
4444

45-
// Not stored with the oter middlewares because it is a special case :
45+
// Not stored with the other middlewares because it is a special case :
4646
// it applies on routes that are not registered.
4747
// For example, it allows OPTIONS /foo even if it is not declared (only GET /foo is declared).
4848
corsMiddleware func(http.Handler) http.Handler
@@ -68,14 +68,16 @@ type Server struct {
6868
template *template.Template // TODO: use preparsed templates
6969

7070
DisallowUnknownFields bool // If true, the server will return an error if the request body contains unknown fields. Useful for quick debugging in development.
71-
DisableOpenapi bool // If true, the the routes within the server will not generate an openapi spec.
71+
DisableOpenapi bool // If true, the routes within the server will not generate an openapi spec.
7272
maxBodySize int64
7373
Serialize func(w http.ResponseWriter, ans any) // Used to serialize the response. Defaults to [SendJSON].
7474
SerializeError func(w http.ResponseWriter, err error) // Used to serialize the error response. Defaults to [SendJSONError].
7575
ErrorHandler func(err error) error // Used to transform any error into a unified error type structure with status code. Defaults to [ErrorHandler]
7676
startTime time.Time
7777

7878
OpenAPIConfig OpenAPIConfig
79+
80+
isTLS bool
7981
}
8082

8183
// NewServer creates a new server with the given options.
@@ -284,7 +286,7 @@ func WithErrorHandler(errorHandler func(err error) error) func(*Server) {
284286
return func(c *Server) { c.ErrorHandler = errorHandler }
285287
}
286288

287-
// WithoutStartupMessage disables the startup message
289+
// WithoutStartupMessages disables the startup message
288290
func WithoutStartupMessages() func(*Server) {
289291
return func(c *Server) { c.disableStartupMessages = true }
290292
}

serve.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,33 @@ func (s *Server) Run() error {
2626
return s.Server.ListenAndServe()
2727
}
2828

29+
// RunTLS starts the server with a TLS listener
30+
// It is blocking.
31+
// It returns an error if the server could not start (it could not bind to the port for example).
32+
// It also generates the OpenAPI spec and outputs it to a file, the UI, and a handler (if enabled).
33+
func (s *Server) RunTLS(certFile, keyFile string) error {
34+
s.isTLS = true
35+
go s.OutputOpenAPISpec()
36+
37+
s.printStartupMessage()
38+
39+
s.Server.Handler = s.Mux
40+
if s.corsMiddleware != nil {
41+
s.Server.Handler = s.corsMiddleware(s.Server.Handler)
42+
}
43+
44+
return s.Server.ListenAndServeTLS(certFile, keyFile)
45+
}
46+
2947
func (s *Server) printStartupMessage() {
3048
if !s.disableStartupMessages {
3149
elapsed := time.Since(s.startTime)
3250
slog.Debug("Server started in "+elapsed.String(), "info", "time between since server creation (fuego.NewServer) and server startup (fuego.Run). Depending on your implementation, there might be things that do not depend on fuego slowing start time")
33-
slog.Info("Server running ✅ on http://"+s.Server.Addr, "started in", elapsed.String())
51+
proto := "http"
52+
if s.isTLS {
53+
proto = "https"
54+
}
55+
slog.Info("Server running ✅ on "+proto+"://"+s.Server.Addr, "started in", elapsed.String())
3456
}
3557
}
3658

serve_test.go

Lines changed: 189 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ package fuego
22

33
import (
44
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/tls"
8+
"crypto/x509"
9+
"crypto/x509/pkix"
10+
"encoding/pem"
511
"errors"
12+
"fmt"
613
"io"
14+
"math/big"
15+
"net"
716
"net/http"
817
"net/http/httptest"
18+
"os"
919
"testing"
1020
"time"
1121

@@ -162,32 +172,6 @@ func TestHttpHandler(t *testing.T) {
162172
})
163173
}
164174

165-
func TestServer_Run(t *testing.T) {
166-
// This is not a standard test, it is here to ensure that the server can run.
167-
// Please do not run this kind of test for your controllers, it is NOT unit testing.
168-
t.Run("can run server", func(t *testing.T) {
169-
s := NewServer(
170-
WithoutLogger(),
171-
)
172-
173-
Get(s, "/test", func(ctx *ContextNoBody) (string, error) {
174-
return "OK", nil
175-
})
176-
177-
go func() {
178-
s.Run()
179-
}()
180-
181-
require.Eventually(t, func() bool {
182-
req := httptest.NewRequest("GET", "/test", nil)
183-
w := httptest.NewRecorder()
184-
s.Mux.ServeHTTP(w, req)
185-
186-
return w.Body.String() == `OK`
187-
}, 5*time.Millisecond, 500*time.Microsecond)
188-
})
189-
}
190-
191175
func TestSetStatusBeforeSend(t *testing.T) {
192176
s := NewServer()
193177

@@ -400,3 +384,182 @@ func TestIni(t *testing.T) {
400384
})
401385
})
402386
}
387+
388+
func TestServer_Run(t *testing.T) {
389+
// This is not a standard test, it is here to ensure that the server can run.
390+
// Please do not run this kind of test for your controllers, it is NOT unit testing.
391+
t.Run("can run server", func(t *testing.T) {
392+
s := NewServer(
393+
WithoutLogger(),
394+
)
395+
396+
Get(s, "/test", func(ctx *ContextNoBody) (string, error) {
397+
return "OK", nil
398+
})
399+
400+
go func() {
401+
s.Run()
402+
}()
403+
defer func() { // stop our test server when we are done
404+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
405+
if err := s.Server.Shutdown(ctx); err != nil {
406+
t.Log(err)
407+
}
408+
cancel()
409+
}()
410+
411+
require.Eventually(t, func() bool {
412+
req := httptest.NewRequest("GET", "/test", nil)
413+
w := httptest.NewRecorder()
414+
s.Mux.ServeHTTP(w, req)
415+
416+
return w.Body.String() == `OK`
417+
}, 5*time.Millisecond, 500*time.Microsecond)
418+
})
419+
}
420+
421+
func TestServer_RunTLS(t *testing.T) {
422+
// This is not a standard test, it is here to ensure that the server can run.
423+
// Please do not run this kind of test for your controllers, it is NOT unit testing.
424+
testHelper, err := newTLSTestHelper()
425+
if err != nil {
426+
t.Fatal(err)
427+
}
428+
testTLSConfig, err := testHelper.getTLSConfig()
429+
if err != nil {
430+
t.Fatal(err)
431+
}
432+
testCertFile, testKeyFile, err := testHelper.getTLSFiles()
433+
if err != nil {
434+
t.Fatal(err)
435+
}
436+
defer os.Remove(testCertFile)
437+
defer os.Remove(testKeyFile)
438+
439+
tt := []struct {
440+
name string
441+
tlsConfig *tls.Config
442+
certFile string
443+
keyFile string
444+
}{
445+
{
446+
name: "can run TLS server with TLS config and empty files",
447+
tlsConfig: testTLSConfig,
448+
},
449+
{
450+
name: "can run TLS server with TLS files",
451+
certFile: testCertFile,
452+
keyFile: testKeyFile,
453+
},
454+
}
455+
for _, tc := range tt {
456+
t.Run(tc.name, func(t *testing.T) {
457+
s := NewServer(WithoutLogger())
458+
459+
if tc.tlsConfig != nil {
460+
s.Server.TLSConfig = tc.tlsConfig
461+
}
462+
463+
Get(s, "/test", func(ctx *ContextNoBody) (string, error) {
464+
return "OK", nil
465+
})
466+
467+
go func() { // start our test server async
468+
_ = s.RunTLS(tc.certFile, tc.keyFile)
469+
}()
470+
defer func() { // stop our test server when we are done
471+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
472+
if err := s.Server.Shutdown(ctx); err != nil {
473+
t.Log(err)
474+
}
475+
cancel()
476+
}()
477+
478+
// wait for the server to start
479+
conn, err := net.DialTimeout("tcp", s.Server.Addr, 1*time.Second)
480+
if err != nil {
481+
t.Fatal(err)
482+
}
483+
defer conn.Close()
484+
485+
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
486+
resp, err := client.Get(fmt.Sprintf("https://%s/test", s.Server.Addr))
487+
if err != nil {
488+
t.Fatal(err)
489+
}
490+
body, err := io.ReadAll(resp.Body)
491+
if err != nil {
492+
t.Fatal(err)
493+
}
494+
require.Equal(t, []byte("OK"), body)
495+
})
496+
}
497+
}
498+
499+
type tlsTestHelper struct {
500+
cert []byte
501+
key []byte
502+
}
503+
504+
func (h *tlsTestHelper) getTLSConfig() (*tls.Config, error) {
505+
cert, err := tls.X509KeyPair(h.cert, h.key)
506+
if err != nil {
507+
return nil, err
508+
}
509+
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
510+
}
511+
512+
func (h *tlsTestHelper) getTLSFiles() (string, string, error) {
513+
certFile, err := os.CreateTemp("", "fuego-test-cert-")
514+
if err != nil {
515+
return "", "", err
516+
}
517+
defer certFile.Close()
518+
519+
keyFile, err := os.CreateTemp("", "fuego-test-key-")
520+
if err != nil {
521+
return "", "", err
522+
}
523+
defer keyFile.Close()
524+
525+
if _, err := certFile.Write(h.cert); err != nil {
526+
return "", "", err
527+
}
528+
529+
if _, err := keyFile.Write(h.key); err != nil {
530+
return "", "", err
531+
}
532+
533+
return certFile.Name(), keyFile.Name(), nil
534+
}
535+
536+
func newTLSTestHelper() (*tlsTestHelper, error) {
537+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
538+
if err != nil {
539+
return nil, err
540+
}
541+
542+
template := x509.Certificate{
543+
SerialNumber: big.NewInt(1),
544+
Subject: pkix.Name{
545+
Organization: []string{"Example Org"},
546+
},
547+
NotBefore: time.Now(),
548+
NotAfter: time.Now().Add(1 * time.Minute),
549+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
550+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
551+
BasicConstraintsValid: true,
552+
}
553+
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
554+
if err != nil {
555+
return nil, err
556+
}
557+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
558+
559+
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
560+
if err != nil {
561+
return nil, err
562+
}
563+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privateKeyBytes})
564+
return &tlsTestHelper{cert: certPEM, key: keyPEM}, nil
565+
}

0 commit comments

Comments
 (0)