summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--caddy/assets/path_test.go12
-rw-r--r--caddy/caddy.go22
-rw-r--r--caddy/config.go24
-rw-r--r--caddy/helpers.go2
-rw-r--r--caddy/letsencrypt/crypto_test.go4
-rw-r--r--caddy/letsencrypt/letsencrypt.go82
-rw-r--r--caddy/letsencrypt/maintain.go (renamed from caddy/letsencrypt/renew.go)67
-rw-r--r--caddy/letsencrypt/user.go23
-rw-r--r--caddy/restart.go2
-rw-r--r--main.go8
-rw-r--r--server/config.go1
-rw-r--r--server/server.go6
12 files changed, 189 insertions, 64 deletions
diff --git a/caddy/assets/path_test.go b/caddy/assets/path_test.go
new file mode 100644
index 000000000..374f813af
--- /dev/null
+++ b/caddy/assets/path_test.go
@@ -0,0 +1,12 @@
+package assets
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPath(t *testing.T) {
+ if actual := Path(); !strings.HasSuffix(actual, ".caddy") {
+ t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
+ }
+}
diff --git a/caddy/caddy.go b/caddy/caddy.go
index f6976ea0a..022cbb909 100644
--- a/caddy/caddy.go
+++ b/caddy/caddy.go
@@ -26,6 +26,7 @@ import (
"strings"
"sync"
+ "github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/server"
)
@@ -90,6 +91,8 @@ const (
// In any case, an error is returned if Caddy could not be
// started.
func Start(cdyfile Input) error {
+ // TODO: What if already started -- is that an error?
+
var err error
// Input must never be nil; try to load something
@@ -104,7 +107,20 @@ func Start(cdyfile Input) error {
caddyfile = cdyfile
caddyfileMu.Unlock()
- groupings, err := Load(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
+ // load the server configs
+ configs, err := load(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
+ if err != nil {
+ return err
+ }
+
+ // secure all the things
+ configs, err = letsencrypt.Activate(configs)
+ if err != nil {
+ return err
+ }
+
+ // group virtualhosts by address
+ groupings, err := arrangeBindings(configs)
if err != nil {
return err
}
@@ -217,11 +233,15 @@ func startServers(groupings Group) error {
// Stop stops all servers. It blocks until they are all stopped.
func Stop() error {
+ letsencrypt.Deactivate()
+
serversMu.Lock()
for _, s := range servers {
s.Stop() // TODO: error checking/reporting?
}
+ servers = []*server.Server{} // don't reuse servers
serversMu.Unlock()
+
return nil
}
diff --git a/caddy/config.go b/caddy/config.go
index 5688a6db2..53432b4e9 100644
--- a/caddy/config.go
+++ b/caddy/config.go
@@ -7,7 +7,6 @@ import (
"net"
"sync"
- "github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
@@ -20,9 +19,9 @@ const (
DefaultConfigFile = "Caddyfile"
)
-// Load reads input (named filename) and parses it, returning server
-// configurations grouped by listening address.
-func Load(filename string, input io.Reader) (Group, error) {
+// load reads input (named filename) and parses it, returning the
+// server configurations in the order they appeared in the input.
+func load(filename string, input io.Reader) ([]server.Config, error) {
var configs []server.Config
// turn off timestamp for parsing
@@ -34,7 +33,7 @@ func Load(filename string, input io.Reader) (Group, error) {
return nil, err
}
if len(serverBlocks) == 0 {
- return Default()
+ return []server.Config{NewDefault()}, nil
}
// Each server block represents similar hosts/addresses.
@@ -101,14 +100,7 @@ func Load(filename string, input io.Reader) (Group, error) {
// restore logging settings
log.SetFlags(flags)
- // secure all the things
- configs, err = letsencrypt.Activate(configs)
- if err != nil {
- return nil, err
- }
-
- // group by address/virtualhosts
- return arrangeBindings(configs)
+ return configs, nil
}
// makeOnces makes a map of directive name to sync.Once
@@ -271,12 +263,6 @@ func NewDefault() server.Config {
}
}
-// Default obtains a default config and arranges
-// bindings so it's ready to use.
-func Default() (Group, error) {
- return arrangeBindings([]server.Config{NewDefault()})
-}
-
// These defaults are configurable through the command line
var (
// Site root
diff --git a/caddy/helpers.go b/caddy/helpers.go
index d8f409708..a22e7f5cc 100644
--- a/caddy/helpers.go
+++ b/caddy/helpers.go
@@ -13,7 +13,7 @@ import (
)
func init() {
- letsencrypt.OnRenew = func() error { return Restart(nil) }
+ letsencrypt.OnChange = func() error { return Restart(nil) }
}
// isLocalhost returns true if the string looks explicitly like a localhost address.
diff --git a/caddy/letsencrypt/crypto_test.go b/caddy/letsencrypt/crypto_test.go
index 938778a8d..7f791a6c3 100644
--- a/caddy/letsencrypt/crypto_test.go
+++ b/caddy/letsencrypt/crypto_test.go
@@ -10,14 +10,14 @@ import (
)
func init() {
- rsaKeySizeToUse = 128 // makes tests faster
+ rsaKeySizeToUse = 128 // make tests faster; small key size OK for testing
}
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
keyFile := "test.key"
defer os.Remove(keyFile)
- privateKey, err := rsa.GenerateKey(rand.Reader, 128) // small key size is OK for testing
+ privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
if err != nil {
t.Fatal(err)
}
diff --git a/caddy/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go
index a7aef7e83..4d58bebb5 100644
--- a/caddy/letsencrypt/letsencrypt.go
+++ b/caddy/letsencrypt/letsencrypt.go
@@ -18,12 +18,6 @@ import (
"github.com/xenolf/lego/acme"
)
-// OnRenew is the function that will be used to restart
-// the application or the part of the application that uses
-// the certificates maintained by this package. When at least
-// one certificate is renewed, this function will be called.
-var OnRenew func() error
-
// Activate sets up TLS for each server config in configs
// as needed. It only skips the config if the cert and key
// are already provided or if plaintext http is explicitly
@@ -36,10 +30,15 @@ var OnRenew func() error
// argument. If absent, it will use the most recent email
// address from last time. If there isn't one, the user
// will be prompted. If the user leaves email blank, <TODO>.
+//
+// Also note that calling this function activates asset
+// management automatically, which <TODO>.
func Activate(configs []server.Config) ([]server.Config, error) {
- // First identify and configure any elligible hosts for which
+ // TODO: Is multiple activation (before a deactivation) an error?
+
+ // First identify and configure any eligible hosts for which
// we already have certs and keys in storage from last time.
- configLen := len(configs) // avoid infinite loop since this loop appends to the slice
+ configLen := len(configs) // avoid infinite loop since this loop appends plaintext to the slice
for i := 0; i < configLen; i++ {
if existingCertAndKey(configs[i].Host) && configs[i].TLS.LetsEncryptEmail != "off" {
configs = autoConfigure(&configs[i], configs)
@@ -47,7 +46,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
}
// First renew any existing certificates that need it
- processCertificateRenewal(configs)
+ renewCertificates(configs)
// Group configs by LE email address; this will help us
// reduce round-trips when getting the certs.
@@ -62,19 +61,19 @@ func Activate(configs []server.Config) ([]server.Config, error) {
// make client to service this email address with CA server
client, err := newClient(leEmail)
if err != nil {
- return configs, err
+ return configs, errors.New("error creating client: " + err.Error())
}
// client is ready, so let's get free, trusted SSL certificates! yeah!
certificates, err := obtainCertificates(client, serverConfigs)
if err != nil {
- return configs, err
+ return configs, errors.New("error obtaining cert: " + err.Error())
}
// ... that's it. save the certs, keys, and metadata files to disk
err = saveCertsAndKeys(certificates)
if err != nil {
- return configs, err
+ return configs, errors.New("error saving assets: " + err.Error())
}
// it all comes down to this: turning TLS on for all the configs
@@ -83,11 +82,26 @@ func Activate(configs []server.Config) ([]server.Config, error) {
}
}
- go keepCertificatesRenewed(configs)
+ stopChan = make(chan struct{})
+ go maintainAssets(configs, stopChan)
return configs, nil
}
+// Deactivate cleans up long-term, in-memory resources
+// allocated by calling Activate(). Essentially, it stops
+// the asset maintainer from running, meaning that certificates
+// will not be renewed, OCSP staples will not be updated, etc.
+func Deactivate() (err error) {
+ defer func() {
+ if rec := recover(); rec != nil {
+ err = errors.New("already deactivated")
+ }
+ }()
+ close(stopChan)
+ return
+}
+
// groupConfigsByEmail groups configs by the Let's Encrypt email address
// associated to them or to the default Let's Encrypt email address. If the
// default email is not available, the user will be prompted to provide one.
@@ -158,7 +172,10 @@ func newClient(leEmail string) (*acme.Client, error) {
}
// The client facilitates our communication with the CA server.
- client := acme.NewClient(caURL, &leUser, rsaKeySizeToUse, exposePort)
+ client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, exposePort)
+ if err != nil {
+ return nil, err
+ }
// If not registered, the user must register an account with the CA
// and agree to terms
@@ -169,7 +186,13 @@ func newClient(leEmail string) (*acme.Client, error) {
}
leUser.Registration = reg
- // TODO: we can just do the agreement once: when registering, right?
+ if !Agreed && reg.TosURL == "" {
+ Agreed = promptUserAgreement("<TODO>", false) // TODO
+ }
+ if !Agreed && reg.TosURL == "" {
+ return nil, errors.New("user must agree to terms")
+ }
+
err = client.AgreeToTOS()
if err != nil {
saveUser(leUser) // TODO: Might as well try, right? Error check?
@@ -238,6 +261,15 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error {
// autoConfigure enables TLS on cfg and appends, if necessary, a new config
// to allConfigs that redirects plaintext HTTP to its new HTTPS counterpart.
func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Config {
+ bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
+ // TODO: Handle these errors better
+ if err == nil {
+ ocsp, status, err := acme.GetOCSPForCert(bundleBytes)
+ ocspStatus[&bundleBytes] = status
+ if err == nil && status == acme.OCSPGood {
+ cfg.TLS.OCSPStaple = ocsp
+ }
+ }
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
cfg.TLS.Enabled = true
@@ -331,20 +363,21 @@ var (
// Whether user has agreed to the Let's Encrypt SA
Agreed bool
+
+ // The base URL to the CA's ACME endpoint
+ CAUrl string
)
// Some essential values related to the Let's Encrypt process
const (
- // The base URL to the Let's Encrypt CA
- // TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
- // TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org
- caURL = "http://192.168.99.100:4000"
-
// The port to expose to the CA server for Simple HTTP Challenge
exposePort = "5001"
// How often to check certificates for renewal
renewInterval = 24 * time.Hour
+
+ // How often to update OCSP stapling
+ ocspInterval = 1 * time.Hour
)
// KeySize represents the length of a key in bits.
@@ -362,3 +395,12 @@ const (
// This shouldn't need to change except for in tests;
// the size can be drastically reduced for speed.
var rsaKeySizeToUse = RSA_2048
+
+// stopChan is used to signal the maintenance goroutine
+// to terminate.
+var stopChan chan struct{}
+
+// ocspStatus maps certificate bundle to OCSP status at start.
+// It is used during regular OCSP checks to see if the OCSP
+// status has changed.
+var ocspStatus = make(map[*[]byte]int)
diff --git a/caddy/letsencrypt/renew.go b/caddy/letsencrypt/maintain.go
index cd19c24e7..62af5e1ca 100644
--- a/caddy/letsencrypt/renew.go
+++ b/caddy/letsencrypt/maintain.go
@@ -10,32 +10,66 @@ import (
"github.com/xenolf/lego/acme"
)
-// keepCertificatesRenewed is a permanently-blocking function
+// OnChange is a callback function that will be used to restart
+// the application or the part of the application that uses
+// the certificates maintained by this package. When at least
+// one certificate is renewed or an OCSP status changes, this
+// function will be called.
+var OnChange func() error
+
+// maintainAssets is a permanently-blocking function
// that loops indefinitely and, on a regular schedule, checks
// certificates for expiration and initiates a renewal of certs
-// that are expiring soon.
-func keepCertificatesRenewed(configs []server.Config) {
- ticker := time.Tick(renewInterval)
- for range ticker {
- if n, errs := processCertificateRenewal(configs); len(errs) > 0 {
- for _, err := range errs {
- log.Printf("[ERROR] cert renewal: %v\n", err)
+// that are expiring soon. It also updates OCSP stapling and
+// performs other maintenance of assets.
+//
+// You must pass in the server configs to maintain and the channel
+// which you'll close when maintenance should stop, to allow this
+// goroutine to clean up after itself.
+func maintainAssets(configs []server.Config, stopChan chan struct{}) {
+ renewalTicker := time.NewTicker(renewInterval)
+ ocspTicker := time.NewTicker(ocspInterval)
+
+ for {
+ select {
+ case <-renewalTicker.C:
+ if n, errs := renewCertificates(configs); len(errs) > 0 {
+ for _, err := range errs {
+ log.Printf("[ERROR] cert renewal: %v\n", err)
+ }
+ if n > 0 && OnChange != nil {
+ err := OnChange()
+ if err != nil {
+ log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
+ }
+ }
}
- if n > 0 && OnRenew != nil {
- err := OnRenew()
- if err != nil {
- log.Printf("[ERROR] onrenew callback: %v\n", err)
+ case <-ocspTicker.C:
+ for bundle, oldStatus := range ocspStatus {
+ _, newStatus, err := acme.GetOCSPForCert(*bundle)
+ if err == nil && newStatus != oldStatus && OnChange != nil {
+ log.Printf("[INFO] ocsp status changed from %v to %v\n", oldStatus, newStatus)
+ err := OnChange()
+ if err != nil {
+ log.Printf("[ERROR] onchange after ocsp update: %v\n", err)
+ }
+ break
}
}
+ case <-stopChan:
+ renewalTicker.Stop()
+ ocspTicker.Stop()
+ return
}
}
}
-// checkCertificateRenewal loops through all configured
-// sites and looks for certificates to renew. Nothing is mutated
+// renewCertificates loops through all configured site and
+// looks for certificates to renew. Nothing is mutated
// through this function. The changes happen directly on disk.
-// It returns the number of certificates renewed and
-func processCertificateRenewal(configs []server.Config) (int, []error) {
+// It returns the number of certificates renewed and any errors
+// that occurred. It only performs a renewal if necessary.
+func renewCertificates(configs []server.Config) (int, []error) {
log.Print("[INFO] Processing certificate renewals...")
var errs []error
var n int
@@ -92,6 +126,7 @@ func processCertificateRenewal(configs []server.Config) (int, []error) {
// Renew certificate.
// TODO: revokeOld should be an option in the caddyfile
+ // TODO: bundle should be an option in the caddyfile as well :)
newCertMeta, err := client.RenewCertificate(certMeta, true, true)
if err != nil {
time.Sleep(10 * time.Second)
diff --git a/caddy/letsencrypt/user.go b/caddy/letsencrypt/user.go
index 752cc510d..ff4d6acb6 100644
--- a/caddy/letsencrypt/user.go
+++ b/caddy/letsencrypt/user.go
@@ -156,6 +156,29 @@ func getEmail(cfg server.Config) string {
return strings.TrimSpace(leEmail)
}
+// promptUserAgreement prompts the user to agree to the agreement
+// at agreementURL via stdin. If the agreement has changed, then pass
+// true as the second argument. If this is the user's first time
+// agreeing, pass false. It returns whether the user agreed or not.
+func promptUserAgreement(agreementURL string, changed bool) bool {
+ if changed {
+ fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n%s\n", agreementURL)
+ fmt.Print("Do you agree to the new terms? (y/n): ")
+ } else {
+ fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n%s\n", agreementURL)
+ fmt.Print("Do you agree to the terms? (y/n): ")
+ }
+
+ reader := bufio.NewReader(stdin) // TODO/BUG: This doesn't work when Caddyfile is piped into caddy
+ answer, err := reader.ReadString('\n')
+ if err != nil {
+ return false
+ }
+ answer = strings.ToLower(strings.TrimSpace(answer))
+
+ return answer == "y" || answer == "yes"
+}
+
// stdin is used to read the user's input if prompted;
// this is changed by tests during tests.
var stdin = io.ReadWriter(os.Stdin)
diff --git a/caddy/restart.go b/caddy/restart.go
index 7a07fbc1b..1c61c5a03 100644
--- a/caddy/restart.go
+++ b/caddy/restart.go
@@ -81,7 +81,7 @@ func Restart(newCaddyfile Input) error {
wpipe.Close()
// Wait for child process to signal success or fail
- sigwpipe.Close() // close our copy of the write end of the pipe
+ sigwpipe.Close() // close our copy of the write end of the pipe or we might be stuck
answer, err := ioutil.ReadAll(sigrpipe)
if err != nil || len(answer) == 0 {
log.Println("restart: child failed to answer; changes not applied")
diff --git a/main.go b/main.go
index 15d39de55..aa3ed0d83 100644
--- a/main.go
+++ b/main.go
@@ -29,16 +29,20 @@ const (
func init() {
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
- flag.BoolVar(&caddy.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
+ flag.BoolVar(&caddy.HTTP2, "http2", true, "HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
flag.BoolVar(&version, "version", false, "Show version")
+ // TODO: Boulder dev URL is: http://192.168.99.100:4000
+ // TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
+ // TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org
+ flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org", "Certificate authority ACME server")
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions")
- flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke its certificate")
+ flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
}
func main() {
diff --git a/server/config.go b/server/config.go
index dedd7ba37..a3bb5f50d 100644
--- a/server/config.go
+++ b/server/config.go
@@ -56,6 +56,7 @@ type TLSConfig struct {
Certificate string
Key string
LetsEncryptEmail string
+ OCSPStaple []byte
Ciphers []uint16
ProtocolMinVersion uint16
ProtocolMaxVersion uint16
diff --git a/server/server.go b/server/server.go
index 0a4dd4bab..9e7bcb389 100644
--- a/server/server.go
+++ b/server/server.go
@@ -66,6 +66,7 @@ func New(addr string, configs []Config) (*Server, error) {
// into sync.WaitGroup.Wait() - basically, an add
// with a positive delta must be guaranteed to
// occur before Wait() is called on the wg.
+ // In a way, this kind of acts as a safety barrier.
s.httpWg.Add(1)
// Set up each virtualhost
@@ -179,6 +180,7 @@ func serveTLSWithSNI(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error {
config.Certificates = make([]tls.Certificate, len(tlsConfigs))
for i, tlsConfig := range tlsConfigs {
config.Certificates[i], err = tls.LoadX509KeyPair(tlsConfig.Certificate, tlsConfig.Key)
+ config.Certificates[i].OCSPStaple = tlsConfig.OCSPStaple
if err != nil {
return err
}
@@ -227,12 +229,12 @@ func (s *Server) Stop() error {
// Wait for remaining connections to finish or
// force them all to close after timeout
select {
- case <-time.After(5 * time.Second): // TODO: configurable?
+ case <-time.After(5 * time.Second): // TODO: make configurable?
case <-done:
}
}
- // Close the listener now; this stops the server and
+ // Close the listener now; this stops the server without delay
s.listenerMu.Lock()
err := s.listener.Close()
s.listenerMu.Unlock()