bootstrapped local mode, for automatically generating a trusted self signed cert via mkcert tool for local development, added example

This commit is contained in:
2019-02-20 18:19:00 +01:00
parent 32f3286dd3
commit e5ca1a3d50
8 changed files with 253 additions and 5 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
*.dll
*.so
*.dylib
*.pem
# Test binary, build with `go test -c`
*.test
+1 -1
View File
@@ -54,7 +54,7 @@ if err != nil {
// redirect HTTP to HTTPS
// CAUTION: This has to be done AFTER simplecert setup
// Otherwise Port 80 will be blocked and cert registration fails!
cLog.Info("starting HTTP Listener on Port 80")
log.Println("starting HTTP Listener on Port 80")
go http.ListenAndServe(":80", http.HandlerFunc(redirect))
// init strict tlsConfig with certReloader
+10
View File
@@ -0,0 +1,10 @@
# TODO
- allow graceful shutdown before renewal
- add a wrapper for ListenAndServeTLS
- use go modules for vendoring
## update readme
- local mode
- set API TOKEN for dns challenge
+4
View File
@@ -30,6 +30,7 @@ var Default = &Config{
Domains: []string{},
CacheDir: "",
DNSProvider: "",
Local: false,
}
// Config allows configuration of simplecert
@@ -66,6 +67,9 @@ type Config struct {
// DNSProvider name for DNS challenges (optional)
// see: https://godoc.org/github.com/xenolf/lego/providers/dns
DNSProvider string
// Local runmode
Local bool
}
// CheckConfig checks if config can be used to obtain a cert
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"log"
"net/http"
"github.com/foomo/simplecert"
"github.com/foomo/tlsconfig"
)
type Handler struct{}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("hello from simplecert"))
}
func main() {
// do the cert magic
cfg := simplecert.Default
cfg.Domains = []string{"yourdomain.com", "www.yourdomain.com"}
cfg.CacheDir = "letsencrypt"
cfg.SSLEmail = "you@emailprovider.com"
cfg.Local = true
certReloader, err := simplecert.Init(cfg)
if err != nil {
log.Fatal("simplecert init failed: ", err)
}
// redirect HTTP to HTTPS
log.Println("starting HTTP Listener on Port 80")
go http.ListenAndServe(":80", http.HandlerFunc(simplecert.Redirect))
// init strict tlsConfig with certReloader
tlsconf := tlsconfig.NewServerTLSConfig(tlsconfig.TLSModeServerStrict)
// now set GetCertificate to the reloaders GetCertificateFunc to enable hot reload
tlsconf.GetCertificate = certReloader.GetCertificateFunc()
// init server
s := &http.Server{
Addr: ":443",
TLSConfig: tlsconf,
Handler: Handler{},
}
log.Println("now visit: https://" + cfg.Domains[0])
// lets go
log.Fatal(s.ListenAndServeTLS("", ""))
}
+1
View File
@@ -74,6 +74,7 @@ func NewCertReloader(certPath, keyPath string, logFile *os.File) (*CertReloader,
log.Fatal("[FATAL] simplecert: failed to close logfile handle: ", err)
}
log.Println("[INFO] simplecert: closed logfile handle")
os.Exit(0)
}
}
}()
+40 -3
View File
@@ -44,6 +44,9 @@ func Init(cfg *Config) (*CertReloader, error) {
// update global config
c = cfg
// make sure the cacheDir exists
ensureCacheDirExists(c.CacheDir)
// open logfile handle
logFile, err := os.OpenFile(filepath.Join(c.CacheDir, logFileName), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0755)
if err != nil {
@@ -53,6 +56,43 @@ func Init(cfg *Config) (*CertReloader, error) {
// configure log pkg to log to stdout and into the logfile
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
if c.Local {
// update the cachedir path
// certs used in local mode are stored in the "local" subfolder
// to avoid overwriting a production certificate
c.CacheDir = filepath.Join(c.CacheDir, "local")
// make sure the cacheDir/local folder exists
ensureCacheDirExists(c.CacheDir)
var (
certFilePath = filepath.Join(c.CacheDir, "cert.pem")
keyFilePath = filepath.Join(c.CacheDir, "key.pem")
)
// check if a local cert is already cached
if certCached(c.CacheDir) {
// cert cached! Did the domains change?
// If the domains have been modified we need to generate a new certificate
if domainsChanged() {
log.Println("[INFO] cert cached but domains have changed. generating a new one...")
createLocalCert(certFilePath, keyFilePath)
}
} else {
// nothing there yet. create a new one
createLocalCert(certFilePath, keyFilePath)
}
// create entries in /etc/hosts if necessary
updateHosts()
// return a cert reloader for the local cert
return NewCertReloader(certFilePath, keyFilePath, logFile)
}
var (
certFilePath = filepath.Join(c.CacheDir, "cert.pem")
keyFilePath = filepath.Join(c.CacheDir, "key.pem")
@@ -94,9 +134,6 @@ func Init(cfg *Config) (*CertReloader, error) {
* No Cert Found. Register a new one
*/
// make sure the cacheDir exists
ensureCacheDirExists(c.CacheDir)
// get ACME Client
client := createClient(getUser())
+144
View File
@@ -13,12 +13,20 @@ import (
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/go-yaml/yaml"
"github.com/lextoumbourou/goodhosts"
"github.com/xenolf/lego/certificate"
)
@@ -26,6 +34,8 @@ import (
// even if renewal happens just before midnight and restoring afterwards
var backupDate string
const localhost = "127.0.0.1"
/*
* Utils
*/
@@ -214,3 +224,137 @@ func saveCertToDisk(cert *certificate.Resource, cacheDir string) error {
return nil
}
// Redirect a request to HTTPS and strip www. subdomain
func Redirect(w http.ResponseWriter, req *http.Request) {
// remove/add not default ports from req.Host
target := "https://" + strings.TrimPrefix(req.Host, "www.") + req.URL.Path
if len(req.URL.RawQuery) > 0 {
target += "?" + req.URL.RawQuery
}
fmt.Println("redirect: ", target, " ("+req.Host+")", "UserAgent:", req.UserAgent())
http.Redirect(w, req, target, http.StatusTemporaryRedirect)
}
// updateHosts is used in local mode
// to add all host entries for the domains
func updateHosts() {
hosts, err := goodhosts.NewHosts()
if err != nil {
log.Fatal("[ERROR] could not open hostsfile: ", err)
}
for _, d := range c.Domains {
if !hosts.Has(localhost, d) {
hosts.Add(localhost, d)
}
}
if err := hosts.Flush(); err != nil {
log.Fatal("[ERROR] could not update /etc/hosts: ", err)
}
}
// createLocalCert first creates a local root CA for mkcert
// and then generates a trusted certificate for the domains specified in the configuration
func createLocalCert(certFilePath, keyFilePath string) {
log.Println("[INFO] no cached cert found. Creating a new one for local development...")
// run mkcert to create root CA
runCommand("mkcert", "-install")
// run mkcert to generate the certificate
runCommand("mkcert", c.Domains...)
var (
newCertFile string
newKeyFile string
)
if len(c.Domains) > 1 {
newCertFile = c.Domains[0] + "+" + strconv.Itoa(len(c.Domains)-1) + ".pem"
newKeyFile = c.Domains[0] + "+" + strconv.Itoa(len(c.Domains)-1) + "-key.pem"
} else {
newCertFile = c.Domains[0] + ".pem"
newKeyFile = c.Domains[0] + "-key.pem"
}
// rename certificate file
log.Println("[INFO] renaming", newCertFile, "to", certFilePath)
err := os.Rename(newCertFile, certFilePath)
if err != nil {
log.Fatal("[ERROR] failed to rename cert file: ", err)
}
// rename key file
log.Println("[INFO] renaming", newKeyFile, "to", keyFilePath)
err = os.Rename(newKeyFile, keyFilePath)
if err != nil {
log.Fatal("[ERROR] failed to rename key file: ", err)
}
// write domains to CacheDir/domains.yml
b, err := yaml.Marshal(c.Domains)
if err != nil {
log.Fatal("[ERROR] failed to marshal domain slice: ", err)
}
// create file to persist the domains used for the cert
f, err := os.Create(filepath.Join(c.CacheDir, "domains.yml"))
if err != nil {
log.Fatal("[ERROR] failed to create domains.yml: ", err)
}
defer f.Close()
// write to disk
_, err = f.Write(b)
if err != nil {
log.Fatal("[ERROR] failed to write domains.yml: ", err)
}
}
// domainsChanged check the stored domains when running in local mode
// if they dont match the domains from the configuration
// this function returns true
func domainsChanged() bool {
// read domains.yml from local cachedir
b, err := ioutil.ReadFile(filepath.Join(c.CacheDir, "domains.yml"))
if err != nil {
log.Fatal(err)
}
// unmarshal string slice
var domains []string
err = yaml.Unmarshal(b, &domains)
if err != nil {
log.Fatal(err)
}
// if the number of entries is not equal, bail out.
if len(domains) != len(c.Domains) {
return true
}
// compare each entry
for i, d := range domains {
if d != c.Domains[i] {
return true
}
}
// identical
return false
}
// runCommand executes the named command with the supplied arguments
// and fatals on error
func runCommand(cmd string, args ...string) {
out, err := exec.Command(cmd, args...).CombinedOutput()
if err != nil {
log.Println("[ERROR] failed to run command: ", cmd+strings.Join(args, " "))
log.Fatal("[ERROR] error: ", err, ", output: ", string(out))
}
}