mirror of
https://github.com/gosticks/simplecert.git
synced 2026-10-06 14:47:28 +00:00
bootstrapped local mode, for automatically generating a trusted self signed cert via mkcert tool for local development, added example
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
*.pem
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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("", ""))
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user