mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-02 13:45:57 +08:00
3c90e370a4
This commit goes a long way toward making automated documentation of Caddy config and Caddy modules possible. It's a broad, sweeping change, but mostly internal. It allows us to automatically generate docs for all Caddy modules (including future third-party ones) and make them viewable on a web page; it also doubles as godoc comments. As such, this commit makes significant progress in migrating the docs from our temporary wiki page toward our new website which is still under construction. With this change, all host modules will use ctx.LoadModule() and pass in both the struct pointer and the field name as a string. This allows the reflect package to read the struct tag from that field so that it can get the necessary information like the module namespace and the inline key. This has the nice side-effect of unifying the code and documentation. It also simplifies module loading, and handles several variations on field types for raw module fields (i.e. variations on json.RawMessage, such as arrays and maps). I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that the ID is the "full name" which includes both the module namespace and the name. This clarity is helpful when describing module hierarchy. As of this change, Caddy modules are no longer an experimental design. I think the architecture is good enough to go forward.
234 lines
6.8 KiB
Go
234 lines
6.8 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package distributedstek provides TLS session ticket ephemeral
|
|
// keys (STEKs) in a distributed fashion by utilizing configured
|
|
// storage for locking and key sharing. This allows a cluster of
|
|
// machines to optimally resume TLS sessions in a load-balanced
|
|
// environment without any hassle. This is similar to what
|
|
// Twitter does, but without needing to rely on SSH, as it is
|
|
// built into the web server this way:
|
|
// https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
|
|
package distributedstek
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
|
"github.com/mholt/certmagic"
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(Provider{})
|
|
}
|
|
|
|
// Provider implements a distributed STEK provider. This
|
|
// module will obtain STEKs from a storage module instead
|
|
// of generating STEKs internally. This allows STEKs to be
|
|
// coordinated, improving TLS session resumption in a cluster.
|
|
type Provider struct {
|
|
// The storage module wherein to store and obtain session
|
|
// ticket keys. If unset, Caddy's default/global-configured
|
|
// storage module will be used.
|
|
Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
|
|
|
storage certmagic.Storage
|
|
stekConfig *caddytls.SessionTicketService
|
|
timer *time.Timer
|
|
}
|
|
|
|
// CaddyModule returns the Caddy module information.
|
|
func (Provider) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "tls.stek.distributed",
|
|
New: func() caddy.Module { return new(Provider) },
|
|
}
|
|
}
|
|
|
|
// Provision provisions s.
|
|
func (s *Provider) Provision(ctx caddy.Context) error {
|
|
// unpack the storage module to use, if different from the default
|
|
if s.Storage != nil {
|
|
val, err := ctx.LoadModule(s, "Storage")
|
|
if err != nil {
|
|
return fmt.Errorf("loading TLS storage module: %s", err)
|
|
}
|
|
cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
|
|
if err != nil {
|
|
return fmt.Errorf("creating TLS storage configuration: %v", err)
|
|
}
|
|
s.storage = cmStorage
|
|
}
|
|
|
|
// otherwise, use default storage
|
|
if s.storage == nil {
|
|
s.storage = ctx.Storage()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Initialize sets the configuration for s and returns the starting keys.
|
|
func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
|
|
// keep a reference to the config; we'll need it when rotating keys
|
|
s.stekConfig = config
|
|
|
|
dstek, err := s.getSTEK()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create timer for the remaining time on the interval;
|
|
// this timer is cleaned up only when rotate() returns
|
|
s.timer = time.NewTimer(time.Until(dstek.NextRotation))
|
|
|
|
return dstek.Keys, nil
|
|
}
|
|
|
|
// Next returns a channel which transmits the latest session ticket keys.
|
|
func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
|
|
keysChan := make(chan [][32]byte)
|
|
go s.rotate(doneChan, keysChan)
|
|
return keysChan
|
|
}
|
|
|
|
func (s *Provider) loadSTEK() (distributedSTEK, error) {
|
|
var sg distributedSTEK
|
|
gobBytes, err := s.storage.Load(stekFileName)
|
|
if err != nil {
|
|
return sg, err // don't wrap, in case error is certmagic.ErrNotExist
|
|
}
|
|
dec := gob.NewDecoder(bytes.NewReader(gobBytes))
|
|
err = dec.Decode(&sg)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("STEK gob corrupted: %v", err)
|
|
}
|
|
return sg, nil
|
|
}
|
|
|
|
func (s *Provider) storeSTEK(dstek distributedSTEK) error {
|
|
var buf bytes.Buffer
|
|
err := gob.NewEncoder(&buf).Encode(dstek)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding STEK gob: %v", err)
|
|
}
|
|
err = s.storage.Store(stekFileName, buf.Bytes())
|
|
if err != nil {
|
|
return fmt.Errorf("storing STEK gob: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getSTEK locks and loads the current STEK from storage. If none
|
|
// currently exists, a new STEK is created and persisted. If the
|
|
// current STEK is outdated (NextRotation time is in the past),
|
|
// then it is rotated and persisted. The resulting STEK is returned.
|
|
func (s *Provider) getSTEK() (distributedSTEK, error) {
|
|
s.storage.Lock(stekLockName)
|
|
defer s.storage.Unlock(stekLockName)
|
|
|
|
// load the current STEKs from storage
|
|
dstek, err := s.loadSTEK()
|
|
if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
|
|
// if there is none, then make some right away
|
|
dstek, err = s.rotateKeys(dstek)
|
|
if err != nil {
|
|
return dstek, fmt.Errorf("creating new STEK: %v", err)
|
|
}
|
|
} else if err != nil {
|
|
// some other error, that's a problem
|
|
return dstek, fmt.Errorf("loading STEK: %v", err)
|
|
} else if time.Now().After(dstek.NextRotation) {
|
|
// if current STEKs are outdated, rotate them
|
|
dstek, err = s.rotateKeys(dstek)
|
|
if err != nil {
|
|
return dstek, fmt.Errorf("rotating keys: %v", err)
|
|
}
|
|
}
|
|
|
|
return dstek, nil
|
|
}
|
|
|
|
// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
|
|
// with updated keys and timestamps. It stores the returned STEK in storage,
|
|
// so this function must only be called in a storage-provided lock.
|
|
func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
|
|
var newSTEK distributedSTEK
|
|
var err error
|
|
|
|
newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
|
|
if err != nil {
|
|
return newSTEK, err
|
|
}
|
|
|
|
now := time.Now()
|
|
newSTEK.LastRotation = now
|
|
newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
|
|
|
|
err = s.storeSTEK(newSTEK)
|
|
if err != nil {
|
|
return newSTEK, err
|
|
}
|
|
|
|
return newSTEK, nil
|
|
}
|
|
|
|
// rotate rotates keys on a regular basis, sending each updated set of
|
|
// keys down keysChan, until doneChan is closed.
|
|
func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
|
|
for {
|
|
select {
|
|
case <-s.timer.C:
|
|
dstek, err := s.getSTEK()
|
|
if err != nil {
|
|
// TODO: improve this handling
|
|
log.Printf("[ERROR] Loading STEK: %v", err)
|
|
continue
|
|
}
|
|
|
|
// send the updated keys to the service
|
|
keysChan <- dstek.Keys
|
|
|
|
// timer channel is already drained, so reset directly (see godoc)
|
|
s.timer.Reset(time.Until(dstek.NextRotation))
|
|
|
|
case <-doneChan:
|
|
// again, see godocs for why timer is stopped this way
|
|
if !s.timer.Stop() {
|
|
<-s.timer.C
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type distributedSTEK struct {
|
|
Keys [][32]byte
|
|
LastRotation, NextRotation time.Time
|
|
}
|
|
|
|
const (
|
|
stekLockName = "stek_check"
|
|
stekFileName = "stek/stek.bin"
|
|
)
|
|
|
|
// Interface guard
|
|
var _ caddytls.STEKProvider = (*Provider)(nil)
|