Skip to content

Commit cf38c73

Browse files
authoredJun 5, 2023
commands: Add TLS/HTTPS support to hugo server
* commands: Add TLS/HTTPS support to hugo server The "auto cert" handling in this PR is backed by mkcert (see link below). To get this up and running on a new PC, you can: ``` hugo server trust hugo server --tlsAuto ``` When `--tlsAuto` (or `--tlsCertFile` and `--tlsKeyFile`) is set and no `--baseURL` is provided as a flag, the server is started with TLS and `https` as the protocol. Note that you only need to run `hugo server trust` once per PC. If you already have the key and the cert file (e.g. by using mkcert directly), you can do: ``` hugo server --tlsCertFile mycert.pem --tlsKeyFile mykey.pem ``` See https://github.com/FiloSottile/mkcert Fixes #11064
1 parent 536bf71 commit cf38c73

File tree

5 files changed

+176
-22
lines changed

5 files changed

+176
-22
lines changed
 

‎commands/commandeer.go

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"errors"
1919
"fmt"
2020
"io"
21+
"log"
2122
"os"
2223
"os/signal"
2324
"path/filepath"
@@ -389,6 +390,9 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
389390
if r.quiet {
390391
r.Out = io.Discard
391392
}
393+
// Used by mkcert (server).
394+
log.SetOutput(r.Out)
395+
392396
r.Printf = func(format string, v ...interface{}) {
393397
if !r.quiet {
394398
fmt.Fprintf(r.Out, format, v...)

‎commands/server.go

+156-18
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@ package commands
1616
import (
1717
"bytes"
1818
"context"
19+
"crypto/tls"
20+
"crypto/x509"
1921
"encoding/json"
22+
"encoding/pem"
2023
"errors"
2124
"fmt"
2225
"io"
26+
"io/ioutil"
2327
"net"
2428
"net/http"
2529
"net/url"
2630
"os"
2731
"sync"
2832
"sync/atomic"
2933

34+
"github.com/bep/mclib"
35+
3036
"os/signal"
3137
"path"
3238
"path/filepath"
@@ -54,6 +60,7 @@ import (
5460
"github.com/gohugoio/hugo/transform"
5561
"github.com/gohugoio/hugo/transform/livereloadinject"
5662
"github.com/spf13/afero"
63+
"github.com/spf13/cobra"
5764
"github.com/spf13/fsync"
5865
"golang.org/x/sync/errgroup"
5966
"golang.org/x/sync/semaphore"
@@ -96,13 +103,40 @@ func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(rel
96103
}
97104

98105
func newServerCommand() *serverCommand {
106+
// Flags.
107+
var uninstall bool
108+
99109
var c *serverCommand
110+
100111
c = &serverCommand{
101112
quit: make(chan bool),
113+
commands: []simplecobra.Commander{
114+
&simpleCommand{
115+
name: "trust",
116+
short: "Install the local CA in the system trust store.",
117+
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
118+
action := "-install"
119+
if uninstall {
120+
action = "-uninstall"
121+
}
122+
os.Args = []string{action}
123+
return mclib.RunMain()
124+
},
125+
withc: func(cmd *cobra.Command, r *rootCommand) {
126+
cmd.Flags().BoolVar(&uninstall, "uninstall", false, "Uninstall the local CA (but do not delete it).")
127+
128+
},
129+
},
130+
},
102131
}
132+
103133
return c
104134
}
105135

136+
func (c *serverCommand) Commands() []simplecobra.Commander {
137+
return c.commands
138+
}
139+
106140
type countingStatFs struct {
107141
afero.Fs
108142
statCounter uint64
@@ -422,6 +456,9 @@ type serverCommand struct {
422456
navigateToChanged bool
423457
serverAppend bool
424458
serverInterface string
459+
tlsCertFile string
460+
tlsKeyFile string
461+
tlsAuto bool
425462
serverPort int
426463
liveReloadPort int
427464
serverWatch bool
@@ -431,10 +468,6 @@ type serverCommand struct {
431468
disableBrowserError bool
432469
}
433470

434-
func (c *serverCommand) Commands() []simplecobra.Commander {
435-
return c.commands
436-
}
437-
438471
func (c *serverCommand) Name() string {
439472
return "server"
440473
}
@@ -494,6 +527,9 @@ of a second, you will be able to save and see your changes nearly instantly.`
494527
cmd.Flags().IntVarP(&c.serverPort, "port", "p", 1313, "port on which the server will listen")
495528
cmd.Flags().IntVar(&c.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
496529
cmd.Flags().StringVarP(&c.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
530+
cmd.Flags().StringVarP(&c.tlsCertFile, "tlsCertFile", "", "", "path to TLS certificate file")
531+
cmd.Flags().StringVarP(&c.tlsKeyFile, "tlsKeyFile", "", "", "path to TLS key file")
532+
cmd.Flags().BoolVar(&c.tlsAuto, "tlsAuto", false, "generate and use locally-trusted certificates.")
497533
cmd.Flags().BoolVarP(&c.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
498534
cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
499535
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.`
507543
cmd.Flags().String("memstats", "", "log memory usage to this file")
508544
cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
509545

546+
cmd.Flags().SetAnnotation("tlsCertFile", cobra.BashCompSubdirsInDir, []string{})
547+
cmd.Flags().SetAnnotation("tlsKeyFile", cobra.BashCompSubdirsInDir, []string{})
548+
510549
r := cd.Root.Command.(*rootCommand)
511550
applyLocalFlagsBuild(cmd, r)
512551

@@ -524,7 +563,14 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
524563
if err := c.createServerPorts(cd); err != nil {
525564
return err
526565
}
566+
567+
if (c.tlsCertFile == "" || c.tlsKeyFile == "") && c.tlsAuto {
568+
c.withConfE(func(conf *commonConfig) error {
569+
return c.createCertificates(conf)
570+
})
571+
}
527572
}
573+
528574
if err := c.setBaseURLsInConfig(); err != nil {
529575
return err
530576
}
@@ -619,6 +665,78 @@ func (c *serverCommand) getErrorWithContext() any {
619665
return m
620666
}
621667

668+
func (c *serverCommand) createCertificates(conf *commonConfig) error {
669+
hostname := "localhost"
670+
if c.r.baseURL != "" {
671+
u, err := url.Parse(c.r.baseURL)
672+
if err != nil {
673+
return err
674+
}
675+
hostname = u.Hostname()
676+
}
677+
678+
// For now, store these in the Hugo cache dir.
679+
// Hugo should probably introduce some concept of a less temporary application directory.
680+
keyDir := filepath.Join(conf.configs.LoadingInfo.BaseConfig.CacheDir, "_mkcerts")
681+
682+
// Create the directory if it doesn't exist.
683+
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
684+
if err := os.MkdirAll(keyDir, 0777); err != nil {
685+
return err
686+
}
687+
}
688+
689+
c.tlsCertFile = filepath.Join(keyDir, fmt.Sprintf("%s.pem", hostname))
690+
c.tlsKeyFile = filepath.Join(keyDir, fmt.Sprintf("%s-key.pem", hostname))
691+
692+
// Check if the certificate already exists and is valid.
693+
certPEM, err := ioutil.ReadFile(c.tlsCertFile)
694+
if err == nil {
695+
rootPem, err := ioutil.ReadFile(filepath.Join(mclib.GetCAROOT(), "rootCA.pem"))
696+
if err == nil {
697+
if err := c.verifyCert(rootPem, certPEM, hostname); err == nil {
698+
c.r.Println("Using existing", c.tlsCertFile, "and", c.tlsKeyFile)
699+
return nil
700+
}
701+
}
702+
}
703+
704+
c.r.Println("Creating TLS certificates in", keyDir)
705+
706+
// Yes, this is unfortunate, but it's currently the only way to use Mkcert as a library.
707+
os.Args = []string{"-cert-file", c.tlsCertFile, "-key-file", c.tlsKeyFile, hostname}
708+
return mclib.RunMain()
709+
710+
}
711+
712+
func (c *serverCommand) verifyCert(rootPEM, certPEM []byte, name string) error {
713+
roots := x509.NewCertPool()
714+
ok := roots.AppendCertsFromPEM(rootPEM)
715+
if !ok {
716+
return fmt.Errorf("failed to parse root certificate")
717+
}
718+
719+
block, _ := pem.Decode(certPEM)
720+
if block == nil {
721+
return fmt.Errorf("failed to parse certificate PEM")
722+
}
723+
cert, err := x509.ParseCertificate(block.Bytes)
724+
if err != nil {
725+
return fmt.Errorf("failed to parse certificate: %v", err.Error())
726+
}
727+
728+
opts := x509.VerifyOptions{
729+
DNSName: name,
730+
Roots: roots,
731+
}
732+
733+
if _, err := cert.Verify(opts); err != nil {
734+
return fmt.Errorf("failed to verify certificate: %v", err.Error())
735+
}
736+
737+
return nil
738+
}
739+
622740
func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
623741
flags := cd.CobraCommand.Flags()
624742
var cerr error
@@ -661,36 +779,40 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
661779

662780
// fixURL massages the baseURL into a form needed for serving
663781
// all pages correctly.
664-
func (c *serverCommand) fixURL(baseURL, s string, port int) (string, error) {
782+
func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port int) (string, error) {
783+
certsSet := (c.tlsCertFile != "" && c.tlsKeyFile != "") || c.tlsAuto
665784
useLocalhost := false
666-
if s == "" {
667-
s = baseURL
785+
baseURL := baseURLFromFlag
786+
if baseURL == "" {
787+
baseURL = baseURLFromConfig
668788
useLocalhost = true
669789
}
670790

671-
if !strings.HasSuffix(s, "/") {
672-
s = s + "/"
791+
if !strings.HasSuffix(baseURL, "/") {
792+
baseURL = baseURL + "/"
673793
}
674794

675795
// do an initial parse of the input string
676-
u, err := url.Parse(s)
796+
u, err := url.Parse(baseURL)
677797
if err != nil {
678798
return "", err
679799
}
680800

681801
// if no Host is defined, then assume that no schema or double-slash were
682802
// present in the url. Add a double-slash and make a best effort attempt.
683-
if u.Host == "" && s != "/" {
684-
s = "//" + s
803+
if u.Host == "" && baseURL != "/" {
804+
baseURL = "//" + baseURL
685805

686-
u, err = url.Parse(s)
806+
u, err = url.Parse(baseURL)
687807
if err != nil {
688808
return "", err
689809
}
690810
}
691811

692812
if useLocalhost {
693-
if u.Scheme == "https" {
813+
if certsSet {
814+
u.Scheme = "https"
815+
} else if u.Scheme == "https" {
694816
u.Scheme = "http"
695817
}
696818
u.Host = "localhost"
@@ -807,10 +929,22 @@ func (c *serverCommand) serve() error {
807929

808930
for i := range baseURLs {
809931
mu, listener, serverURL, endpoint, err := srv.createEndpoint(i)
810-
srv := &http.Server{
811-
Addr: endpoint,
812-
Handler: mu,
932+
var srv *http.Server
933+
if c.tlsCertFile != "" && c.tlsKeyFile != "" {
934+
srv = &http.Server{
935+
Addr: endpoint,
936+
Handler: mu,
937+
TLSConfig: &tls.Config{
938+
MinVersion: tls.VersionTLS12,
939+
},
940+
}
941+
} else {
942+
srv = &http.Server{
943+
Addr: endpoint,
944+
Handler: mu,
945+
}
813946
}
947+
814948
servers = append(servers, srv)
815949

816950
if doLiveReload {
@@ -824,7 +958,11 @@ func (c *serverCommand) serve() error {
824958
}
825959
c.r.Printf("Web Server is available at %s (bind address %s)\n", serverURL, c.serverInterface)
826960
wg1.Go(func() error {
827-
err = srv.Serve(listener)
961+
if c.tlsCertFile != "" && c.tlsKeyFile != "" {
962+
err = srv.ServeTLS(listener, c.tlsCertFile, c.tlsKeyFile)
963+
} else {
964+
err = srv.Serve(listener)
965+
}
828966
if err != nil && err != http.ErrServerClosed {
829967
return err
830968
}

‎go.mod

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/bep/gowebp v0.2.0
1616
github.com/bep/helpers v0.4.0
1717
github.com/bep/lazycache v0.2.0
18+
github.com/bep/mclib v1.20400.20402
1819
github.com/bep/overlayfs v0.6.0
1920
github.com/bep/simplecobra v0.3.1
2021
github.com/bep/tmc v0.5.1
@@ -125,7 +126,7 @@ require (
125126
github.com/perimeterx/marshmallow v1.1.4 // indirect
126127
github.com/russross/blackfriday/v2 v2.1.0 // indirect
127128
go.opencensus.io v0.24.0 // indirect
128-
golang.org/x/crypto v0.3.0 // indirect
129+
golang.org/x/crypto v0.9.0 // indirect
129130
golang.org/x/mod v0.10.0 // indirect
130131
golang.org/x/oauth2 v0.7.0 // indirect
131132
golang.org/x/sys v0.8.0 // indirect
@@ -135,6 +136,8 @@ require (
135136
google.golang.org/grpc v1.54.0 // indirect
136137
google.golang.org/protobuf v1.30.0 // indirect
137138
gopkg.in/yaml.v3 v3.0.1 // indirect
139+
howett.net/plist v1.0.0 // indirect
140+
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
138141
)
139142

140143
go 1.18

‎go.sum

+11-2
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ github.com/bep/helpers v0.4.0 h1:ab9veaAiWY4ST48Oxp5usaqivDmYdB744fz+tcZ3Ifs=
178178
github.com/bep/helpers v0.4.0/go.mod h1:/QpHdmcPagDw7+RjkLFCvnlUc8lQ5kg4KDrEkb2Yyco=
179179
github.com/bep/lazycache v0.2.0 h1:HKrlZTrDxHIrNKqmnurH42ryxkngCMYLfBpyu40VcwY=
180180
github.com/bep/lazycache v0.2.0/go.mod h1:xUIsoRD824Vx0Q/n57+ZO7kmbEhMBOnTjM/iPixNGbg=
181+
github.com/bep/mclib v1.20400.20402 h1:olpCE2WSPpOAbFE1R4hnftSEmQ34+xzy2HRzd0m69rA=
182+
github.com/bep/mclib v1.20400.20402/go.mod h1:pkrk9Kyfqg34Uj6XlDq9tdEFJBiL1FvCoCgVKRzw1EY=
181183
github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo=
182184
github.com/bep/overlayfs v0.6.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM=
183185
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=
411413
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
412414
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
413415
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
416+
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
414417
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
415418
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
416419
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
595598
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
596599
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
597600
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
598-
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
599-
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
601+
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
602+
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
603+
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
600604
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
601605
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
602606
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
10241028
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
10251029
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
10261030
gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0=
1031+
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
10271032
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
10281033
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
10291034
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
10421047
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
10431048
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
10441049
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
1050+
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
1051+
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
10451052
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
10461053
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
10471054
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
10481055
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
10491056
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
1057+
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
1058+
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=

‎testscripts/commands/gen.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Test the gen commands.
22
# Note that adding new commands will require updating the NUM_COMMANDS value.
3-
env NUM_COMMANDS=41
3+
env NUM_COMMANDS=42
44

55
hugo gen -h
66
stdout 'A collection of several useful generators\.'

0 commit comments

Comments
 (0)
Please sign in to comment.