From bceeadecae246001017b1ba2ac42db37bf47dbd3 Mon Sep 17 00:00:00 2001 From: Mickael Kerjean Date: Sun, 12 May 2019 01:58:52 +1000 Subject: [PATCH] feature (https): support for HTTPS on the server --- server/common/constants.go | 3 +- server/common/ssl/cert.go | 73 ++++++++++++++ server/common/ssl/generate.go | 46 +++++++++ server/common/ssl/index.go | 19 ++++ server/common/ssl/private.go | 73 ++++++++++++++ server/common/ssl/root.go | 54 +++++++++++ server/plugin/plg_starter_https/index.go | 116 +++++++++++++++++++++++ 7 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 server/common/ssl/cert.go create mode 100644 server/common/ssl/generate.go create mode 100644 server/common/ssl/index.go create mode 100644 server/common/ssl/private.go create mode 100644 server/common/ssl/root.go create mode 100644 server/plugin/plg_starter_https/index.go diff --git a/server/common/constants.go b/server/common/constants.go index ed6311ea..03e1cc43 100644 --- a/server/common/constants.go +++ b/server/common/constants.go @@ -12,7 +12,8 @@ const ( CONFIG_PATH = "data/state/config/" DB_PATH = "data/state/db/" FTS_PATH = "data/state/db/search/" - TMP_PATH = "data/cache/tmp/" + TMP_PATH = "data/cache/tmp/" + CERT_PATH = "data/certs" COOKIE_NAME_AUTH = "auth" COOKIE_NAME_PROOF = "proof" COOKIE_NAME_ADMIN = "admin" diff --git a/server/common/ssl/cert.go b/server/common/ssl/cert.go new file mode 100644 index 00000000..f50d99c7 --- /dev/null +++ b/server/common/ssl/cert.go @@ -0,0 +1,73 @@ +package ssl + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" +) + +func GetCertificate(key *rsa.PrivateKey, root *x509.Certificate) (*x509.Certificate, []byte, error) { + if cert, certPEM, err := pullCertificateFromFS(); err == nil { + return cert, certPEM, nil + } + cert, certPEM, err := generateNewCertificate(root, key) + if err != nil { + Clear() + return nil, nil, err + } + if err = saveCertificateToFS(certPEM); err != nil { + Clear() + return nil, nil, err + } + return cert, certPEM, nil +} + +func generateNewCertificate(root *x509.Certificate, key *rsa.PrivateKey) (*x509.Certificate, []byte, error) { + certDER, err := x509.CreateCertificate(rand.Reader, root, root, &key.PublicKey, key) + if err != nil { + return nil, nil, err + } + cert, err := x509.ParseCertificate(certDER); + if err != nil { + return nil, nil, err + } + return cert, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", Bytes: certDER, + }), nil +} + +func pullCertificateFromFS() (*x509.Certificate, []byte, error) { + file, err := os.OpenFile(certPEMPath, os.O_RDONLY, os.ModePerm) + if err != nil { + return nil, nil, err + } + defer file.Close() + certPEM, err := ioutil.ReadAll(file) + if err != nil { + return nil, nil, err + } + block, _ := pem.Decode(certPEM) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, err + } + return cert, certPEM, nil +} + +func saveCertificateToFS(certPEM []byte) error { + file, err := os.OpenFile(certPEMPath, os.O_WRONLY | os.O_CREATE, 0600) + if err != nil { + return err + } + _, err = file.Write(certPEM) + file.Close() + return err + +} + +func clearCert() { + os.Remove(certPEMPath) +} diff --git a/server/common/ssl/generate.go b/server/common/ssl/generate.go new file mode 100644 index 00000000..1cd8c9aa --- /dev/null +++ b/server/common/ssl/generate.go @@ -0,0 +1,46 @@ +package ssl + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + . "github.com/mickael-kerjean/filestash/server/common" +) + +func GenerateSelfSigned() (tls.Certificate, *x509.CertPool, error) { + var err error + var key *rsa.PrivateKey + var root *x509.Certificate + var keyPEM []byte + var certPEM []byte + var TLSCert tls.Certificate + + if key, keyPEM, err = GetPrivateKey(); err != nil { + Log.Error("[https] key_generation %v", err) + Clear() + return TLSCert, nil, err + } + if root, err = GetRoot(); err != nil { + Log.Error("[https] root_certificate %v", err) + Clear() + return TLSCert, nil, err + } + if _, certPEM, err = GetCertificate(key, root); err != nil { + Log.Error("[https] x509_certificate %v", err) + Clear() + return TLSCert, nil, err + } + if TLSCert, err = tls.X509KeyPair(certPEM, keyPEM); err != nil { + Log.Error("[https] tls_certificate %v", err) + Clear() + return TLSCert, nil, err + } + + roots := x509.NewCertPool() + if ok := roots.AppendCertsFromPEM([]byte(certPEM)); ok == false { + Log.Error("[https] tls_client") + Clear() + return TLSCert, nil, err + } + return TLSCert, roots, nil +} diff --git a/server/common/ssl/index.go b/server/common/ssl/index.go new file mode 100644 index 00000000..8ca3247e --- /dev/null +++ b/server/common/ssl/index.go @@ -0,0 +1,19 @@ +package ssl + +import ( + . "github.com/mickael-kerjean/filestash/server/common" + "path/filepath" + "os" +) + +var keyPEMPath string = filepath.Join(GetCurrentDir(), CERT_PATH, "key.pem") +var certPEMPath string = filepath.Join(GetCurrentDir(), CERT_PATH, "cert.pem") + +func init() { + os.MkdirAll(filepath.Join(GetCurrentDir(), CERT_PATH), os.ModePerm) +} + +func Clear() { + clearPrivateKey() + clearCert() +} diff --git a/server/common/ssl/private.go b/server/common/ssl/private.go new file mode 100644 index 00000000..8647d3d0 --- /dev/null +++ b/server/common/ssl/private.go @@ -0,0 +1,73 @@ +package ssl + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" +) + +func GetPrivateKey() (*rsa.PrivateKey, []byte, error) { + if private, privatePEM, err := pullPrivateKeyFromFS(); err == nil { + return private, privatePEM, nil + } + key, keyPEM, err := generateNewPrivateKey() + if err != nil { + Clear() + return nil, nil, err + } + if err = savePrivateKeyToFS(keyPEM); err != nil { + Clear() + return nil, nil, err + } + return key, keyPEM, nil +} + +func generateNewPrivateKey() (*rsa.PrivateKey, []byte, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + return key, pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }), nil +} + +func pullPrivateKeyFromFS() (*rsa.PrivateKey, []byte, error) { + file, err := os.OpenFile(keyPEMPath, os.O_RDONLY, os.ModePerm) + if err != nil { + return nil, nil, err + } + defer file.Close() + + keyPEM, err := ioutil.ReadAll(file) + if err != nil { + return nil, nil, err + } + block, _ := pem.Decode(keyPEM) + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, nil, err + } + return priv, keyPEM, nil +} + +func savePrivateKeyToFS(privatePEM []byte) error { + file, err := os.OpenFile(keyPEMPath, os.O_WRONLY | os.O_CREATE, 0600) + if err != nil { + return err + } + _, err = file.Write(privatePEM) + file.Close() + if err != nil { + return err + } + return nil +} + +func clearPrivateKey() { + os.Remove(keyPEMPath) +} diff --git a/server/common/ssl/root.go b/server/common/ssl/root.go new file mode 100644 index 00000000..b31414bb --- /dev/null +++ b/server/common/ssl/root.go @@ -0,0 +1,54 @@ +package ssl + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "time" +) + +func GetRoot() (*x509.Certificate, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + return &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Filestash"}, + }, + NotBefore: time.Now().Add(- 24 * time.Hour), + NotAfter: time.Now().Add(24 * 365 * 100 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: false, + IPAddresses: func() []net.IP { + ips := []net.IP{} + ifaces, err := net.Interfaces() + if err != nil { + return []net.IP{ net.ParseIP("127.0.0.1") } + } + for _, i := range ifaces { + addrs, err := i.Addrs() + if err != nil { + return []net.IP{ net.ParseIP("127.0.0.1") } + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + ips = append(ips, ip) + } + } + return ips + }(), + }, nil +} diff --git a/server/plugin/plg_starter_https/index.go b/server/plugin/plg_starter_https/index.go new file mode 100644 index 00000000..5b46eb2e --- /dev/null +++ b/server/plugin/plg_starter_https/index.go @@ -0,0 +1,116 @@ +package plg_start_https + +import ( + "crypto/tls" + "fmt" + "github.com/gorilla/mux" + . "github.com/mickael-kerjean/filestash/server/common" + "github.com/mickael-kerjean/filestash/server/common/ssl" + "golang.org/x/crypto/acme/autocert" + "net/http" + "path/filepath" + "time" +) + +func init() { + domain := Config.Get("general.host").String() + + Hooks.Register.Starter(func (r *mux.Router) { + Log.Info("[https] starting...%s", domain) + srv := &http.Server{ + Addr: fmt.Sprintf(":https"), + Handler: r, + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + PreferServerCipherSuites: true, + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.X25519, + }, + }, + } + + switch domain { + case "": + TLSCert, roots, err := ssl.GenerateSelfSigned() + if err != nil { + return + } + srv.TLSConfig.Certificates = []tls.Certificate{ TLSCert } + HTTPClient.Transport.(*TransformedTransport).Orig.(*http.Transport).TLSClientConfig = &tls.Config{ + RootCAs: roots, + } + HTTP.Transport.(*TransformedTransport).Orig.(*http.Transport).TLSClientConfig = &tls.Config{ + RootCAs: roots, + } + default: + mngr := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(domain), + Cache: autocert.DirCache(filepath.Join(GetCurrentDir(), CERT_PATH)), + } + srv.TLSConfig.GetCertificate = mngr.GetCertificate + } + + go func() { + if err := srv.ListenAndServeTLS("", ""); err != nil { + Log.Error("[https]: listen_serve %v", err) + return + } + }() + go func() { + srv := http.Server{ + Addr: fmt.Sprintf(":http"), + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Connection", "close") + http.Redirect( + w, + req, + "https://" + req.Host + req.URL.String(), + http.StatusMovedPermanently, + ) + }), + } + if err := srv.ListenAndServe(); err != nil { + Log.Error("[https]: http_redirect %v", err) + return + } + }() + go ensureAppHasBooted("https://127.0.0.1/about", fmt.Sprintf("[https] started")) + }) +} + + +func ensureAppHasBooted(address string, message string) { + i := 0 + for { + if i > 10 { + Log.Warning("[https] no boot") + break + } + time.Sleep(250 * time.Millisecond) + res, err := HTTPClient.Get(address) + if err != nil { + i += 1 + continue + } + res.Body.Close() + if res.StatusCode != http.StatusOK { + i += 1 + continue + } + Log.Info(message) + break + } +}