diff --git a/commands/commandeer.go b/commands/commandeer.go index 0ae6aefce..d43d06b8a 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "io" + "log" "os" "os/signal" "path/filepath" @@ -389,6 +390,9 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { if r.quiet { r.Out = io.Discard } + // Used by mkcert (server). + log.SetOutput(r.Out) + r.Printf = func(format string, v ...interface{}) { if !r.quiet { fmt.Fprintf(r.Out, format, v...) diff --git a/commands/server.go b/commands/server.go index 048b13ba3..4efb47f27 100644 --- a/commands/server.go +++ b/commands/server.go @@ -16,10 +16,14 @@ package commands import ( "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" + "io/ioutil" "net" "net/http" "net/url" @@ -27,6 +31,8 @@ import ( "sync" "sync/atomic" + "github.com/bep/mclib" + "os/signal" "path" "path/filepath" @@ -54,6 +60,7 @@ import ( "github.com/gohugoio/hugo/transform" "github.com/gohugoio/hugo/transform/livereloadinject" "github.com/spf13/afero" + "github.com/spf13/cobra" "github.com/spf13/fsync" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" @@ -96,13 +103,40 @@ func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(rel } func newServerCommand() *serverCommand { + // Flags. + var uninstall bool + var c *serverCommand + c = &serverCommand{ quit: make(chan bool), + commands: []simplecobra.Commander{ + &simpleCommand{ + name: "trust", + short: "Install the local CA in the system trust store.", + run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { + action := "-install" + if uninstall { + action = "-uninstall" + } + os.Args = []string{action} + return mclib.RunMain() + }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.Flags().BoolVar(&uninstall, "uninstall", false, "Uninstall the local CA (but do not delete it).") + + }, + }, + }, } + return c } +func (c *serverCommand) Commands() []simplecobra.Commander { + return c.commands +} + type countingStatFs struct { afero.Fs statCounter uint64 @@ -422,6 +456,9 @@ type serverCommand struct { navigateToChanged bool serverAppend bool serverInterface string + tlsCertFile string + tlsKeyFile string + tlsAuto bool serverPort int liveReloadPort int serverWatch bool @@ -431,10 +468,6 @@ type serverCommand struct { disableBrowserError bool } -func (c *serverCommand) Commands() []simplecobra.Commander { - return c.commands -} - func (c *serverCommand) Name() string { return "server" } @@ -494,6 +527,9 @@ of a second, you will be able to save and see your changes nearly instantly.` cmd.Flags().IntVarP(&c.serverPort, "port", "p", 1313, "port on which the server will listen") cmd.Flags().IntVar(&c.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)") cmd.Flags().StringVarP(&c.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind") + cmd.Flags().StringVarP(&c.tlsCertFile, "tlsCertFile", "", "", "path to TLS certificate file") + cmd.Flags().StringVarP(&c.tlsKeyFile, "tlsKeyFile", "", "", "path to TLS key file") + cmd.Flags().BoolVar(&c.tlsAuto, "tlsAuto", false, "generate and use locally-trusted certificates.") cmd.Flags().BoolVarP(&c.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed") cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching") cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL") @@ -507,6 +543,9 @@ of a second, you will be able to save and see your changes nearly instantly.` cmd.Flags().String("memstats", "", "log memory usage to this file") cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") + cmd.Flags().SetAnnotation("tlsCertFile", cobra.BashCompSubdirsInDir, []string{}) + cmd.Flags().SetAnnotation("tlsKeyFile", cobra.BashCompSubdirsInDir, []string{}) + r := cd.Root.Command.(*rootCommand) applyLocalFlagsBuild(cmd, r) @@ -524,7 +563,14 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error { if err := c.createServerPorts(cd); err != nil { return err } + + if (c.tlsCertFile == "" || c.tlsKeyFile == "") && c.tlsAuto { + c.withConfE(func(conf *commonConfig) error { + return c.createCertificates(conf) + }) + } } + if err := c.setBaseURLsInConfig(); err != nil { return err } @@ -619,6 +665,78 @@ func (c *serverCommand) getErrorWithContext() any { return m } +func (c *serverCommand) createCertificates(conf *commonConfig) error { + hostname := "localhost" + if c.r.baseURL != "" { + u, err := url.Parse(c.r.baseURL) + if err != nil { + return err + } + hostname = u.Hostname() + } + + // For now, store these in the Hugo cache dir. + // Hugo should probably introduce some concept of a less temporary application directory. + keyDir := filepath.Join(conf.configs.LoadingInfo.BaseConfig.CacheDir, "_mkcerts") + + // Create the directory if it doesn't exist. + if _, err := os.Stat(keyDir); os.IsNotExist(err) { + if err := os.MkdirAll(keyDir, 0777); err != nil { + return err + } + } + + c.tlsCertFile = filepath.Join(keyDir, fmt.Sprintf("%s.pem", hostname)) + c.tlsKeyFile = filepath.Join(keyDir, fmt.Sprintf("%s-key.pem", hostname)) + + // Check if the certificate already exists and is valid. + certPEM, err := ioutil.ReadFile(c.tlsCertFile) + if err == nil { + rootPem, err := ioutil.ReadFile(filepath.Join(mclib.GetCAROOT(), "rootCA.pem")) + if err == nil { + if err := c.verifyCert(rootPem, certPEM, hostname); err == nil { + c.r.Println("Using existing", c.tlsCertFile, "and", c.tlsKeyFile) + return nil + } + } + } + + c.r.Println("Creating TLS certificates in", keyDir) + + // Yes, this is unfortunate, but it's currently the only way to use Mkcert as a library. + os.Args = []string{"-cert-file", c.tlsCertFile, "-key-file", c.tlsKeyFile, hostname} + return mclib.RunMain() + +} + +func (c *serverCommand) verifyCert(rootPEM, certPEM []byte, name string) error { + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(rootPEM) + if !ok { + return fmt.Errorf("failed to parse root certificate") + } + + block, _ := pem.Decode(certPEM) + if block == nil { + return fmt.Errorf("failed to parse certificate PEM") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate: %v", err.Error()) + } + + opts := x509.VerifyOptions{ + DNSName: name, + Roots: roots, + } + + if _, err := cert.Verify(opts); err != nil { + return fmt.Errorf("failed to verify certificate: %v", err.Error()) + } + + return nil +} + func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error { flags := cd.CobraCommand.Flags() var cerr error @@ -661,36 +779,40 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error { // fixURL massages the baseURL into a form needed for serving // all pages correctly. -func (c *serverCommand) fixURL(baseURL, s string, port int) (string, error) { +func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port int) (string, error) { + certsSet := (c.tlsCertFile != "" && c.tlsKeyFile != "") || c.tlsAuto useLocalhost := false - if s == "" { - s = baseURL + baseURL := baseURLFromFlag + if baseURL == "" { + baseURL = baseURLFromConfig useLocalhost = true } - if !strings.HasSuffix(s, "/") { - s = s + "/" + if !strings.HasSuffix(baseURL, "/") { + baseURL = baseURL + "/" } // do an initial parse of the input string - u, err := url.Parse(s) + u, err := url.Parse(baseURL) if err != nil { return "", err } // if no Host is defined, then assume that no schema or double-slash were // present in the url. Add a double-slash and make a best effort attempt. - if u.Host == "" && s != "/" { - s = "//" + s + if u.Host == "" && baseURL != "/" { + baseURL = "//" + baseURL - u, err = url.Parse(s) + u, err = url.Parse(baseURL) if err != nil { return "", err } } if useLocalhost { - if u.Scheme == "https" { + if certsSet { + u.Scheme = "https" + } else if u.Scheme == "https" { u.Scheme = "http" } u.Host = "localhost" @@ -807,10 +929,22 @@ func (c *serverCommand) serve() error { for i := range baseURLs { mu, listener, serverURL, endpoint, err := srv.createEndpoint(i) - srv := &http.Server{ - Addr: endpoint, - Handler: mu, + var srv *http.Server + if c.tlsCertFile != "" && c.tlsKeyFile != "" { + srv = &http.Server{ + Addr: endpoint, + Handler: mu, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + } + } else { + srv = &http.Server{ + Addr: endpoint, + Handler: mu, + } } + servers = append(servers, srv) if doLiveReload { @@ -824,7 +958,11 @@ func (c *serverCommand) serve() error { } c.r.Printf("Web Server is available at %s (bind address %s)\n", serverURL, c.serverInterface) wg1.Go(func() error { - err = srv.Serve(listener) + if c.tlsCertFile != "" && c.tlsKeyFile != "" { + err = srv.ServeTLS(listener, c.tlsCertFile, c.tlsKeyFile) + } else { + err = srv.Serve(listener) + } if err != nil && err != http.ErrServerClosed { return err } diff --git a/go.mod b/go.mod index 18847d142..d19c1b0ba 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/bep/gowebp v0.2.0 github.com/bep/helpers v0.4.0 github.com/bep/lazycache v0.2.0 + github.com/bep/mclib v1.20400.20402 github.com/bep/overlayfs v0.6.0 github.com/bep/simplecobra v0.3.1 github.com/bep/tmc v0.5.1 @@ -125,7 +126,7 @@ require ( github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sys v0.8.0 // indirect @@ -135,6 +136,8 @@ require ( google.golang.org/grpc v1.54.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + howett.net/plist v1.0.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect ) go 1.18 diff --git a/go.sum b/go.sum index f02171e68..ec96735a6 100644 --- a/go.sum +++ b/go.sum @@ -178,6 +178,8 @@ github.com/bep/helpers v0.4.0 h1:ab9veaAiWY4ST48Oxp5usaqivDmYdB744fz+tcZ3Ifs= github.com/bep/helpers v0.4.0/go.mod h1:/QpHdmcPagDw7+RjkLFCvnlUc8lQ5kg4KDrEkb2Yyco= github.com/bep/lazycache v0.2.0 h1:HKrlZTrDxHIrNKqmnurH42ryxkngCMYLfBpyu40VcwY= github.com/bep/lazycache v0.2.0/go.mod h1:xUIsoRD824Vx0Q/n57+ZO7kmbEhMBOnTjM/iPixNGbg= +github.com/bep/mclib v1.20400.20402 h1:olpCE2WSPpOAbFE1R4hnftSEmQ34+xzy2HRzd0m69rA= +github.com/bep/mclib v1.20400.20402/go.mod h1:pkrk9Kyfqg34Uj6XlDq9tdEFJBiL1FvCoCgVKRzw1EY= github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo= github.com/bep/overlayfs v0.6.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM= github.com/bep/simplecobra v0.3.1 h1:Ms9BucXcJRiGbPYpaJyxItYceQN/pvEZ0+V1+cUcsZ4= @@ -411,6 +413,7 @@ github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -595,8 +598,9 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1024,6 +1028,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1042,8 +1047,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/testscripts/commands/gen.txt b/testscripts/commands/gen.txt index 06f060b3c..891079e11 100644 --- a/testscripts/commands/gen.txt +++ b/testscripts/commands/gen.txt @@ -1,6 +1,6 @@ # Test the gen commands. # Note that adding new commands will require updating the NUM_COMMANDS value. -env NUM_COMMANDS=41 +env NUM_COMMANDS=42 hugo gen -h stdout 'A collection of several useful generators\.'