@@ -16,17 +16,23 @@ package commands
16
16
import (
17
17
"bytes"
18
18
"context"
19
+ "crypto/tls"
20
+ "crypto/x509"
19
21
"encoding/json"
22
+ "encoding/pem"
20
23
"errors"
21
24
"fmt"
22
25
"io"
26
+ "io/ioutil"
23
27
"net"
24
28
"net/http"
25
29
"net/url"
26
30
"os"
27
31
"sync"
28
32
"sync/atomic"
29
33
34
+ "github.com/bep/mclib"
35
+
30
36
"os/signal"
31
37
"path"
32
38
"path/filepath"
@@ -54,6 +60,7 @@ import (
54
60
"github.com/gohugoio/hugo/transform"
55
61
"github.com/gohugoio/hugo/transform/livereloadinject"
56
62
"github.com/spf13/afero"
63
+ "github.com/spf13/cobra"
57
64
"github.com/spf13/fsync"
58
65
"golang.org/x/sync/errgroup"
59
66
"golang.org/x/sync/semaphore"
@@ -96,13 +103,40 @@ func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(rel
96
103
}
97
104
98
105
func newServerCommand () * serverCommand {
106
+ // Flags.
107
+ var uninstall bool
108
+
99
109
var c * serverCommand
110
+
100
111
c = & serverCommand {
101
112
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
+ },
102
131
}
132
+
103
133
return c
104
134
}
105
135
136
+ func (c * serverCommand ) Commands () []simplecobra.Commander {
137
+ return c .commands
138
+ }
139
+
106
140
type countingStatFs struct {
107
141
afero.Fs
108
142
statCounter uint64
@@ -422,6 +456,9 @@ type serverCommand struct {
422
456
navigateToChanged bool
423
457
serverAppend bool
424
458
serverInterface string
459
+ tlsCertFile string
460
+ tlsKeyFile string
461
+ tlsAuto bool
425
462
serverPort int
426
463
liveReloadPort int
427
464
serverWatch bool
@@ -431,10 +468,6 @@ type serverCommand struct {
431
468
disableBrowserError bool
432
469
}
433
470
434
- func (c * serverCommand ) Commands () []simplecobra.Commander {
435
- return c .commands
436
- }
437
-
438
471
func (c * serverCommand ) Name () string {
439
472
return "server"
440
473
}
@@ -494,6 +527,9 @@ of a second, you will be able to save and see your changes nearly instantly.`
494
527
cmd .Flags ().IntVarP (& c .serverPort , "port" , "p" , 1313 , "port on which the server will listen" )
495
528
cmd .Flags ().IntVar (& c .liveReloadPort , "liveReloadPort" , - 1 , "port for live reloading (i.e. 443 in HTTPS proxy situations)" )
496
529
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." )
497
533
cmd .Flags ().BoolVarP (& c .serverWatch , "watch" , "w" , true , "watch filesystem for changes and recreate as needed" )
498
534
cmd .Flags ().BoolVar (& c .noHTTPCache , "noHTTPCache" , false , "prevent HTTP caching" )
499
535
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.`
507
543
cmd .Flags ().String ("memstats" , "" , "log memory usage to this file" )
508
544
cmd .Flags ().String ("meminterval" , "100ms" , "interval to poll memory usage (requires --memstats), valid time units are \" ns\" , \" us\" (or \" µs\" ), \" ms\" , \" s\" , \" m\" , \" h\" ." )
509
545
546
+ cmd .Flags ().SetAnnotation ("tlsCertFile" , cobra .BashCompSubdirsInDir , []string {})
547
+ cmd .Flags ().SetAnnotation ("tlsKeyFile" , cobra .BashCompSubdirsInDir , []string {})
548
+
510
549
r := cd .Root .Command .(* rootCommand )
511
550
applyLocalFlagsBuild (cmd , r )
512
551
@@ -524,7 +563,14 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
524
563
if err := c .createServerPorts (cd ); err != nil {
525
564
return err
526
565
}
566
+
567
+ if (c .tlsCertFile == "" || c .tlsKeyFile == "" ) && c .tlsAuto {
568
+ c .withConfE (func (conf * commonConfig ) error {
569
+ return c .createCertificates (conf )
570
+ })
571
+ }
527
572
}
573
+
528
574
if err := c .setBaseURLsInConfig (); err != nil {
529
575
return err
530
576
}
@@ -619,6 +665,78 @@ func (c *serverCommand) getErrorWithContext() any {
619
665
return m
620
666
}
621
667
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
+
622
740
func (c * serverCommand ) createServerPorts (cd * simplecobra.Commandeer ) error {
623
741
flags := cd .CobraCommand .Flags ()
624
742
var cerr error
@@ -661,36 +779,40 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
661
779
662
780
// fixURL massages the baseURL into a form needed for serving
663
781
// 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
665
784
useLocalhost := false
666
- if s == "" {
667
- s = baseURL
785
+ baseURL := baseURLFromFlag
786
+ if baseURL == "" {
787
+ baseURL = baseURLFromConfig
668
788
useLocalhost = true
669
789
}
670
790
671
- if ! strings .HasSuffix (s , "/" ) {
672
- s = s + "/"
791
+ if ! strings .HasSuffix (baseURL , "/" ) {
792
+ baseURL = baseURL + "/"
673
793
}
674
794
675
795
// do an initial parse of the input string
676
- u , err := url .Parse (s )
796
+ u , err := url .Parse (baseURL )
677
797
if err != nil {
678
798
return "" , err
679
799
}
680
800
681
801
// if no Host is defined, then assume that no schema or double-slash were
682
802
// 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
685
805
686
- u , err = url .Parse (s )
806
+ u , err = url .Parse (baseURL )
687
807
if err != nil {
688
808
return "" , err
689
809
}
690
810
}
691
811
692
812
if useLocalhost {
693
- if u .Scheme == "https" {
813
+ if certsSet {
814
+ u .Scheme = "https"
815
+ } else if u .Scheme == "https" {
694
816
u .Scheme = "http"
695
817
}
696
818
u .Host = "localhost"
@@ -807,10 +929,22 @@ func (c *serverCommand) serve() error {
807
929
808
930
for i := range baseURLs {
809
931
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
+ }
813
946
}
947
+
814
948
servers = append (servers , srv )
815
949
816
950
if doLiveReload {
@@ -824,7 +958,11 @@ func (c *serverCommand) serve() error {
824
958
}
825
959
c .r .Printf ("Web Server is available at %s (bind address %s)\n " , serverURL , c .serverInterface )
826
960
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
+ }
828
966
if err != nil && err != http .ErrServerClosed {
829
967
return err
830
968
}
0 commit comments