mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-08 11:58:49 +03:00
letsencrypt: Fix OCSP stapling and restarts with new LE-capable hosts
Before, Caddy couldn't support graceful (zero-downtime) restarts when the reloaded Caddyfile had a host in it that was elligible for a LE certificate because the port was already in use. This commit makes it possible to do zero-downtime reloads and issue certificates for new hosts that need it. Supports only http-01 challenge at this time.
OCSP stapling is improved in that it updates before the expiration time when the validity window has shifted forward. See 30c949085c
. Before it only used to update when the status changed.
This commit also sets the user agent for Let's Encrypt requests with a string containing "Caddy".
This commit is contained in:
parent
829a0f34d0
commit
55601d3ec2
9 changed files with 284 additions and 246 deletions
|
@ -190,7 +190,8 @@ func startServers(groupings bindingGroup) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.HTTP2 = HTTP2 // TODO: This setting is temporary
|
s.HTTP2 = HTTP2 // TODO: This setting is temporary
|
||||||
|
s.ReqCallback = letsencrypt.RequestCallback // ensures we can solve ACME challenges while running
|
||||||
|
|
||||||
var ln server.ListenerFile
|
var ln server.ListenerFile
|
||||||
if IsRestart() {
|
if IsRestart() {
|
||||||
|
|
|
@ -40,12 +40,12 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
|
|
||||||
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
|
|
||||||
return x509.MarshalPKCS1PrivateKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
|
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
|
||||||
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
|
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
|
||||||
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
|
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
|
||||||
|
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
|
||||||
|
return x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
}
|
||||||
|
|
|
@ -2,30 +2,21 @@ package letsencrypt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mholt/caddy/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const challengeBasePath = "/.well-known/acme-challenge"
|
const challengeBasePath = "/.well-known/acme-challenge"
|
||||||
|
|
||||||
// Handler is a Caddy middleware that can proxy ACME challenge
|
// RequestCallback proxies challenge requests to ACME client if the
|
||||||
// requests to the real ACME client endpoint. This is necessary
|
// request path starts with challengeBasePath. It returns true if it
|
||||||
// to renew certificates while the server is running.
|
// handled the request and no more needs to be done; it returns false
|
||||||
type Handler struct {
|
// if this call was a no-op and the request still needs handling.
|
||||||
Next middleware.Handler
|
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
|
||||||
//ChallengeActive int32 // (TODO) use sync/atomic to set/get this flag safely and efficiently
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP is basically a no-op unless an ACME challenge is active on this host
|
|
||||||
// and the request path matches the expected path exactly.
|
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
// Proxy challenge requests to ACME client
|
|
||||||
// TODO: Only do this if a challenge is active?
|
|
||||||
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
|
@ -37,9 +28,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
hostname = r.URL.Host
|
hostname = r.URL.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort)
|
upstream, err := url.Parse(scheme + "://" + hostname + ":" + AlternatePort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("[ERROR] letsencrypt handler: %v", err)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||||
|
@ -48,8 +41,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
}
|
}
|
||||||
proxy.ServeHTTP(w, r)
|
proxy.ServeHTTP(w, r)
|
||||||
|
|
||||||
return 0, nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.Next.ServeHTTP(w, r)
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,14 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
|
||||||
"github.com/mholt/caddy/caddy/setup"
|
"github.com/mholt/caddy/caddy/setup"
|
||||||
"github.com/mholt/caddy/middleware"
|
"github.com/mholt/caddy/middleware"
|
||||||
"github.com/mholt/caddy/middleware/redirect"
|
"github.com/mholt/caddy/middleware/redirect"
|
||||||
|
@ -19,6 +22,91 @@ import (
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func configureExisting(configs []server.Config) []server.Config {
|
||||||
|
// 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 plaintext to the slice
|
||||||
|
for i := 0; i < configLen; i++ {
|
||||||
|
if existingCertAndKey(configs[i].Host) && ConfigQualifies(configs, i) {
|
||||||
|
configs = autoConfigure(configs, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObtainCertsAndConfigure obtains certificates for all qualifying configs.
|
||||||
|
func ObtainCertsAndConfigure(configs []server.Config, optPort string) ([]server.Config, error) {
|
||||||
|
// Group configs by email address; only configs that are eligible
|
||||||
|
// for TLS management are included. We group by email so that we
|
||||||
|
// can request certificates in batches with the same client.
|
||||||
|
// Note: The return value is a map, and iteration over a map is
|
||||||
|
// not ordered. I don't think it will be a problem, but if an
|
||||||
|
// ordering problem arises, look at this carefully.
|
||||||
|
groupedConfigs, err := groupConfigsByEmail(configs)
|
||||||
|
if err != nil {
|
||||||
|
return configs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// obtain certificates for configs that need one, and reconfigure each
|
||||||
|
// config to use the certificates
|
||||||
|
for leEmail, cfgIndexes := range groupedConfigs {
|
||||||
|
// make client to service this email address with CA server
|
||||||
|
client, err := newClientPort(leEmail, optPort)
|
||||||
|
if err != nil {
|
||||||
|
return configs, errors.New("error creating client: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// let's get free, trusted SSL certificates!
|
||||||
|
for _, idx := range cfgIndexes {
|
||||||
|
hostname := configs[idx].Host
|
||||||
|
|
||||||
|
Obtain:
|
||||||
|
certificate, failures := client.ObtainCertificate([]string{hostname}, true)
|
||||||
|
if len(failures) == 0 {
|
||||||
|
// Success - immediately save the certificate resource
|
||||||
|
err := saveCertResource(certificate)
|
||||||
|
if err != nil {
|
||||||
|
return configs, errors.New("error saving assets for " + hostname + ": " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Error - either try to fix it or report them it to the user and abort
|
||||||
|
var errMsg string // we'll combine all the failures into a single error message
|
||||||
|
var promptedForAgreement bool // only prompt user for agreement at most once
|
||||||
|
|
||||||
|
for errDomain, obtainErr := range failures {
|
||||||
|
if obtainErr != nil {
|
||||||
|
if tosErr, ok := obtainErr.(acme.TOSError); ok {
|
||||||
|
if !Agreed && !promptedForAgreement {
|
||||||
|
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
|
||||||
|
promptedForAgreement = true
|
||||||
|
}
|
||||||
|
if Agreed {
|
||||||
|
err := client.AgreeToTOS()
|
||||||
|
if err != nil {
|
||||||
|
return configs, errors.New("error agreeing to updated terms: " + err.Error())
|
||||||
|
}
|
||||||
|
goto Obtain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user did not agree or it was any other kind of error, just append to the list of errors
|
||||||
|
errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, errors.New(errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// it all comes down to this: turning on TLS with all the new certs
|
||||||
|
for _, idx := range cfgIndexes {
|
||||||
|
configs = autoConfigure(configs, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Activate sets up TLS for each server config in configs
|
// Activate sets up TLS for each server config in configs
|
||||||
// as needed. It only skips the config if the cert and key
|
// as needed. It only skips the config if the cert and key
|
||||||
// are already provided, if plaintext http is explicitly
|
// are already provided, if plaintext http is explicitly
|
||||||
|
@ -43,106 +131,24 @@ import (
|
||||||
// plaintext HTTP requests to their HTTPS counterpart.
|
// plaintext HTTP requests to their HTTPS counterpart.
|
||||||
// This function only appends; it does not prepend or splice.
|
// This function only appends; it does not prepend or splice.
|
||||||
func Activate(configs []server.Config) ([]server.Config, error) {
|
func Activate(configs []server.Config) ([]server.Config, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
// just in case previous caller forgot...
|
// just in case previous caller forgot...
|
||||||
Deactivate()
|
Deactivate()
|
||||||
|
|
||||||
// reset cached ocsp statuses from any previous activations
|
// reset cached ocsp from any previous activations
|
||||||
ocspStatus = make(map[*[]byte]int)
|
ocspCache = make(map[*[]byte]*ocsp.Response)
|
||||||
|
|
||||||
// Identify and configure any eligible hosts for which
|
// configure configs for which we have an existing certificate
|
||||||
// we already have certs and keys in storage from last time.
|
configs = configureExisting(configs)
|
||||||
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) && configQualifies(configs, i) {
|
|
||||||
configs = autoConfigure(configs, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group configs by email address; only configs that are eligible
|
// obtain certificates for configs which need one, and make them use them
|
||||||
// for TLS management are included. We group by email so that we
|
configs, err = ObtainCertsAndConfigure(configs, "")
|
||||||
// can request certificates in batches with the same client.
|
|
||||||
// Note: The return value is a map, and iteration over a map is
|
|
||||||
// not ordered. I don't think it will be a problem, but if an
|
|
||||||
// ordering problem arises, look at this carefully.
|
|
||||||
groupedConfigs, err := groupConfigsByEmail(configs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtain certificates for configs that need one, and reconfigure each
|
// renew all relevant certificates that need renewal; TODO: handle errors
|
||||||
// config to use the certificates
|
|
||||||
for leEmail, cfgIndexes := range groupedConfigs {
|
|
||||||
// make client to service this email address with CA server
|
|
||||||
client, err := newClient(leEmail)
|
|
||||||
if err != nil {
|
|
||||||
return configs, errors.New("error creating client: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// little bit of housekeeping; gather the hostnames into a slice
|
|
||||||
hosts := make([]string, len(cfgIndexes))
|
|
||||||
for i, idx := range cfgIndexes {
|
|
||||||
hosts[i] = configs[idx].Host
|
|
||||||
}
|
|
||||||
|
|
||||||
// client is ready, so let's get free, trusted SSL certificates!
|
|
||||||
Obtain:
|
|
||||||
certificates, failures := client.ObtainCertificates(hosts, true)
|
|
||||||
if len(failures) > 0 {
|
|
||||||
// Build an error string to return, using all the failures in the list.
|
|
||||||
var errMsg string
|
|
||||||
|
|
||||||
// If an error is because of updated SA, only prompt user for agreement once
|
|
||||||
var promptedForAgreement bool
|
|
||||||
|
|
||||||
for domain, obtainErr := range failures {
|
|
||||||
// If the failure was simply because the terms have changed, re-prompt and re-try
|
|
||||||
if tosErr, ok := obtainErr.(acme.TOSError); ok {
|
|
||||||
if !Agreed && !promptedForAgreement {
|
|
||||||
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
|
|
||||||
promptedForAgreement = true
|
|
||||||
}
|
|
||||||
if Agreed {
|
|
||||||
err := client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
return configs, errors.New("error agreeing to updated terms: " + err.Error())
|
|
||||||
}
|
|
||||||
goto Obtain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user did not agree or it was any other kind of error, just append to the list of errors
|
|
||||||
errMsg += "[" + domain + "] failed to get certificate: " + obtainErr.Error() + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the certs we did obtain, though, before leaving
|
|
||||||
if err := saveCertsAndKeys(certificates); err == nil {
|
|
||||||
if len(certificates) > 0 {
|
|
||||||
var certList []string
|
|
||||||
for _, cert := range certificates {
|
|
||||||
certList = append(certList, cert.Domain)
|
|
||||||
}
|
|
||||||
errMsg += "Saved certificates for: " + strings.Join(certList, ", ") + "\n"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errMsg += "Unable to save obtained certificates: " + err.Error() + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs, errors.New(errMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... that's it. save the certs, keys, and metadata files to disk
|
|
||||||
err = saveCertsAndKeys(certificates)
|
|
||||||
if err != nil {
|
|
||||||
return configs, errors.New("error saving assets: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// it all comes down to this: turning on TLS with all the new certs
|
|
||||||
for _, idx := range cfgIndexes {
|
|
||||||
configs = autoConfigure(configs, idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// renew all certificates that need renewal
|
|
||||||
renewCertificates(configs, false)
|
renewCertificates(configs, false)
|
||||||
|
|
||||||
// keep certificates renewed and OCSP stapling updated
|
// keep certificates renewed and OCSP stapling updated
|
||||||
|
@ -166,16 +172,17 @@ func Deactivate() (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// configQualifies returns true if the config at cfgIndex (within allConfigs)
|
// ConfigQualifies returns true if the config at cfgIndex (within allConfigs)
|
||||||
// qualifes for automatic LE activation. It does NOT check to see if a cert
|
// qualifes for automatic LE activation. It does NOT check to see if a cert
|
||||||
// and key already exist for the config.
|
// and key already exist for the config.
|
||||||
func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
|
func ConfigQualifies(allConfigs []server.Config, cfgIndex int) bool {
|
||||||
cfg := allConfigs[cfgIndex]
|
cfg := allConfigs[cfgIndex]
|
||||||
return cfg.TLS.Certificate == "" && // user could provide their own cert and key
|
return cfg.TLS.Certificate == "" && // user could provide their own cert and key
|
||||||
cfg.TLS.Key == "" &&
|
cfg.TLS.Key == "" &&
|
||||||
|
|
||||||
// user can force-disable automatic HTTPS for this host
|
// user can force-disable automatic HTTPS for this host
|
||||||
cfg.Port != "http" &&
|
cfg.Scheme != "http" &&
|
||||||
|
cfg.Port != "80" &&
|
||||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
cfg.TLS.LetsEncryptEmail != "off" &&
|
||||||
|
|
||||||
// obviously we get can't certs for loopback or internal hosts
|
// obviously we get can't certs for loopback or internal hosts
|
||||||
|
@ -193,13 +200,11 @@ func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
|
||||||
func HostQualifies(hostname string) bool {
|
func HostQualifies(hostname string) bool {
|
||||||
return hostname != "localhost" &&
|
return hostname != "localhost" &&
|
||||||
strings.TrimSpace(hostname) != "" &&
|
strings.TrimSpace(hostname) != "" &&
|
||||||
hostname != "0.0.0.0" &&
|
net.ParseIP(hostname) == nil && // cannot be an IP address, see: https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
||||||
hostname != "[::]" && // before parsing
|
hostname != "[::]" && // before parsing
|
||||||
hostname != "::" && // after parsing
|
hostname != "::" && // after parsing
|
||||||
hostname != "[::1]" && // before parsing
|
hostname != "[::1]" && // before parsing
|
||||||
hostname != "::1" && // after parsing
|
hostname != "::1" // after parsing
|
||||||
!strings.HasPrefix(hostname, "127.") // to use boulder on your own machine, add fake domain to hosts file
|
|
||||||
// not excluding 10.* and 192.168.* hosts for possibility of running internal Boulder instance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// groupConfigsByEmail groups configs by user email address. The returned map is
|
// groupConfigsByEmail groups configs by user email address. The returned map is
|
||||||
|
@ -214,7 +219,7 @@ func groupConfigsByEmail(configs []server.Config) (map[string][]int, error) {
|
||||||
// that we won't be obtaining certs for - this way we won't
|
// that we won't be obtaining certs for - this way we won't
|
||||||
// bother the user for an email address unnecessarily and
|
// bother the user for an email address unnecessarily and
|
||||||
// we don't obtain new certs for a host we already have certs for.
|
// we don't obtain new certs for a host we already have certs for.
|
||||||
if existingCertAndKey(configs[i].Host) || !configQualifies(configs, i) {
|
if existingCertAndKey(configs[i].Host) || !ConfigQualifies(configs, i) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
leEmail := getEmail(configs[i])
|
leEmail := getEmail(configs[i])
|
||||||
|
@ -258,10 +263,13 @@ func newClientPort(leEmail, port string) (*acme.Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The client facilitates our communication with the CA server.
|
// The client facilitates our communication with the CA server.
|
||||||
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, port)
|
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
client.SetHTTPPort(port)
|
||||||
|
client.SetTLSPort(port)
|
||||||
|
client.ExcludeChallenges([]string{"tls-sni-01", "dns-01"}) // We can only guarantee http-01 at this time
|
||||||
|
|
||||||
// If not registered, the user must register an account with the CA
|
// If not registered, the user must register an account with the CA
|
||||||
// and agree to terms
|
// and agree to terms
|
||||||
|
@ -295,48 +303,37 @@ func newClientPort(leEmail, port string) (*acme.Client, error) {
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtainCertificates obtains certificates from the CA server for
|
// saveCertResource saves the certificate resource to disk. This
|
||||||
// the configurations in serverConfigs using client.
|
|
||||||
func obtainCertificates(client *acme.Client, serverConfigs []server.Config) ([]acme.CertificateResource, map[string]error) {
|
|
||||||
var hosts []string
|
|
||||||
for _, cfg := range serverConfigs {
|
|
||||||
hosts = append(hosts, cfg.Host)
|
|
||||||
}
|
|
||||||
return client.ObtainCertificates(hosts, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveCertificates saves each certificate resource to disk. This
|
|
||||||
// includes the certificate file itself, the private key, and the
|
// includes the certificate file itself, the private key, and the
|
||||||
// metadata file.
|
// metadata file.
|
||||||
func saveCertsAndKeys(certificates []acme.CertificateResource) error {
|
func saveCertResource(cert acme.CertificateResource) error {
|
||||||
for _, cert := range certificates {
|
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||||
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save cert
|
|
||||||
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save private key
|
|
||||||
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save cert metadata
|
|
||||||
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save cert
|
||||||
|
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save private key
|
||||||
|
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save cert metadata
|
||||||
|
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,52 +348,28 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
|
||||||
bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
|
bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
|
||||||
// TODO: Handle these errors better
|
// TODO: Handle these errors better
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ocsp, status, err := acme.GetOCSPForCert(bundleBytes)
|
ocspBytes, ocspResp, err := acme.GetOCSPForCert(bundleBytes)
|
||||||
ocspStatus[&bundleBytes] = status
|
ocspCache[&bundleBytes] = ocspResp
|
||||||
if err == nil && status == acme.OCSPGood {
|
if err == nil && ocspResp.Status == ocsp.Good {
|
||||||
cfg.TLS.OCSPStaple = ocsp
|
cfg.TLS.OCSPStaple = ocspBytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
|
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
|
||||||
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
|
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
|
||||||
cfg.TLS.Enabled = true
|
cfg.TLS.Enabled = true
|
||||||
// Ensure all defaults are set for the TLS config
|
|
||||||
setup.SetDefaultTLSParams(cfg)
|
setup.SetDefaultTLSParams(cfg)
|
||||||
|
|
||||||
if cfg.Port == "" {
|
if cfg.Port == "" {
|
||||||
cfg.Port = "https"
|
cfg.Port = "443"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up http->https redirect as long as there isn't already a http counterpart
|
// Set up http->https redirect as long as there isn't already a http counterpart
|
||||||
// in the configs and this isn't, for some reason, already on port 80.
|
// in the configs and this isn't, for some reason, already on port 80.
|
||||||
// Also, the port 80 variant of this config is necessary for proxying challenge requests.
|
// Also, the port 80 variant of this config is necessary for proxying challenge requests.
|
||||||
if !otherHostHasScheme(allConfigs, cfgIndex, "http") &&
|
if !otherHostHasScheme(allConfigs, cfgIndex, "http") && cfg.Port != "80" && cfg.Scheme != "http" {
|
||||||
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case)
|
|
||||||
allConfigs = append(allConfigs, redirPlaintextHost(*cfg))
|
allConfigs = append(allConfigs, redirPlaintextHost(*cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// To support renewals, we need handlers at ports 80 and 443,
|
|
||||||
// depending on the challenge type that is used to complete renewal.
|
|
||||||
for i, c := range allConfigs {
|
|
||||||
if c.Address() == cfg.Host+":80" ||
|
|
||||||
c.Address() == cfg.Host+":443" ||
|
|
||||||
c.Address() == cfg.Host+":http" ||
|
|
||||||
c.Address() == cfg.Host+":https" {
|
|
||||||
|
|
||||||
// Each virtualhost must have their own handlers, or the chaining gets messed up when middlewares are compiled!
|
|
||||||
handler := new(Handler)
|
|
||||||
mid := func(next middleware.Handler) middleware.Handler {
|
|
||||||
handler.Next = next
|
|
||||||
return handler
|
|
||||||
}
|
|
||||||
// TODO: Currently, acmeHandlers are not referenced, but we need to add a way to toggle
|
|
||||||
// their proxy functionality -- or maybe not. Gotta figure this out for sure.
|
|
||||||
acmeHandlers[c.Address()] = handler
|
|
||||||
|
|
||||||
allConfigs[i].Middleware["/"] = append(allConfigs[i].Middleware["/"], mid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allConfigs
|
return allConfigs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,21 +379,17 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
|
||||||
// "http" and "80". It does not tell you whether there is ANY config with scheme,
|
// "http" and "80". It does not tell you whether there is ANY config with scheme,
|
||||||
// only if there's a different one with it.
|
// only if there's a different one with it.
|
||||||
func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool {
|
func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool {
|
||||||
if scheme == "80" {
|
if scheme == "http" {
|
||||||
scheme = "http"
|
scheme = "80"
|
||||||
} else if scheme == "443" {
|
} else if scheme == "https" {
|
||||||
scheme = "https"
|
scheme = "443"
|
||||||
}
|
}
|
||||||
for i, otherCfg := range allConfigs {
|
for i, otherCfg := range allConfigs {
|
||||||
if i == cfgIndex {
|
if i == cfgIndex {
|
||||||
continue // has to be a config OTHER than the one we're comparing against
|
continue // has to be a config OTHER than the one we're comparing against
|
||||||
}
|
}
|
||||||
if otherCfg.Host == allConfigs[cfgIndex].Host {
|
if otherCfg.Host == allConfigs[cfgIndex].Host && otherCfg.Port == scheme {
|
||||||
if (otherCfg.Port == scheme) ||
|
return true
|
||||||
(scheme == "https" && otherCfg.Port == "443") ||
|
|
||||||
(scheme == "http" && otherCfg.Port == "80") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -432,7 +401,7 @@ func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string)
|
||||||
// to listen on the "http" port (port 80).
|
// to listen on the "http" port (port 80).
|
||||||
func redirPlaintextHost(cfg server.Config) server.Config {
|
func redirPlaintextHost(cfg server.Config) server.Config {
|
||||||
toURL := "https://" + cfg.Host
|
toURL := "https://" + cfg.Host
|
||||||
if cfg.Port != "https" && cfg.Port != "http" {
|
if cfg.Port != "443" && cfg.Port != "80" {
|
||||||
toURL += ":" + cfg.Port
|
toURL += ":" + cfg.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,7 +418,7 @@ func redirPlaintextHost(cfg server.Config) server.Config {
|
||||||
|
|
||||||
return server.Config{
|
return server.Config{
|
||||||
Host: cfg.Host,
|
Host: cfg.Host,
|
||||||
Port: "http",
|
Port: "80",
|
||||||
Middleware: map[string][]middleware.Middleware{
|
Middleware: map[string][]middleware.Middleware{
|
||||||
"/": []middleware.Middleware{redirMidware},
|
"/": []middleware.Middleware{redirMidware},
|
||||||
},
|
},
|
||||||
|
@ -504,17 +473,17 @@ var (
|
||||||
|
|
||||||
// Some essential values related to the Let's Encrypt process
|
// Some essential values related to the Let's Encrypt process
|
||||||
const (
|
const (
|
||||||
// alternatePort is the port on which the acme client will open a
|
// AlternatePort is the port on which the acme client will open a
|
||||||
// listener and solve the CA's challenges. If this alternate port
|
// listener and solve the CA's challenges. If this alternate port
|
||||||
// is used instead of the default port (80 or 443), then the
|
// is used instead of the default port (80 or 443), then the
|
||||||
// default port for the challenge must be forwarded to this one.
|
// default port for the challenge must be forwarded to this one.
|
||||||
alternatePort = "5033"
|
AlternatePort = "5033"
|
||||||
|
|
||||||
// How often to check certificates for renewal.
|
// RenewInterval is how often to check certificates for renewal.
|
||||||
renewInterval = 24 * time.Hour
|
RenewInterval = 24 * time.Hour
|
||||||
|
|
||||||
// How often to update OCSP stapling.
|
// OCSPInterval is how often to check if OCSP stapling needs updating.
|
||||||
ocspInterval = 1 * time.Hour
|
OCSPInterval = 1 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeySize represents the length of a key in bits.
|
// KeySize represents the length of a key in bits.
|
||||||
|
@ -522,22 +491,22 @@ type KeySize int
|
||||||
|
|
||||||
// Key sizes are used to determine the strength of a key.
|
// Key sizes are used to determine the strength of a key.
|
||||||
const (
|
const (
|
||||||
ECC_224 KeySize = 224
|
Ecc224 KeySize = 224
|
||||||
ECC_256 = 256
|
Ecc256 = 256
|
||||||
RSA_2048 = 2048
|
Rsa2048 = 2048
|
||||||
RSA_4096 = 4096
|
Rsa4096 = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
// rsaKeySizeToUse is the size to use for new RSA keys.
|
// rsaKeySizeToUse is the size to use for new RSA keys.
|
||||||
// This shouldn't need to change except for in tests;
|
// This shouldn't need to change except for in tests;
|
||||||
// the size can be drastically reduced for speed.
|
// the size can be drastically reduced for speed.
|
||||||
var rsaKeySizeToUse = RSA_2048
|
var rsaKeySizeToUse = Rsa2048
|
||||||
|
|
||||||
// stopChan is used to signal the maintenance goroutine
|
// stopChan is used to signal the maintenance goroutine
|
||||||
// to terminate.
|
// to terminate.
|
||||||
var stopChan chan struct{}
|
var stopChan chan struct{}
|
||||||
|
|
||||||
// ocspStatus maps certificate bundle to OCSP status at start.
|
// ocspCache maps certificate bundle to OCSP response.
|
||||||
// It is used during regular OCSP checks to see if the OCSP
|
// It is used during regular OCSP checks to see if the OCSP
|
||||||
// status has changed.
|
// response needs to be updated.
|
||||||
var ocspStatus = make(map[*[]byte]int)
|
var ocspCache = make(map[*[]byte]*ocsp.Response)
|
||||||
|
|
|
@ -23,9 +23,11 @@ func TestHostQualifies(t *testing.T) {
|
||||||
{"", false},
|
{"", false},
|
||||||
{" ", false},
|
{" ", false},
|
||||||
{"0.0.0.0", false},
|
{"0.0.0.0", false},
|
||||||
{"192.168.1.3", true},
|
{"192.168.1.3", false},
|
||||||
{"10.0.2.1", true},
|
{"10.0.2.1", false},
|
||||||
|
{"169.112.53.4", false},
|
||||||
{"foobar.com", true},
|
{"foobar.com", true},
|
||||||
|
{"sub.foobar.com", true},
|
||||||
} {
|
} {
|
||||||
if HostQualifies(test.host) && !test.expect {
|
if HostQualifies(test.host) && !test.expect {
|
||||||
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
|
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
|
||||||
|
@ -39,14 +41,14 @@ func TestHostQualifies(t *testing.T) {
|
||||||
func TestRedirPlaintextHost(t *testing.T) {
|
func TestRedirPlaintextHost(t *testing.T) {
|
||||||
cfg := redirPlaintextHost(server.Config{
|
cfg := redirPlaintextHost(server.Config{
|
||||||
Host: "example.com",
|
Host: "example.com",
|
||||||
Port: "http",
|
Port: "80",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check host and port
|
// Check host and port
|
||||||
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
||||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||||
}
|
}
|
||||||
if actual, expected := cfg.Port, "http"; actual != expected {
|
if actual, expected := cfg.Port, "80"; actual != expected {
|
||||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,8 @@ var OnChange func() error
|
||||||
// which you'll close when maintenance should stop, to allow this
|
// which you'll close when maintenance should stop, to allow this
|
||||||
// goroutine to clean up after itself and unblock.
|
// goroutine to clean up after itself and unblock.
|
||||||
func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
||||||
renewalTicker := time.NewTicker(renewInterval)
|
renewalTicker := time.NewTicker(RenewInterval)
|
||||||
ocspTicker := time.NewTicker(ocspInterval)
|
ocspTicker := time.NewTicker(OCSPInterval)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -47,15 +47,25 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-ocspTicker.C:
|
case <-ocspTicker.C:
|
||||||
for bundle, oldStatus := range ocspStatus {
|
for bundle, oldResp := range ocspCache {
|
||||||
_, newStatus, err := acme.GetOCSPForCert(*bundle)
|
// start checking OCSP staple about halfway through validity period for good measure
|
||||||
if err == nil && newStatus != oldStatus && OnChange != nil {
|
refreshTime := oldResp.ThisUpdate.Add(oldResp.NextUpdate.Sub(oldResp.ThisUpdate) / 10)
|
||||||
log.Printf("[INFO] OCSP status changed from %v to %v", oldStatus, newStatus)
|
if time.Now().After(refreshTime) {
|
||||||
err := OnChange()
|
_, newResp, err := acme.GetOCSPForCert(*bundle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] OnChange after OCSP update: %v", err)
|
log.Printf("[ERROR] Checking OCSP for bundle: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if newResp.NextUpdate != oldResp.NextUpdate {
|
||||||
|
if OnChange != nil {
|
||||||
|
log.Printf("[INFO] Updating OCSP stapling to extend validity period to %v", newResp.NextUpdate)
|
||||||
|
err := OnChange()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] OnChange after OCSP trigger: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
|
@ -107,7 +117,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
|
||||||
log.Printf("[INFO] Certificate for %s has %d days remaining; attempting renewal", cfg.Host, daysLeft)
|
log.Printf("[INFO] Certificate for %s has %d days remaining; attempting renewal", cfg.Host, daysLeft)
|
||||||
var client *acme.Client
|
var client *acme.Client
|
||||||
if useCustomPort {
|
if useCustomPort {
|
||||||
client, err = newClientPort("", alternatePort) // email not used for renewal
|
client, err = newClientPort("", AlternatePort) // email not used for renewal
|
||||||
} else {
|
} else {
|
||||||
client, err = newClient("")
|
client, err = newClient("")
|
||||||
}
|
}
|
||||||
|
@ -134,7 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
|
||||||
|
|
||||||
// Renew certificate
|
// Renew certificate
|
||||||
Renew:
|
Renew:
|
||||||
newCertMeta, err := client.RenewCertificate(certMeta, true, true)
|
newCertMeta, err := client.RenewCertificate(certMeta, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(acme.TOSError); ok {
|
if _, ok := err.(acme.TOSError); ok {
|
||||||
err := client.AgreeToTOS()
|
err := client.AgreeToTOS()
|
||||||
|
@ -145,24 +155,20 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
newCertMeta, err = client.RenewCertificate(certMeta, true, true)
|
newCertMeta, err = client.RenewCertificate(certMeta, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
|
saveCertResource(newCertMeta)
|
||||||
n++
|
n++
|
||||||
} else if daysLeft <= 30 {
|
} else if daysLeft <= 21 {
|
||||||
// Warn on 30 days remaining. TODO: Just do this once...
|
// Warn on 21 days remaining. TODO: Just do this once...
|
||||||
log.Printf("[WARNING] Certificate for %s has %d days remaining; will automatically renew when 14 days remain\n", cfg.Host, daysLeft)
|
log.Printf("[WARNING] Certificate for %s has %d days remaining; will automatically renew when 14 days remain\n", cfg.Host, daysLeft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, errs
|
return n, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// acmeHandlers is a map of host to ACME handler. These
|
|
||||||
// are used to proxy ACME requests to the ACME client.
|
|
||||||
var acmeHandlers = make(map[string]*Handler)
|
|
||||||
|
|
|
@ -3,11 +3,17 @@
|
||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||||
|
"github.com/mholt/caddy/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -33,6 +39,12 @@ func Restart(newCaddyfile Input) error {
|
||||||
caddyfileMu.Unlock()
|
caddyfileMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get certificates for any new hosts in the new Caddyfile without causing downtime
|
||||||
|
err := getCertsForNewCaddyfile(newCaddyfile)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("TLS preload: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if len(os.Args) == 0 { // this should never happen, but...
|
if len(os.Args) == 0 { // this should never happen, but...
|
||||||
os.Args = []string{""}
|
os.Args = []string{""}
|
||||||
}
|
}
|
||||||
|
@ -61,7 +73,7 @@ func Restart(newCaddyfile Input) error {
|
||||||
|
|
||||||
// Pass along relevant file descriptors to child process; ordering
|
// Pass along relevant file descriptors to child process; ordering
|
||||||
// is very important since we rely on these being in certain positions.
|
// is very important since we rely on these being in certain positions.
|
||||||
extraFiles := []*os.File{sigwpipe}
|
extraFiles := []*os.File{sigwpipe} // fd 3
|
||||||
|
|
||||||
// Add file descriptors of all the sockets
|
// Add file descriptors of all the sockets
|
||||||
serversMu.Lock()
|
serversMu.Lock()
|
||||||
|
@ -110,3 +122,45 @@ func Restart(newCaddyfile Input) error {
|
||||||
// Looks like child is successful; we can exit gracefully.
|
// Looks like child is successful; we can exit gracefully.
|
||||||
return Stop()
|
return Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCertsForNewCaddyfile(newCaddyfile Input) error {
|
||||||
|
// parse the new caddyfile only up to (and including) TLS
|
||||||
|
// so we can know what we need to get certs for.
|
||||||
|
configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("loading Caddyfile: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Yuck, this is hacky. port 443 not set until letsencrypt is activated, so we change it here.
|
||||||
|
for i := range configs {
|
||||||
|
if configs[i].Port == "" && letsencrypt.ConfigQualifies(configs, i) {
|
||||||
|
configs[i].Port = "443"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only get certs for configs that bind to an address we're already listening on
|
||||||
|
groupings, err := arrangeBindings(configs)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("arranging bindings: " + err.Error())
|
||||||
|
}
|
||||||
|
var configsToSetup []server.Config
|
||||||
|
serversMu.Lock()
|
||||||
|
GroupLoop:
|
||||||
|
for _, group := range groupings {
|
||||||
|
for _, server := range servers {
|
||||||
|
if server.Addr == group.BindAddr.String() {
|
||||||
|
configsToSetup = append(configsToSetup, group.Configs...)
|
||||||
|
continue GroupLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serversMu.Unlock()
|
||||||
|
|
||||||
|
// obtain certs for eligible configs; letsencrypt pkg will filter out the rest.
|
||||||
|
configs, err = letsencrypt.ObtainCertsAndConfigure(configsToSetup, letsencrypt.AlternatePort)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("obtaining certs: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
2
main.go
2
main.go
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
"github.com/mholt/caddy/caddy"
|
"github.com/mholt/caddy/caddy"
|
||||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -53,6 +54,7 @@ func main() {
|
||||||
|
|
||||||
caddy.AppName = appName
|
caddy.AppName = appName
|
||||||
caddy.AppVersion = appVersion
|
caddy.AppVersion = appVersion
|
||||||
|
acme.UserAgent = appName + "/" + appVersion
|
||||||
|
|
||||||
// set up process log before anything bad happens
|
// set up process log before anything bad happens
|
||||||
switch logfile {
|
switch logfile {
|
||||||
|
|
|
@ -33,6 +33,7 @@ type Server struct {
|
||||||
httpWg sync.WaitGroup // used to wait on outstanding connections
|
httpWg sync.WaitGroup // used to wait on outstanding connections
|
||||||
startChan chan struct{} // used to block until server is finished starting
|
startChan chan struct{} // used to block until server is finished starting
|
||||||
connTimeout time.Duration // the maximum duration of a graceful shutdown
|
connTimeout time.Duration // the maximum duration of a graceful shutdown
|
||||||
|
ReqCallback OptionalCallback // if non-nil, is executed at the beginning of every request
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenerFile represents a listener.
|
// ListenerFile represents a listener.
|
||||||
|
@ -41,6 +42,11 @@ type ListenerFile interface {
|
||||||
File() (*os.File, error)
|
File() (*os.File, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OptionalCallback is a function that may or may not handle a request.
|
||||||
|
// It returns whether or not it handled the request. If it handled the
|
||||||
|
// request, it is presumed that no further request handling should occur.
|
||||||
|
type OptionalCallback func(http.ResponseWriter, *http.Request) bool
|
||||||
|
|
||||||
// New creates a new Server which will bind to addr and serve
|
// New creates a new Server which will bind to addr and serve
|
||||||
// the sites/hosts configured in configs. Its listener will
|
// the sites/hosts configured in configs. Its listener will
|
||||||
// gracefully close when the server is stopped which will take
|
// gracefully close when the server is stopped which will take
|
||||||
|
@ -309,6 +315,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
w.Header().Set("Server", "Caddy")
|
||||||
|
|
||||||
|
// Execute the optional request callback if it exists
|
||||||
|
if s.ReqCallback != nil && s.ReqCallback(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
host, _, err := net.SplitHostPort(r.Host)
|
host, _, err := net.SplitHostPort(r.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = r.Host // oh well
|
host = r.Host // oh well
|
||||||
|
@ -324,8 +337,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if vh, ok := s.vhosts[host]; ok {
|
if vh, ok := s.vhosts[host]; ok {
|
||||||
w.Header().Set("Server", "Caddy")
|
|
||||||
|
|
||||||
status, _ := vh.stack.ServeHTTP(w, r)
|
status, _ := vh.stack.ServeHTTP(w, r)
|
||||||
|
|
||||||
// Fallback error response in case error handling wasn't chained in
|
// Fallback error response in case error handling wasn't chained in
|
||||||
|
|
Loading…
Reference in a new issue