diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index 892201a..fd37907 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -31,8 +31,9 @@ type command struct { flagset *flag.FlagSet fileserver Fileserver - cacheControl *string - certificatePath *string + cacheControl *string + tlsCertPath *string + tlsKeyPath *string } func Command() *command { @@ -56,7 +57,8 @@ func (c *command) Setup() error { c.masterPath = path.Clean(relPath) if c.masterPath != "" { - c.fileserver = wsinject.NewFileServer(*c.port, *c.wsPath, *c.forceReload) + expectTLS := *c.tlsCertPath != "" && *c.tlsKeyPath != "" + c.fileserver = wsinject.NewFileServer(*c.port, *c.wsPath, *c.forceReload, expectTLS) mirrorPath, err := c.fileserver.Setup(c.masterPath) if err != nil { return fmt.Errorf("failed to setup websocket injected mirror filesystem: %v", err) @@ -85,8 +87,16 @@ func (c *command) Run(ctx context.Context) error { serverErrChan := make(chan error, 1) fsErrChan := make(chan error, 1) go func() { - ancli.PrintfOK("now serving directory: '%v' on port: '%v', mirror dir is: '%v'", c.masterPath, *c.port, c.mirrorPath) - err := s.ListenAndServe() + serveTLS := *c.tlsCertPath != "" && *c.tlsKeyPath != "" + + ancli.PrintfOK("now serving directory: '%v' on port: '%v', mirror dir is: '%v', tls: %v, certPath: '%v', keyPath: '%v'", + c.masterPath, *c.port, c.mirrorPath, serveTLS, *c.tlsCertPath, *c.tlsKeyPath) + var err error + if serveTLS { + err = s.ListenAndServeTLS(*c.tlsCertPath, *c.tlsKeyPath) + } else { + err = s.ListenAndServe() + } if !errors.Is(err, http.ErrServerClosed) { serverErrChan <- err } @@ -128,7 +138,8 @@ func (c *command) Flagset() *flag.FlagSet { c.wsPath = fs.String("wsPort", "/delta-streamer-ws", "the path which the delta streamer websocket should be hosted on") c.forceReload = fs.Bool("forceReload", false, "set to true if you wish to reload all attached browser pages on any file change") c.cacheControl = fs.String("cacheControl", "no-cache", "set to configure the cache-control header") - c.certificatePath = fs.String("certPath", "", "set to some existing cert which should be used for TLS (https)") + c.tlsCertPath = fs.String("tlsCertPath", "", "set to a path to a cert, requires tlsKeyPath to be set") + c.tlsKeyPath = fs.String("tlsKeyPath", "", "set to a path to a key, requires tlsCertPath to be set") c.flagset = fs return fs } diff --git a/cmd/serve/serve_test.go b/cmd/serve/serve_test.go index 0c7101a..bbb7329 100644 --- a/cmd/serve/serve_test.go +++ b/cmd/serve/serve_test.go @@ -2,6 +2,7 @@ package serve import ( "context" + "crypto/tls" "fmt" "net/http" "net/http/httptest" @@ -167,13 +168,34 @@ func TestRun(t *testing.T) { testboil.FailTestIfDiff(t, got, want) }) - t.Run("it should serve with tls if cert is specified", func(t *testing.T) { + t.Run("it should serve with tls if cert and key is specified", func(t *testing.T) { cmd := setup() ctx, ctxCancel := context.WithCancel(context.Background()) t.Cleanup(ctxCancel) + testCert := testboil.CreateTestFile(t, "cert.pem") + testCert.Write([]byte(`-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow +EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d +7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B +5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr +BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 +NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l +Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc +6MF9+Yw1Yy0t +-----END CERTIFICATE-----`)) + testKey := testboil.CreateTestFile(t, "key.pem") + testKey.Write([]byte(`-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 +AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q +EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== +-----END EC PRIVATE KEY-----`)) port := 13337 cmd.port = &port - cmd.certificatePath = "TODO" + certPath := testCert.Name() + cmd.tlsCertPath = &certPath + keyPath := testKey.Name() + cmd.tlsKeyPath = &keyPath ready := make(chan struct{}) go func() { @@ -185,11 +207,24 @@ func TestRun(t *testing.T) { }() <-ready time.Sleep(time.Millisecond) - resp, err := http.Get(fmt.Sprintf("http://localhost:%v", port)) + + // Cert above expired in 2018 + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + // Create an HTTP client with the custom transport + client := &http.Client{ + Transport: transport, + } + resp, err := client.Get(fmt.Sprintf("https://localhost:%v", port)) if err != nil { t.Fatal(err) } - got := resp.Header.Get("Cache-Control") - testboil.FailTestIfDiff(t, got, want) + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected status code: %v", resp.StatusCode) + } }) } diff --git a/internal/wsinject/delta_streamer.ws.go b/internal/wsinject/delta_streamer.ws.go index 981c2f5..f21c9fa 100644 --- a/internal/wsinject/delta_streamer.ws.go +++ b/internal/wsinject/delta_streamer.ws.go @@ -13,7 +13,7 @@ function startWebsocket() { } // Establish a connection with the WebSocket server - const socket = new WebSocket('ws://localhost:%v%v'); + const socket = new WebSocket('ws%v://localhost:%v%v'); // Event handler for when the WebSocket connection is established socket.addEventListener('open', function (event) { diff --git a/internal/wsinject/wsinject.go b/internal/wsinject/wsinject.go index b4bd363..8272cac 100644 --- a/internal/wsinject/wsinject.go +++ b/internal/wsinject/wsinject.go @@ -22,6 +22,7 @@ type Fileserver struct { masterPath string mirrorPath string forceReload bool + expectTLS bool wsPort int wsPath string watcher *fsnotify.Watcher @@ -37,7 +38,7 @@ var ErrNoHeaderTagFound = errors.New("no header tag found") const deltaStreamer = ` ` -func NewFileServer(wsPort int, wsPath string, forceReload bool) *Fileserver { +func NewFileServer(wsPort int, wsPath string, forceReload, expectTLS bool) *Fileserver { mirrorDir, err := os.MkdirTemp("", "wd-41_*") if err != nil { panic(err) @@ -47,6 +48,7 @@ func NewFileServer(wsPort int, wsPath string, forceReload bool) *Fileserver { mirrorPath: mirrorDir, wsPort: wsPort, wsPath: wsPath, + expectTLS: expectTLS, forceReload: forceReload, pageReloadChan: make(chan string), wsDispatcher: sync.Map{}, @@ -97,9 +99,13 @@ func (fs *Fileserver) mirrorMaker(p string, info os.DirEntry, err error) error { } func (fs *Fileserver) writeDeltaStreamerScript() error { + tlsS := "" + if fs.expectTLS { + tlsS = "s" + } err := os.WriteFile( path.Join(fs.mirrorPath, "delta-streamer.js"), - []byte(fmt.Sprintf(deltaStreamerSourceCode, fs.wsPort, fs.wsPath, fs.forceReload)), + []byte(fmt.Sprintf(deltaStreamerSourceCode, tlsS, fs.wsPort, fs.wsPath, fs.forceReload)), 0o755) if err != nil { return fmt.Errorf("failed to write delta-streamer.js: %w", err) diff --git a/internal/wsinject/wsinject_test.go b/internal/wsinject/wsinject_test.go index 3577908..5602698 100644 --- a/internal/wsinject/wsinject_test.go +++ b/internal/wsinject/wsinject_test.go @@ -76,7 +76,7 @@ func Test_Setup(t *testing.T) { } nestedFile := path.Join(nestedDir, "nested.html") os.WriteFile(nestedFile, []byte(mockHtml), 0o777) - fs := NewFileServer(8080, "/delta-streamer-ws.js", false) + fs := NewFileServer(8080, "/delta-streamer-ws.js", false, false) _, err = fs.Setup(tmpDir) if err != nil { t.Fatalf("failed to setup: %v", err) @@ -144,7 +144,7 @@ func Test_Start(t *testing.T) { if err != nil { t.Fatalf("failed to create temp dir: %v", err) } - return NewFileServer(8080, "/delta-streamer-ws.js", false), testFileSystem{ + return NewFileServer(8080, "/delta-streamer-ws.js", false, false), testFileSystem{ root: tmpDir, nestedDir: nestedDir, }