mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-02 14:17:01 +01:00
Merge pull request #866 from mholt/0.9-wip
Merge 0.9 into master (warning: huge diff)
This commit is contained in:
commit
9b4134b287
238 changed files with 7166 additions and 6269 deletions
|
@ -1,4 +1,4 @@
|
|||
(Are you asking for help with Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!)
|
||||
(Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!)
|
||||
|
||||
#### 1. What version of Caddy are you running (`caddy -version`)?
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package assets
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
@ -6,10 +6,15 @@ import (
|
|||
"runtime"
|
||||
)
|
||||
|
||||
// Path returns the path to the folder
|
||||
// where the application may store data. This
|
||||
// currently resolves to ~/.caddy
|
||||
func Path() string {
|
||||
// AssetsPath returns the path to the folder
|
||||
// where the application may store data. If
|
||||
// CADDYPATH env variable is set, that value
|
||||
// is used. Otherwise, the path is the result
|
||||
// of evaluating "$HOME/.caddy".
|
||||
func AssetsPath() string {
|
||||
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
|
||||
return caddyPath
|
||||
}
|
||||
return filepath.Join(userHomeDir(), ".caddy")
|
||||
}
|
||||
|
19
assets_test.go
Normal file
19
assets_test.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAssetsPath(t *testing.T) {
|
||||
if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
|
||||
os.Setenv("CADDYPATH", "testpath")
|
||||
if actual, expected := AssetsPath(), "testpath"; actual != expected {
|
||||
t.Errorf("Expected path to be %v, got: %v", expected, actual)
|
||||
}
|
||||
os.Setenv("CADDYPATH", "")
|
||||
}
|
745
caddy.go
Normal file
745
caddy.go
Normal file
|
@ -0,0 +1,745 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// AppName is the name of the application.
|
||||
AppName string
|
||||
|
||||
// AppVersion is the version of the application.
|
||||
AppVersion string
|
||||
|
||||
// Quiet mode will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
|
||||
// PidFile is the path to the pidfile to create.
|
||||
PidFile string
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
|
||||
// isUpgrade will be set to true if this process
|
||||
// was started as part of an upgrade, where a parent
|
||||
// Caddy process started this one.
|
||||
isUpgrade bool
|
||||
)
|
||||
|
||||
// Instance contains the state of servers created as a result of
|
||||
// calling Start and can be used to access or control those servers.
|
||||
type Instance struct {
|
||||
// serverType is the name of the instance's server type
|
||||
serverType string
|
||||
|
||||
// caddyfileInput is the input configuration text used for this process
|
||||
caddyfileInput Input
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg sync.WaitGroup
|
||||
|
||||
// servers is the list of servers with their listeners...
|
||||
servers []serverListener
|
||||
|
||||
// these are callbacks to execute when certain events happen
|
||||
onStartup []func() error
|
||||
onRestart []func() error
|
||||
onShutdown []func() error
|
||||
}
|
||||
|
||||
// Stop stops all servers contained in i. It does NOT
|
||||
// execute shutdown callbacks.
|
||||
func (i *Instance) Stop() error {
|
||||
// stop the servers
|
||||
for _, s := range i.servers {
|
||||
if gs, ok := s.server.(GracefulServer); ok {
|
||||
if err := gs.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", gs.Address(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// splice instance list to delete this one
|
||||
for j, other := range instances {
|
||||
if other == i {
|
||||
instances = append(instances[:j], instances[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shutdownCallbacks executes all the shutdown callbacks of i.
|
||||
// An error returned from one does not stop execution of the rest.
|
||||
// All the errors will be returned.
|
||||
func (i *Instance) shutdownCallbacks() []error {
|
||||
var errs []error
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
err := shutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Restart replaces the servers in i with new servers created from
|
||||
// executing the newCaddyfile. Upon success, it returns the new
|
||||
// instance to replace i. Upon failure, i will not be replaced.
|
||||
func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
log.Println("[INFO] Reloading")
|
||||
|
||||
// run restart callbacks
|
||||
for _, fn := range i.onRestart {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
if newCaddyfile == nil {
|
||||
newCaddyfile = i.caddyfileInput
|
||||
}
|
||||
|
||||
// Add file descriptors of all the sockets that are capable of it
|
||||
restartFds := make(map[string]restartPair)
|
||||
for _, s := range i.servers {
|
||||
gs, srvOk := s.server.(GracefulServer)
|
||||
ln, lnOk := s.listener.(Listener)
|
||||
if srvOk && lnOk {
|
||||
restartFds[gs.Address()] = restartPair{server: gs, listener: ln}
|
||||
}
|
||||
}
|
||||
|
||||
// create new instance; if the restart fails, it is simply discarded
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType()}
|
||||
|
||||
// attempt to start new instance
|
||||
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
// success! bump the old instance out so it will be garbage-collected
|
||||
instancesMu.Lock()
|
||||
for j, other := range instances {
|
||||
if other == i {
|
||||
instances = append(instances[:j], instances[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
instancesMu.Unlock()
|
||||
|
||||
log.Println("[INFO] Reloading complete")
|
||||
|
||||
return newInst, nil
|
||||
}
|
||||
|
||||
// SaveServer adds s and its associated listener ln to the
|
||||
// internally-kept list of servers that is running. For
|
||||
// saved servers, graceful restarts will be provided.
|
||||
func (i *Instance) SaveServer(s Server, ln net.Listener) {
|
||||
i.servers = append(i.servers, serverListener{server: s, listener: ln})
|
||||
}
|
||||
|
||||
// HasListenerWithAddress returns whether this package is
|
||||
// tracking a server using a listener with the address
|
||||
// addr.
|
||||
func HasListenerWithAddress(addr string) bool {
|
||||
instancesMu.Lock()
|
||||
defer instancesMu.Unlock()
|
||||
for _, inst := range instances {
|
||||
for _, sln := range inst.servers {
|
||||
if listenerAddrEqual(sln.listener, addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listenerAddrEqual compares a listener's address with
|
||||
// addr. Extra care is taken to match addresses with an
|
||||
// empty hostname portion, as listeners tend to report
|
||||
// [::]:80, for example, when the matching address that
|
||||
// created the listener might be simply :80.
|
||||
func listenerAddrEqual(ln net.Listener, addr string) bool {
|
||||
lnAddr := ln.Addr().String()
|
||||
hostname, port, err := net.SplitHostPort(addr)
|
||||
if err != nil || hostname != "" {
|
||||
return lnAddr == addr
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("::", port) {
|
||||
return true
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: We should be able to support UDP servers... I'm considering this pattern.
|
||||
|
||||
type UDPListener struct {
|
||||
*net.UDPConn
|
||||
}
|
||||
|
||||
func (u UDPListener) Accept() (net.Conn, error) {
|
||||
return u.UDPConn, nil
|
||||
}
|
||||
|
||||
func (u UDPListener) Close() error {
|
||||
return u.UDPConn.Close()
|
||||
}
|
||||
|
||||
func (u UDPListener) Addr() net.Addr {
|
||||
return u.UDPConn.LocalAddr()
|
||||
}
|
||||
|
||||
var _ net.Listener = UDPListener{}
|
||||
*/
|
||||
|
||||
// Server is a type that can listen and serve. A Server
|
||||
// must associate with exactly zero or one listeners.
|
||||
type Server interface {
|
||||
// Listen starts listening by creating a new listener
|
||||
// and returning it. It does not start accepting
|
||||
// connections.
|
||||
Listen() (net.Listener, error)
|
||||
|
||||
// Serve starts serving using the provided listener.
|
||||
// Serve must start the server loop nearly immediately,
|
||||
// or at least not return any errors before the server
|
||||
// loop begins. Serve blocks indefinitely, or in other
|
||||
// words, until the server is stopped.
|
||||
Serve(net.Listener) error
|
||||
}
|
||||
|
||||
// Stopper is a type that can stop serving. The stop
|
||||
// does not necessarily have to be graceful.
|
||||
type Stopper interface {
|
||||
// Stop stops the server. It blocks until the
|
||||
// server is completely stopped.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// GracefulServer is a Server and Stopper, the stopping
|
||||
// of which is graceful (whatever that means for the kind
|
||||
// of server being implemented). It must be able to return
|
||||
// the address it is configured to listen on so that its
|
||||
// listener can be paired with it upon graceful restarts.
|
||||
// The net.Listener that a GracefulServer creates must
|
||||
// implement the Listener interface for restarts to be
|
||||
// graceful (assuming the listener is for TCP).
|
||||
type GracefulServer interface {
|
||||
Server
|
||||
Stopper
|
||||
|
||||
// Address returns the address the server should
|
||||
// listen on; it is used to pair the server to
|
||||
// its listener during a graceful/zero-downtime
|
||||
// restart. Thus when implementing this method,
|
||||
// you must not access a listener to get the
|
||||
// address; you must store the address the
|
||||
// server is to serve on some other way.
|
||||
Address() string
|
||||
}
|
||||
|
||||
// Listener is a net.Listener with an underlying file descriptor.
|
||||
// A server's listener should implement this interface if it is
|
||||
// to support zero-downtime reloads.
|
||||
type Listener interface {
|
||||
net.Listener
|
||||
File() (*os.File, error)
|
||||
}
|
||||
|
||||
// AfterStartup is an interface that can be implemented
|
||||
// by a server type that wants to run some code after all
|
||||
// servers for the same Instance have started.
|
||||
type AfterStartup interface {
|
||||
OnStartupComplete()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile by calling the plugged in
|
||||
// Caddyfile loader methods. An error is returned if more than
|
||||
// one loader returns a non-nil Caddyfile input. If no loaders
|
||||
// load a Caddyfile, the default loader is used. If no default
|
||||
// loader is registered or it returns nil, the server type's
|
||||
// default Caddyfile is loaded. If the server type does not
|
||||
// specify any default Caddyfile value, then an empty Caddyfile
|
||||
// is returned. Consequently, this function never returns a nil
|
||||
// value as long as there are no errors.
|
||||
func LoadCaddyfile(serverType string) (Input, error) {
|
||||
// Ask plugged-in loaders for a Caddyfile
|
||||
cdyfile, err := loadCaddyfileInput(serverType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput(serverType)
|
||||
}
|
||||
|
||||
// Still nil? Geez.
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{ServerTypeName: serverType}
|
||||
}
|
||||
|
||||
return cdyfile, nil
|
||||
}
|
||||
|
||||
// Wait blocks until all of i's servers have stopped.
|
||||
func (i *Instance) Wait() {
|
||||
i.wg.Wait()
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// NOTE: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the Caddyfile used to create i.
|
||||
func (i *Instance) Caddyfile() Input {
|
||||
return i.caddyfileInput
|
||||
}
|
||||
|
||||
// Start starts Caddy with the given Caddyfile.
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
func Start(cdyfile Input) (*Instance, error) {
|
||||
writePidFile()
|
||||
inst := &Instance{serverType: cdyfile.ServerType()}
|
||||
return inst, startWithListenerFds(cdyfile, inst, nil)
|
||||
}
|
||||
|
||||
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartPair) error {
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{}
|
||||
}
|
||||
|
||||
stypeName := cdyfile.ServerType()
|
||||
|
||||
stype, err := getServerType(stypeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inst.caddyfileInput = cdyfile
|
||||
|
||||
sblocks, err := loadServerBlocks(stypeName, path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := stype.NewContext()
|
||||
|
||||
sblocks, err = ctx.InspectServerBlocks(cdyfile.Path(), sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = executeDirectives(inst, cdyfile.Path(), stype.Directives, sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slist, err := ctx.MakeServers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if restartFds == nil {
|
||||
// run startup callbacks since this is not a restart
|
||||
for _, startupFunc := range inst.onStartup {
|
||||
err := startupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = startServers(slist, inst, restartFds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instancesMu.Lock()
|
||||
instances = append(instances, inst)
|
||||
instancesMu.Unlock()
|
||||
|
||||
// run any AfterStartup callbacks if this is not
|
||||
// part of a restart; then show file descriptor notice
|
||||
if restartFds == nil {
|
||||
for _, srvln := range inst.servers {
|
||||
if srv, ok := srvln.server.(AfterStartup); ok {
|
||||
srv.OnStartupComplete()
|
||||
}
|
||||
}
|
||||
if !Quiet {
|
||||
for _, srvln := range inst.servers {
|
||||
if !IsLoopback(srvln.listener.Addr().String()) {
|
||||
checkFdlimit()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDirectives(inst *Instance, filename string,
|
||||
directives []string, sblocks []caddyfile.ServerBlock) error {
|
||||
|
||||
// map of server block ID to map of directive name to whatever.
|
||||
storages := make(map[int]map[string]interface{})
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
// We loop with the directives on the outer loop so we execute
|
||||
// a directive for all server blocks before going to the next directive.
|
||||
// This is important mainly due to the parsing callbacks (below).
|
||||
for _, dir := range directives {
|
||||
for i, sb := range sblocks {
|
||||
var once sync.Once
|
||||
if _, ok := storages[i]; !ok {
|
||||
storages[i] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for j, key := range sb.Keys {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir]; ok {
|
||||
controller := &Controller{
|
||||
instance: inst,
|
||||
Key: key,
|
||||
Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockKeyIndex: j,
|
||||
ServerBlockKeys: sb.Keys,
|
||||
ServerBlockStorage: storages[i][dir],
|
||||
}
|
||||
|
||||
setup, err := DirectiveAction(inst.serverType, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setup(controller)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storages[i][dir] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See if there are any callbacks to execute after this directive
|
||||
if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok {
|
||||
callbacks := allCallbacks[dir]
|
||||
for _, callback := range callbacks {
|
||||
if err := callback(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartPair) error {
|
||||
errChan := make(chan error, len(serverList))
|
||||
|
||||
for _, s := range serverList {
|
||||
var ln net.Listener
|
||||
var err error
|
||||
|
||||
// If this is a reload and s is a GracefulServer,
|
||||
// reuse the listener for a graceful restart.
|
||||
if gs, ok := s.(GracefulServer); ok && restartFds != nil {
|
||||
addr := gs.Address()
|
||||
if old, ok := restartFds[addr]; ok {
|
||||
file, err := old.listener.File()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
delete(restartFds, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if ln == nil {
|
||||
ln, err = s.Listen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
inst.wg.Add(1)
|
||||
go func(s Server, ln net.Listener, inst *Instance) {
|
||||
defer inst.wg.Done()
|
||||
errChan <- s.Serve(ln)
|
||||
}(s, ln, inst)
|
||||
|
||||
inst.servers = append(inst.servers, serverListener{server: s, listener: ln})
|
||||
}
|
||||
|
||||
// Close the remaining (unused) file descriptors to free up resources
|
||||
// and stop old servers that aren't used anymore
|
||||
for key, old := range restartFds {
|
||||
if err := old.server.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", old.server.Address(), err)
|
||||
}
|
||||
delete(restartFds, key)
|
||||
}
|
||||
|
||||
// Log errors that may be returned from Serve() calls,
|
||||
// these errors should only be occurring in the server loop.
|
||||
go func() {
|
||||
for err := range errChan {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener
|
||||
continue
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServerType(serverType string) (ServerType, error) {
|
||||
stype, ok := serverTypes[serverType]
|
||||
if ok {
|
||||
return stype, nil
|
||||
}
|
||||
if len(serverTypes) == 0 {
|
||||
return ServerType{}, fmt.Errorf("no server types plugged in")
|
||||
}
|
||||
if serverType == "" {
|
||||
if len(serverTypes) == 1 {
|
||||
for _, stype := range serverTypes {
|
||||
return stype, nil
|
||||
}
|
||||
}
|
||||
return ServerType{}, fmt.Errorf("multiple server types available; must choose one")
|
||||
}
|
||||
return ServerType{}, fmt.Errorf("unknown server type '%s'", serverType)
|
||||
}
|
||||
|
||||
func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) {
|
||||
validDirectives := ValidDirectives(serverType)
|
||||
serverBlocks, err := caddyfile.ServerBlocks(filename, input, validDirectives)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(serverBlocks) == 0 && serverTypes[serverType].DefaultInput != nil {
|
||||
newInput := serverTypes[serverType].DefaultInput()
|
||||
serverBlocks, err = caddyfile.ServerBlocks(newInput.Path(),
|
||||
bytes.NewReader(newInput.Body()), validDirectives)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return serverBlocks, nil
|
||||
}
|
||||
|
||||
// Stop stops ALL servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks, and it deletes all
|
||||
// instances after stopping is completed. Do not re-use any
|
||||
// references to old instances after calling Stop.
|
||||
func Stop() error {
|
||||
instancesMu.Lock()
|
||||
for _, inst := range instances {
|
||||
if err := inst.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
|
||||
}
|
||||
}
|
||||
instances = []*Instance{}
|
||||
instancesMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsLoopback returns true if the hostname of addr looks
|
||||
// explicitly like a common local hostname. addr must only
|
||||
// be a host or a host:port combination.
|
||||
func IsLoopback(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname
|
||||
}
|
||||
return host == "localhost" ||
|
||||
strings.Trim(host, "[]") == "::1" ||
|
||||
strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// checkFdlimit issues a warning if the OS limit for
|
||||
// max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 8192
|
||||
|
||||
// Warn if ulimit is too low for production sites
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
|
||||
if err == nil {
|
||||
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
|
||||
if err == nil && lim < min {
|
||||
fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+
|
||||
"At least %d is recommended. Fix with \"ulimit -n %d\".\n", lim, min, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade re-launches the process, preserving the listeners
|
||||
// for a graceful restart. It does NOT load new configuration;
|
||||
// it only starts the process anew with a fresh binary.
|
||||
//
|
||||
// TODO: This is not yet implemented
|
||||
func Upgrade() error {
|
||||
return fmt.Errorf("not implemented")
|
||||
// TODO: have child process set isUpgrade = true
|
||||
}
|
||||
|
||||
// IsUpgrade returns true if this process is part of an upgrade
|
||||
// where a parent caddy process spawned this one to ugprade
|
||||
// the binary.
|
||||
func IsUpgrade() bool {
|
||||
return isUpgrade
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
ServerTypeName string
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// ServerType returns c.ServerType.
|
||||
func (c CaddyfileInput) ServerType() string { return c.ServerTypeName }
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// The type of server this input is intended for
|
||||
ServerType() string
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
// It uses the default host and port (depends on
|
||||
// host, e.g. localhost is 2015, otherwise 443) and
|
||||
// root.
|
||||
func DefaultInput(serverType string) Input {
|
||||
if _, ok := serverTypes[serverType]; !ok {
|
||||
return nil
|
||||
}
|
||||
if serverTypes[serverType].DefaultInput == nil {
|
||||
return nil
|
||||
}
|
||||
return serverTypes[serverType].DefaultInput()
|
||||
}
|
||||
|
||||
// writePidFile writes the process ID to the file at PidFile.
|
||||
// It does nothing if PidFile is not set.
|
||||
func writePidFile() error {
|
||||
if PidFile == "" {
|
||||
return nil
|
||||
}
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
return ioutil.WriteFile(PidFile, pid, 0644)
|
||||
}
|
||||
|
||||
type restartPair struct {
|
||||
server GracefulServer
|
||||
listener Listener
|
||||
}
|
||||
|
||||
var (
|
||||
// instances is the list of running Instances.
|
||||
instances []*Instance
|
||||
|
||||
// instancesMu protects instances.
|
||||
instancesMu sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -7,19 +7,18 @@
|
|||
# $ ./build.bash [output_filename] [git_repo]
|
||||
#
|
||||
# Outputs compiled program in current directory.
|
||||
# Default file name is 'ecaddy'.
|
||||
# Default git repo is current directory.
|
||||
# Builds always take place from current directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: ${output_filename:="${1:-}"}
|
||||
: ${output_filename:="ecaddy"}
|
||||
: ${output_filename:="caddy"}
|
||||
|
||||
: ${git_repo:="${2:-}"}
|
||||
: ${git_repo:="."}
|
||||
|
||||
pkg=main
|
||||
pkg=github.com/mholt/caddy/caddy/caddymain
|
||||
ldflags=()
|
||||
|
||||
# Timestamp of build
|
346
caddy/caddy.go
346
caddy/caddy.go
|
@ -1,346 +0,0 @@
|
|||
// Package caddy implements the Caddy web server as a service
|
||||
// in your own Go programs.
|
||||
//
|
||||
// To use this package, follow a few simple steps:
|
||||
//
|
||||
// 1. Set the AppName and AppVersion variables.
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile.
|
||||
// You should pass in your own Caddyfile loader.
|
||||
// 3. Call caddy.Start() to start Caddy, caddy.Stop()
|
||||
// to stop it, or caddy.Restart() to restart it.
|
||||
//
|
||||
// You should use caddy.Wait() to wait for all Caddy servers
|
||||
// to quit before your process exits.
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// AppName is the name of the application.
|
||||
AppName string
|
||||
|
||||
// AppVersion is the version of the application.
|
||||
AppVersion string
|
||||
|
||||
// Quiet when set to true, will not show any informative output on initialization.
|
||||
Quiet bool
|
||||
|
||||
// HTTP2 indicates whether HTTP2 is enabled or not.
|
||||
HTTP2 bool
|
||||
|
||||
// PidFile is the path to the pidfile to create.
|
||||
PidFile string
|
||||
|
||||
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
||||
GracefulTimeout time.Duration
|
||||
)
|
||||
|
||||
var (
|
||||
// caddyfile is the input configuration text used for this process
|
||||
caddyfile Input
|
||||
|
||||
// caddyfileMu protects caddyfile during changes
|
||||
caddyfileMu sync.Mutex
|
||||
|
||||
// servers is a list of all the currently-listening servers
|
||||
servers []*server.Server
|
||||
|
||||
// serversMu protects the servers slice during changes
|
||||
serversMu sync.Mutex
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg sync.WaitGroup
|
||||
|
||||
// restartFds keeps the servers' sockets for graceful in-process restart
|
||||
restartFds = make(map[string]*os.File)
|
||||
|
||||
// startedBefore should be set to true if caddy has been started
|
||||
// at least once (does not indicate whether currently running).
|
||||
startedBefore bool
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHost is the default host.
|
||||
DefaultHost = ""
|
||||
// DefaultPort is the default port.
|
||||
DefaultPort = "2015"
|
||||
// DefaultRoot is the default root folder.
|
||||
DefaultRoot = "."
|
||||
)
|
||||
|
||||
// Start starts Caddy with the given Caddyfile. If cdyfile
|
||||
// is nil, the LoadCaddyfile function will be called to get
|
||||
// one.
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
func Start(cdyfile Input) (err error) {
|
||||
// Input must never be nil; try to load something
|
||||
if cdyfile == nil {
|
||||
cdyfile, err = LoadCaddyfile(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
caddyfileMu.Lock()
|
||||
caddyfile = cdyfile
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
// load the server configs (activates Let's Encrypt)
|
||||
configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// group virtualhosts by address
|
||||
groupings, err := arrangeBindings(configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start each server with its one or more configurations
|
||||
err = startServers(groupings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
showInitializationOutput(groupings)
|
||||
|
||||
startedBefore = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showInitializationOutput just outputs some basic information about
|
||||
// what is being served to stdout, as well as any applicable, non-essential
|
||||
// warnings for the user.
|
||||
func showInitializationOutput(groupings bindingGroup) {
|
||||
// Show initialization output
|
||||
if !Quiet && !IsRestart() {
|
||||
var checkedFdLimit bool
|
||||
for _, group := range groupings {
|
||||
for _, conf := range group.Configs {
|
||||
// Print address of site
|
||||
fmt.Println(conf.Address())
|
||||
|
||||
// Note if non-localhost site resolves to loopback interface
|
||||
if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
|
||||
conf.Host, group.BindAddr.IP.String())
|
||||
}
|
||||
if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
checkFdlimit()
|
||||
checkedFdLimit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startServers starts all the servers in groupings,
|
||||
// taking into account whether or not this process is
|
||||
// from a graceful restart or not. It blocks until
|
||||
// the servers are listening.
|
||||
func startServers(groupings bindingGroup) error {
|
||||
var startupWg sync.WaitGroup
|
||||
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
|
||||
|
||||
for _, group := range groupings {
|
||||
s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.HTTP2 = HTTP2
|
||||
s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running
|
||||
if s.OnDemandTLS {
|
||||
s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome!
|
||||
} else {
|
||||
s.TLSConfig.GetCertificate = https.GetCertificate
|
||||
}
|
||||
|
||||
var ln server.ListenerFile
|
||||
if len(restartFds) > 0 {
|
||||
// Reuse the listeners for in-process restart
|
||||
if file, ok := restartFds[s.Addr]; ok {
|
||||
fln, err := net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ln, ok = fln.(server.ListenerFile)
|
||||
if !ok {
|
||||
return errors.New("listener for " + s.Addr + " was not a ListenerFile")
|
||||
}
|
||||
|
||||
file.Close()
|
||||
delete(restartFds, s.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(s *server.Server, ln server.ListenerFile) {
|
||||
defer wg.Done()
|
||||
|
||||
// run startup functions that should only execute when
|
||||
// the original parent process is starting.
|
||||
if !startedBefore {
|
||||
err := s.RunFirstStartupFuncs()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// start the server
|
||||
if ln != nil {
|
||||
errChan <- s.Serve(ln)
|
||||
} else {
|
||||
errChan <- s.ListenAndServe()
|
||||
}
|
||||
}(s, ln)
|
||||
|
||||
startupWg.Add(1)
|
||||
go func(s *server.Server) {
|
||||
defer startupWg.Done()
|
||||
s.WaitUntilStarted()
|
||||
}(s)
|
||||
|
||||
serversMu.Lock()
|
||||
servers = append(servers, s)
|
||||
serversMu.Unlock()
|
||||
}
|
||||
|
||||
// Close the remaining (unused) file descriptors to free up resources
|
||||
if len(restartFds) > 0 {
|
||||
for key, file := range restartFds {
|
||||
file.Close()
|
||||
delete(restartFds, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all servers to finish starting
|
||||
startupWg.Wait()
|
||||
|
||||
// Return the first error, if any
|
||||
select {
|
||||
case err := <-errChan:
|
||||
// "use of closed network connection" is normal if it was a graceful shutdown
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops all servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks that may have been
|
||||
// configured by middleware (they must be executed separately).
|
||||
func Stop() error {
|
||||
https.Deactivate()
|
||||
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
if err := s.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", s.Addr, err)
|
||||
}
|
||||
}
|
||||
servers = []*server.Server{} // don't reuse servers
|
||||
serversMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks until all servers are stopped.
|
||||
func Wait() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile by calling the user's loader function,
|
||||
// and if that returns nil, then this function resorts to the default
|
||||
// configuration. Thus, if there are no other errors, this function
|
||||
// always returns at least the default Caddyfile.
|
||||
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
|
||||
// Try user's loader
|
||||
if cdyfile == nil && loader != nil {
|
||||
cdyfile, err = loader()
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the current Caddyfile
|
||||
func Caddyfile() Input {
|
||||
caddyfileMu.Lock()
|
||||
defer caddyfileMu.Unlock()
|
||||
return caddyfile
|
||||
}
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// IsFile returns true if the original input was a file on the file system
|
||||
// that could be loaded again later if requested.
|
||||
IsFile() bool
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCaddyStartStop(t *testing.T) {
|
||||
caddyfile := "localhost:1984"
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting, iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(2 * time.Second),
|
||||
}
|
||||
resp, err := client.Get("http://localhost:1984")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Error stopping, iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -7,46 +7,55 @@ import (
|
|||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddy"
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
// plug in the HTTP server type
|
||||
_ "github.com/mholt/caddy/caddyhttp"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
// This is where other plugins get plugged in (imported)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.TrapSignals()
|
||||
setVersion()
|
||||
flag.BoolVar(&https.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
|
||||
flag.StringVar(&https.CAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "Certificate authority ACME server")
|
||||
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
|
||||
|
||||
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
// TODO: Change from staging to v01
|
||||
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-staging.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
|
||||
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.StringVar(&https.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
|
||||
flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown")
|
||||
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
|
||||
flag.BoolVar(&caddy.HTTP2, "http2", true, "Use HTTP/2")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
|
||||
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
|
||||
flag.StringVar(&serverType, "type", "http", "Type of server to run")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&directives, "directives", false, "List supported directives")
|
||||
|
||||
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
|
||||
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse() // called here in main() to allow other packages to set flags in their inits
|
||||
// Run is Caddy's main() function.
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
moveStorage() // TODO: This is temporary for the 0.9 release, or until most users upgrade to 0.9+
|
||||
|
||||
caddy.AppName = appName
|
||||
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 {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
|
@ -63,8 +72,9 @@ func main() {
|
|||
})
|
||||
}
|
||||
|
||||
// Check for one-time actions
|
||||
if revoke != "" {
|
||||
err := https.Revoke(revoke)
|
||||
err := caddytls.Revoke(revoke)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -78,10 +88,8 @@ func main() {
|
|||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
if directives {
|
||||
for _, d := range caddy.Directives() {
|
||||
fmt.Println(d)
|
||||
}
|
||||
if plugins {
|
||||
fmt.Println(caddy.DescribePlugins())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
|
@ -92,77 +100,124 @@ func main() {
|
|||
}
|
||||
|
||||
// Get Caddyfile input
|
||||
caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile)
|
||||
caddyfile, err := caddy.LoadCaddyfile(serverType)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
err = caddy.Start(caddyfile)
|
||||
instance, err := caddy.Start(caddyfile)
|
||||
if err != nil {
|
||||
mustLogFatal(err)
|
||||
}
|
||||
|
||||
// Twiddle your thumbs
|
||||
caddy.Wait()
|
||||
instance.Wait()
|
||||
}
|
||||
|
||||
// mustLogFatal just wraps log.Fatal() in a way that ensures the
|
||||
// mustLogFatal wraps log.Fatal() in a way that ensures the
|
||||
// output is always printed to stderr so the user can see it
|
||||
// if the user is still there, even if the process log was not
|
||||
// enabled. If this process is a restart, however, and the user
|
||||
// might not be there anymore, this just logs to the process log
|
||||
// and exits.
|
||||
// enabled. If this process is an upgrade, however, and the user
|
||||
// might not be there anymore, this just logs to the process
|
||||
// log and exits.
|
||||
func mustLogFatal(args ...interface{}) {
|
||||
if !caddy.IsRestart() {
|
||||
if !caddy.IsUpgrade() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
log.Fatal(args...)
|
||||
}
|
||||
|
||||
func loadCaddyfile() (caddy.Input, error) {
|
||||
// Try -conf flag
|
||||
if conf != "" {
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
RealFile: true,
|
||||
}, nil
|
||||
// confLoader loads the Caddyfile using the -conf flag.
|
||||
func confLoader(serverType string) (caddy.Input, error) {
|
||||
if conf == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// command line args
|
||||
if flag.NArg() > 0 {
|
||||
confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n")
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(confBody),
|
||||
Filepath: "args",
|
||||
}, nil
|
||||
if conf == "stdin" {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin)
|
||||
}
|
||||
|
||||
// Caddyfile in cwd
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// defaultLoader loads the Caddyfile from the current working directory.
|
||||
func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return caddy.DefaultInput(), nil
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
RealFile: true,
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
ServerTypeName: serverType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// moveStorage moves the old certificate storage location by
|
||||
// renaming the "letsencrypt" folder to the hostname of the
|
||||
// CA URL. This is TEMPORARY until most users have upgraded to 0.9+.
|
||||
func moveStorage() {
|
||||
oldPath := filepath.Join(caddy.AssetsPath(), "letsencrypt")
|
||||
_, err := os.Stat(oldPath)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
newPath, err := caddytls.StorageFor(caddytls.DefaultCAUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to get new path for certificate storage: %v", err)
|
||||
}
|
||||
err = os.MkdirAll(string(newPath), 0700)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to make new certificate storage path: %v", err)
|
||||
}
|
||||
err = os.Rename(oldPath, string(newPath))
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to migrate certificate storage: %v", err)
|
||||
}
|
||||
// convert mixed case folder and file names to lowercase
|
||||
filepath.Walk(string(newPath), func(path string, info os.FileInfo, err error) error {
|
||||
// must be careful to only lowercase the base of the path, not the whole thing!!
|
||||
base := filepath.Base(path)
|
||||
if lowerBase := strings.ToLower(base); base != lowerBase {
|
||||
lowerPath := filepath.Join(filepath.Dir(path), lowerBase)
|
||||
err = os.Rename(path, lowerPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Unable to lower-case: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// setVersion figures out the version information
|
||||
// based on variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
|
@ -198,33 +253,17 @@ func setCPU(cpu string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// setVersion figures out the version information based on
|
||||
// variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s %s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appName = "Caddy"
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
logfile string
|
||||
revoke string
|
||||
version bool
|
||||
directives bool
|
||||
plugins bool
|
||||
)
|
||||
|
||||
// Build information obtained with the help of -ldflags
|
348
caddy/config.go
348
caddy/config.go
|
@ -1,348 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
||||
|
||||
// loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them,
|
||||
// the parsed server blocks, the index of the last directive it processed, and an error (if any).
|
||||
func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) {
|
||||
var configs []server.Config
|
||||
|
||||
// Each server block represents similar hosts/addresses, since they
|
||||
// were grouped together in the Caddyfile.
|
||||
serverBlocks, err := parse.ServerBlocks(filename, input, true)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
if len(serverBlocks) == 0 {
|
||||
newInput := DefaultInput()
|
||||
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off
|
||||
|
||||
// Iterate each server block and make a config for each one,
|
||||
// executing the directives that were parsed in order up to the tls
|
||||
// directive; this is because we must activate Let's Encrypt.
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j, addr := range sb.Addresses {
|
||||
config := server.Config{
|
||||
Host: addr.Host,
|
||||
Port: addr.Port,
|
||||
Scheme: addr.Scheme,
|
||||
Root: Root,
|
||||
ConfigFile: filename,
|
||||
AppName: AppName,
|
||||
AppVersion: AppVersion,
|
||||
}
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
for k, dir := range directiveOrder {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
// Each setup function gets a controller, from which setup functions
|
||||
// get access to the config, tokens, and other state information useful
|
||||
// to set up its own host only.
|
||||
controller := &setup.Controller{
|
||||
Config: &config,
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
// execute setup function and append middleware handler, if any
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, nil, lastDirectiveIndex, err
|
||||
}
|
||||
if midware != nil {
|
||||
config.Middleware = append(config.Middleware, midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
|
||||
// Stop after TLS setup, since we need to activate Let's Encrypt before continuing;
|
||||
// it makes some changes to the configs that middlewares might want to know about.
|
||||
if dir.name == "tls" {
|
||||
lastDirectiveIndex = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
}
|
||||
|
||||
return configs, serverBlocks, lastDirectiveIndex, nil
|
||||
}
|
||||
|
||||
// loadConfigs reads input (named filename) and parses it, returning the
|
||||
// server configurations in the order they appeared in the input. As part
|
||||
// of this, it activates Let's Encrypt for the configs that are produced.
|
||||
// Thus, the returned configs are already optimally configured for HTTPS.
|
||||
func loadConfigs(filename string, input io.Reader) ([]server.Config, error) {
|
||||
configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we have all the configs, but they have only been set up to the
|
||||
// point of tls. We need to activate Let's Encrypt before setting up
|
||||
// the rest of the middlewares so they have correct information regarding
|
||||
// TLS configuration, if necessary. (this only appends, so our iterations
|
||||
// over server blocks below shouldn't be affected)
|
||||
if !IsRestart() && !Quiet {
|
||||
fmt.Print("Activating privacy features...")
|
||||
}
|
||||
configs, err = https.Activate(configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !IsRestart() && !Quiet {
|
||||
fmt.Println(" done.")
|
||||
}
|
||||
|
||||
// Finish setting up the rest of the directives, now that TLS is
|
||||
// optimally configured. These loops are similar to above except
|
||||
// we don't iterate all the directives from the beginning and we
|
||||
// don't create new configs.
|
||||
configIndex := -1
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j := range sb.Addresses {
|
||||
configIndex++
|
||||
|
||||
for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ {
|
||||
dir := directiveOrder[k]
|
||||
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
controller := &setup.Controller{
|
||||
Config: &configs[configIndex],
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if midware != nil {
|
||||
configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// makeOnces makes a map of directive name to sync.Once
|
||||
// instance. This is intended to be called once per server
|
||||
// block when setting up configs so that Setup functions
|
||||
// for each directive can perform a task just once per
|
||||
// server block, even if there are multiple hosts on the block.
|
||||
//
|
||||
// We need one Once per directive, otherwise the first
|
||||
// directive to use it would exclude other directives from
|
||||
// using it at all, which would be a bug.
|
||||
func makeOnces() map[string]*sync.Once {
|
||||
onces := make(map[string]*sync.Once)
|
||||
for _, dir := range directiveOrder {
|
||||
onces[dir.name] = new(sync.Once)
|
||||
}
|
||||
return onces
|
||||
}
|
||||
|
||||
// makeStorages makes a map of directive name to interface{}
|
||||
// so that directives' setup functions can persist state
|
||||
// between different hosts on the same server block during the
|
||||
// setup phase.
|
||||
func makeStorages() map[string]interface{} {
|
||||
storages := make(map[string]interface{})
|
||||
for _, dir := range directiveOrder {
|
||||
storages[dir.name] = nil
|
||||
}
|
||||
return storages
|
||||
}
|
||||
|
||||
// arrangeBindings groups configurations by their bind address. For example,
|
||||
// a server that should listen on localhost and another on 127.0.0.1 will
|
||||
// be grouped into the same address: 127.0.0.1. It will return an error
|
||||
// if an address is malformed or a TLS listener is configured on the
|
||||
// same address as a plaintext HTTP listener. The return value is a map of
|
||||
// bind address to list of configs that would become VirtualHosts on that
|
||||
// server. Use the keys of the returned map to create listeners, and use
|
||||
// the associated values to set up the virtualhosts.
|
||||
func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) {
|
||||
var groupings bindingGroup
|
||||
|
||||
// Group configs by bind address
|
||||
for _, conf := range allConfigs {
|
||||
// use default port if none is specified
|
||||
if conf.Port == "" {
|
||||
conf.Port = Port
|
||||
}
|
||||
|
||||
bindAddr, warnErr, fatalErr := resolveAddr(conf)
|
||||
if fatalErr != nil {
|
||||
return groupings, fatalErr
|
||||
}
|
||||
if warnErr != nil {
|
||||
log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr)
|
||||
}
|
||||
|
||||
// Make sure to compare the string representation of the address,
|
||||
// not the pointer, since a new *TCPAddr is created each time.
|
||||
var existing bool
|
||||
for i := 0; i < len(groupings); i++ {
|
||||
if groupings[i].BindAddr.String() == bindAddr.String() {
|
||||
groupings[i].Configs = append(groupings[i].Configs, conf)
|
||||
existing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existing {
|
||||
groupings = append(groupings, bindingMapping{
|
||||
BindAddr: bindAddr,
|
||||
Configs: []server.Config{conf},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow HTTP and HTTPS to be served on the same address
|
||||
for _, group := range groupings {
|
||||
isTLS := group.Configs[0].TLS.Enabled
|
||||
for _, config := range group.Configs {
|
||||
if config.TLS.Enabled != isTLS {
|
||||
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
|
||||
if config.TLS.Enabled {
|
||||
thisConfigProto = "HTTPS"
|
||||
}
|
||||
if group.Configs[0].TLS.Enabled {
|
||||
otherConfigProto = "HTTPS"
|
||||
}
|
||||
return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
|
||||
group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupings, nil
|
||||
}
|
||||
|
||||
// resolveAddr determines the address (host and port) that a config will
|
||||
// bind to. The returned address, resolvAddr, should be used to bind the
|
||||
// listener or group the config with other configs using the same address.
|
||||
// The first error, if not nil, is just a warning and should be reported
|
||||
// but execution may continue. The second error, if not nil, is a real
|
||||
// problem and the server should not be started.
|
||||
//
|
||||
// This function does not handle edge cases like port "http" or "https" if
|
||||
// they are not known to the system. It does, however, serve on the wildcard
|
||||
// host if resolving the address of the specific hostname fails.
|
||||
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) {
|
||||
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port))
|
||||
if warnErr != nil {
|
||||
// the hostname probably couldn't be resolved, just bind to wildcard then
|
||||
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port))
|
||||
if fatalErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// validDirective returns true if d is a valid
|
||||
// directive; false otherwise.
|
||||
func validDirective(d string) bool {
|
||||
for _, dir := range directiveOrder {
|
||||
if dir.name == d {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
// It uses the default host and port (depends on
|
||||
// host, e.g. localhost is 2015, otherwise 443) and
|
||||
// root.
|
||||
func DefaultInput() CaddyfileInput {
|
||||
port := Port
|
||||
if https.HostQualifies(Host) && port == DefaultPort {
|
||||
port = "443"
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)),
|
||||
}
|
||||
}
|
||||
|
||||
// These defaults are configurable through the command line
|
||||
var (
|
||||
// Root is the site root
|
||||
Root = DefaultRoot
|
||||
|
||||
// Host is the site host
|
||||
Host = DefaultHost
|
||||
|
||||
// Port is the site port
|
||||
Port = DefaultPort
|
||||
)
|
||||
|
||||
// bindingMapping maps a network address to configurations
|
||||
// that will bind to it. The order of the configs is important.
|
||||
type bindingMapping struct {
|
||||
BindAddr *net.TCPAddr
|
||||
Configs []server.Config
|
||||
}
|
||||
|
||||
// bindingGroup maps network addresses to their configurations.
|
||||
// Preserving the order of the groupings is important
|
||||
// (related to graceful shutdown and restart)
|
||||
// so this is a slice, not a literal map.
|
||||
type bindingGroup []bindingMapping
|
|
@ -1,159 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func TestDefaultInput(t *testing.T) {
|
||||
if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
// next few tests simulate user providing -host and/or -port flags
|
||||
|
||||
Host = "not-localhost.com"
|
||||
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "[::1]"
|
||||
if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "127.0.1.1"
|
||||
if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = "not-localhost.com"
|
||||
Port = "1234"
|
||||
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
|
||||
Host = DefaultHost
|
||||
Port = "1234"
|
||||
if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected {
|
||||
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAddr(t *testing.T) {
|
||||
// NOTE: If tests fail due to comparing to string "127.0.0.1",
|
||||
// it's possible that system env resolves with IPv6, or ::1.
|
||||
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
|
||||
// for the assertion, rather than a direct string comparison.
|
||||
|
||||
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
|
||||
// will not behave the same cross-platform, so they have been omitted.
|
||||
|
||||
for i, test := range []struct {
|
||||
config server.Config
|
||||
shouldWarnErr bool
|
||||
shouldFatalErr bool
|
||||
expectedIP string
|
||||
expectedPort int
|
||||
}{
|
||||
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
|
||||
{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0},
|
||||
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234},
|
||||
} {
|
||||
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
|
||||
|
||||
if test.shouldFatalErr && fatalErr == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldFatalErr && fatalErr != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
|
||||
}
|
||||
if fatalErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if test.shouldWarnErr && warnErr == nil {
|
||||
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldWarnErr && warnErr != nil {
|
||||
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
|
||||
}
|
||||
|
||||
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
|
||||
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
|
||||
}
|
||||
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
|
||||
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeOnces(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
onces := makeOnces()
|
||||
if len(onces) != len(directives) {
|
||||
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
|
||||
}
|
||||
expected := map[string]*sync.Once{
|
||||
"dummy": new(sync.Once),
|
||||
"dummy2": new(sync.Once),
|
||||
}
|
||||
if !reflect.DeepEqual(onces, expected) {
|
||||
t.Errorf("onces was %v, expected %v", onces, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeStorages(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
storages := makeStorages()
|
||||
if len(storages) != len(directives) {
|
||||
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
|
||||
}
|
||||
expected := map[string]interface{}{
|
||||
"dummy": nil,
|
||||
"dummy2": nil,
|
||||
}
|
||||
if !reflect.DeepEqual(storages, expected) {
|
||||
t.Errorf("storages was %v, expected %v", storages, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidDirective(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
for i, test := range []struct {
|
||||
directive string
|
||||
valid bool
|
||||
}{
|
||||
{"dummy", true},
|
||||
{"dummy2", true},
|
||||
{"dummy3", false},
|
||||
} {
|
||||
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
|
||||
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The parse package must know which directives
|
||||
// are valid, but it must not import the setup
|
||||
// or config package. To solve this problem, we
|
||||
// fill up this map in our init function here.
|
||||
// The parse package does not need to know the
|
||||
// ordering of the directives.
|
||||
for _, dir := range directiveOrder {
|
||||
parse.ValidDirectives[dir.name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed. Middleware (directives that inject a handler)
|
||||
// are executed in the order A-B-C-*-C-B-A, assuming
|
||||
// they all call the Next handler in the chain.
|
||||
//
|
||||
// Ordering is VERY important. Every middleware will
|
||||
// feel the effects of all other middleware below
|
||||
// (after) them during a request, but they must not
|
||||
// care what middleware above them are doing.
|
||||
//
|
||||
// For example, log needs to know the status code and
|
||||
// exactly how many bytes were written to the client,
|
||||
// which every other middleware can affect, so it gets
|
||||
// registered first. The errors middleware does not
|
||||
// care if gzip or log modifies its response, so it
|
||||
// gets registered below them. Gzip, on the other hand,
|
||||
// DOES care what errors does to the response since it
|
||||
// must compress every output to the client, even error
|
||||
// pages, so it must be registered before the errors
|
||||
// middleware and any others that would write to the
|
||||
// response.
|
||||
var directiveOrder = []directive{
|
||||
// Essential directives that initialize vital configuration settings
|
||||
{"root", setup.Root},
|
||||
{"bind", setup.BindHost},
|
||||
{"tls", https.Setup},
|
||||
|
||||
// Other directives that don't create HTTP handlers
|
||||
{"startup", setup.Startup},
|
||||
{"shutdown", setup.Shutdown},
|
||||
|
||||
// Directives that inject handlers (middleware)
|
||||
{"log", setup.Log},
|
||||
{"gzip", setup.Gzip},
|
||||
{"errors", setup.Errors},
|
||||
{"header", setup.Headers},
|
||||
{"rewrite", setup.Rewrite},
|
||||
{"redir", setup.Redir},
|
||||
{"ext", setup.Ext},
|
||||
{"mime", setup.Mime},
|
||||
{"basicauth", setup.BasicAuth},
|
||||
{"internal", setup.Internal},
|
||||
{"pprof", setup.PProf},
|
||||
{"expvar", setup.ExpVar},
|
||||
{"proxy", setup.Proxy},
|
||||
{"fastcgi", setup.FastCGI},
|
||||
{"websocket", setup.WebSocket},
|
||||
{"markdown", setup.Markdown},
|
||||
{"templates", setup.Templates},
|
||||
{"browse", setup.Browse},
|
||||
}
|
||||
|
||||
// Directives returns the list of directives in order of priority.
|
||||
func Directives() []string {
|
||||
directives := make([]string, len(directiveOrder))
|
||||
for i, d := range directiveOrder {
|
||||
directives[i] = d.name
|
||||
}
|
||||
return directives
|
||||
}
|
||||
|
||||
// RegisterDirective adds the given directive to caddy's list of directives.
|
||||
// Pass the name of a directive you want it to be placed after,
|
||||
// otherwise it will be placed at the bottom of the stack.
|
||||
func RegisterDirective(name string, setup SetupFunc, after string) {
|
||||
dir := directive{name: name, setup: setup}
|
||||
idx := len(directiveOrder)
|
||||
for i := range directiveOrder {
|
||||
if directiveOrder[i].name == after {
|
||||
idx = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...)
|
||||
directiveOrder = newDirectives
|
||||
parse.ValidDirectives[name] = struct{}{}
|
||||
}
|
||||
|
||||
// directive ties together a directive name with its setup function.
|
||||
type directive struct {
|
||||
name string
|
||||
setup SetupFunc
|
||||
}
|
||||
|
||||
// SetupFunc takes a controller and may optionally return a middleware.
|
||||
// If the resulting middleware is not nil, it will be chained into
|
||||
// the HTTP handlers in the order specified in this package.
|
||||
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
|
|
@ -1,31 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
RegisterDirective("foo", nil, "dummy")
|
||||
if len(directiveOrder) != 3 {
|
||||
t.Fatal("Should have 3 directives now")
|
||||
}
|
||||
getNames := func() (s []string) {
|
||||
for _, d := range directiveOrder {
|
||||
s = append(s, d.name)
|
||||
}
|
||||
return s
|
||||
}
|
||||
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) {
|
||||
t.Fatalf("directive order doesn't match: %s", getNames())
|
||||
}
|
||||
RegisterDirective("bar", nil, "ASDASD")
|
||||
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) {
|
||||
t.Fatalf("directive order doesn't match: %s", getNames())
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isLocalhost returns true if host looks explicitly like a localhost address.
|
||||
func isLocalhost(host string) bool {
|
||||
return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 4096
|
||||
|
||||
// Warn if ulimit is too low for production sites
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
|
||||
if err == nil {
|
||||
// Note that an error here need not be reported
|
||||
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
|
||||
if err == nil && lim < min {
|
||||
fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsRestart returns whether this process is, according
|
||||
// to env variables, a fork as part of a graceful restart.
|
||||
func IsRestart() bool {
|
||||
return startedBefore
|
||||
}
|
||||
|
||||
// writePidFile writes the process ID to the file at PidFile, if specified.
|
||||
func writePidFile() error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
return ioutil.WriteFile(PidFile, pid, 0644)
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
RealFile bool
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// IsFile returns true if the original input was a real file on the file system.
|
||||
func (c CaddyfileInput) IsFile() bool { return c.RealFile }
|
|
@ -1,57 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// loadPrivateKey loads a PEM-encoded ECC/RSA private key from file.
|
||||
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
keyBytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown private key type")
|
||||
}
|
||||
|
||||
// savePrivateKey saves a PEM-encoded ECC/RSA private key to file.
|
||||
func savePrivateKey(key crypto.PrivateKey, file string) error {
|
||||
var pemType string
|
||||
var keyBytes []byte
|
||||
switch key := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
var err error
|
||||
pemType = "EC"
|
||||
keyBytes, err = x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
pemType = "RSA"
|
||||
keyBytes = x509.MarshalPKCS1PrivateKey(key)
|
||||
}
|
||||
|
||||
pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
|
||||
keyOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyOut.Chmod(0600)
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pemKey)
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const challengeBasePath = "/.well-known/acme-challenge"
|
||||
|
||||
// RequestCallback proxies challenge requests to ACME client if the
|
||||
// request path starts with challengeBasePath. It returns true if it
|
||||
// handled the request and no more needs to be done; it returns false
|
||||
// if this call was a no-op and the request still needs handling.
|
||||
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
|
||||
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("[ERROR] ACME proxy handler: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,411 +0,0 @@
|
|||
// Package https facilitates the management of TLS assets and integrates
|
||||
// Let's Encrypt functionality into Caddy with first-class support for
|
||||
// creating and renewing certificates automatically. It is designed to
|
||||
// configure sites for HTTPS by default.
|
||||
package https
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Activate sets up TLS for each server config in configs
|
||||
// as needed; this consists of acquiring and maintaining
|
||||
// certificates and keys for qualifying configs and enabling
|
||||
// OCSP stapling for all TLS-enabled configs.
|
||||
//
|
||||
// This function may prompt the user to provide an email
|
||||
// address if none is available through other means. It
|
||||
// prefers the email address specified in the config, but
|
||||
// if that is not available it will check the command line
|
||||
// argument. If absent, it will use the most recent email
|
||||
// address from last time. If there isn't one, the user
|
||||
// will be prompted and shown SA link.
|
||||
//
|
||||
// Also note that calling this function activates asset
|
||||
// management automatically, which keeps certificates
|
||||
// renewed and OCSP stapling updated.
|
||||
//
|
||||
// Activate returns the updated list of configs, since
|
||||
// some may have been appended, for example, to redirect
|
||||
// plaintext HTTP requests to their HTTPS counterpart.
|
||||
// This function only appends; it does not splice.
|
||||
func Activate(configs []server.Config) ([]server.Config, error) {
|
||||
// just in case previous caller forgot...
|
||||
Deactivate()
|
||||
|
||||
// pre-screen each config and earmark the ones that qualify for managed TLS
|
||||
MarkQualified(configs)
|
||||
|
||||
// place certificates and keys on disk
|
||||
err := ObtainCerts(configs, true, false)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// update TLS configurations
|
||||
err = EnableTLS(configs, true)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// set up redirects
|
||||
configs = MakePlaintextRedirects(configs)
|
||||
|
||||
// renew all relevant certificates that need renewal. this is important
|
||||
// to do right away for a couple reasons, mainly because each restart,
|
||||
// the renewal ticker is reset, so if restarts happen more often than
|
||||
// the ticker interval, renewals would never happen. but doing
|
||||
// it right away at start guarantees that renewals aren't missed.
|
||||
err = renewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// keep certificates renewed and OCSP stapling updated
|
||||
go maintainAssets(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)
|
||||
stopChan = make(chan struct{})
|
||||
return
|
||||
}
|
||||
|
||||
// MarkQualified scans each config and, if it qualifies for managed
|
||||
// TLS, it sets the Managed field of the TLSConfig to true.
|
||||
func MarkQualified(configs []server.Config) {
|
||||
for i := 0; i < len(configs); i++ {
|
||||
if ConfigQualifies(configs[i]) {
|
||||
configs[i].TLS.Managed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCerts obtains certificates for all these configs as long as a
|
||||
// certificate does not already exist on disk. It does not modify the
|
||||
// configs at all; it only obtains and stores certificates and keys to
|
||||
// the disk. If allowPrompts is true, the user may be shown a prompt.
|
||||
// If proxyACME is true, the ACME challenges will be proxied to our alt port.
|
||||
func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
|
||||
// We group configs by email so we don't make the same clients over and
|
||||
// over. This has the potential to prompt the user for an email, but we
|
||||
// prevent that by assuming that if we already have a listener that can
|
||||
// proxy ACME challenge requests, then the server is already running and
|
||||
// the operator is no longer present.
|
||||
groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
|
||||
|
||||
for email, group := range groupedConfigs {
|
||||
// Wait as long as we can before creating the client, because it
|
||||
// may not be needed, for example, if we already have what we
|
||||
// need on disk. Creating a client involves the network and
|
||||
// potentially prompting the user, etc., so only do if necessary.
|
||||
var client *ACMEClient
|
||||
|
||||
for _, cfg := range group {
|
||||
if !HostQualifies(cfg.Host) || existingCertAndKey(cfg.Host) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Now we definitely do need a client
|
||||
if client == nil {
|
||||
var err error
|
||||
client, err = NewACMEClient(email, allowPrompts)
|
||||
if err != nil {
|
||||
return errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// c.Configure assumes that allowPrompts == !proxyACME,
|
||||
// but that's not always true. For example, a restart where
|
||||
// the user isn't present and we're not listening on port 80.
|
||||
// TODO: This could probably be refactored better.
|
||||
if proxyACME {
|
||||
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
|
||||
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
|
||||
} else {
|
||||
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
|
||||
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
|
||||
}
|
||||
|
||||
err := client.Obtain([]string{cfg.Host})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// groupConfigsByEmail groups configs by the email address to be used by an
|
||||
// ACME client. It only groups configs that have TLS enabled and that are
|
||||
// marked as Managed. If userPresent is true, the operator MAY be prompted
|
||||
// for an email address.
|
||||
func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
|
||||
initMap := make(map[string][]server.Config)
|
||||
for _, cfg := range configs {
|
||||
if !cfg.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
leEmail := getEmail(cfg, userPresent)
|
||||
initMap[leEmail] = append(initMap[leEmail], cfg)
|
||||
}
|
||||
return initMap
|
||||
}
|
||||
|
||||
// EnableTLS configures each config to use TLS according to default settings.
|
||||
// It will only change configs that are marked as managed, and assumes that
|
||||
// certificates and keys are already on disk. If loadCertificates is true,
|
||||
// the certificates will be loaded from disk into the cache for this process
|
||||
// to use. If false, TLS will still be enabled and configured with default
|
||||
// settings, but no certificates will be parsed loaded into the cache, and
|
||||
// the returned error value will always be nil.
|
||||
func EnableTLS(configs []server.Config, loadCertificates bool) error {
|
||||
for i := 0; i < len(configs); i++ {
|
||||
if !configs[i].TLS.Managed {
|
||||
continue
|
||||
}
|
||||
configs[i].TLS.Enabled = true
|
||||
if loadCertificates && HostQualifies(configs[i].Host) {
|
||||
_, err := cacheManagedCertificate(configs[i].Host, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
setDefaultTLSParams(&configs[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hostHasOtherPort returns true if there is another config in the list with the same
|
||||
// hostname that has port otherPort, or false otherwise. All the configs are checked
|
||||
// against the hostname of allConfigs[thisConfigIdx].
|
||||
func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == thisConfigIdx {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
|
||||
// hosts. You must pass in all configs, not just configs that qualify, since
|
||||
// we must know whether the same host already exists on port 80, and those would
|
||||
// not be in a list of configs that qualify for automatic HTTPS. This function will
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func MakePlaintextRedirects(allConfigs []server.Config) []server.Config {
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// ConfigQualifies returns true if cfg qualifies for
|
||||
// fully managed TLS (but not on-demand TLS, which is
|
||||
// not considered here). It does NOT check to see if a
|
||||
// cert and key already exist for the config. If the
|
||||
// config does qualify, you should set cfg.TLS.Managed
|
||||
// to true and check that instead, because the process of
|
||||
// setting up the config may make it look like it
|
||||
// doesn't qualify even though it originally did.
|
||||
func ConfigQualifies(cfg server.Config) bool {
|
||||
return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
|
||||
|
||||
// user can force-disable automatic HTTPS for this host
|
||||
cfg.Scheme != "http" &&
|
||||
cfg.Port != "80" &&
|
||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
||||
|
||||
// we get can't certs for some kinds of hostnames, but
|
||||
// on-demand TLS allows empty hostnames at startup
|
||||
(HostQualifies(cfg.Host) || cfg.TLS.OnDemand)
|
||||
}
|
||||
|
||||
// HostQualifies returns true if the hostname alone
|
||||
// appears eligible for automatic HTTPS. For example,
|
||||
// localhost, empty hostname, and IP addresses are
|
||||
// not eligible because we cannot obtain certificates
|
||||
// for those names.
|
||||
func HostQualifies(hostname string) bool {
|
||||
return hostname != "localhost" && // localhost is ineligible
|
||||
|
||||
// hostname must not be empty
|
||||
strings.TrimSpace(hostname) != "" &&
|
||||
|
||||
// cannot be an IP address, see
|
||||
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
||||
// (also trim [] from either end, since that special case can sneak through
|
||||
// for IPv6 addresses using the -host flag and with empty/no Caddyfile)
|
||||
net.ParseIP(strings.Trim(hostname, "[]")) == nil
|
||||
}
|
||||
|
||||
// existingCertAndKey returns true if the host has a certificate
|
||||
// and private key in storage already, false otherwise.
|
||||
func existingCertAndKey(host string) bool {
|
||||
_, err := os.Stat(storage.SiteCertFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(storage.SiteKeyFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// saveCertResource saves the certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func saveCertResource(cert acme.CertificateResource) error {
|
||||
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||
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
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on port 80.
|
||||
func redirPlaintextHost(cfg server.Config) server.Config {
|
||||
toURL := "https://{host}" // serve any host, since cfg.Host could be empty
|
||||
if cfg.Port != "443" && cfg.Port != "80" {
|
||||
toURL += ":" + cfg.Port
|
||||
}
|
||||
|
||||
redirMidware := func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: []redirect.Rule{
|
||||
{
|
||||
FromScheme: "http",
|
||||
FromPath: "/",
|
||||
To: toURL + "{uri}",
|
||||
Code: http.StatusMovedPermanently,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
return server.Config{
|
||||
Host: cfg.Host,
|
||||
BindHost: cfg.BindHost,
|
||||
Port: "80",
|
||||
Middleware: []middleware.Middleware{redirMidware},
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for host via ACME protocol.
|
||||
func Revoke(host string) error {
|
||||
if !existingCertAndKey(host) {
|
||||
return errors.New("no certificate and key for " + host)
|
||||
}
|
||||
|
||||
email := getEmail(server.Config{Host: host}, true)
|
||||
if email == "" {
|
||||
return errors.New("email is required to revoke")
|
||||
}
|
||||
|
||||
client, err := NewACMEClient(email, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile := storage.SiteCertFile(host)
|
||||
certBytes, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.RevokeCertificate(certBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Remove(certFile)
|
||||
if err != nil {
|
||||
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultEmail represents the Let's Encrypt account email to use if none provided
|
||||
DefaultEmail string
|
||||
|
||||
// Agreed indicates whether user has agreed to the Let's Encrypt SA
|
||||
Agreed bool
|
||||
|
||||
// CAUrl represents the base URL to the CA's ACME endpoint
|
||||
CAUrl string
|
||||
)
|
||||
|
||||
// AlternatePort is the port on which the acme client will open a
|
||||
// listener and solve the CA's challenges. If this alternate port
|
||||
// is used instead of the default port (80 or 443), then the
|
||||
// default port for the challenge must be forwarded to this one.
|
||||
const AlternatePort = "5033"
|
||||
|
||||
// KeyType is the type to use for new keys.
|
||||
// This shouldn't need to change except for in tests;
|
||||
// the size can be drastically reduced for speed.
|
||||
var KeyType = acme.RSA2048
|
||||
|
||||
// stopChan is used to signal the maintenance goroutine
|
||||
// to terminate.
|
||||
var stopChan chan struct{}
|
|
@ -1,332 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func TestHostQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
host string
|
||||
expect bool
|
||||
}{
|
||||
{"localhost", false},
|
||||
{"127.0.0.1", false},
|
||||
{"127.0.1.5", false},
|
||||
{"::1", false},
|
||||
{"[::1]", false},
|
||||
{"[::]", false},
|
||||
{"::", false},
|
||||
{"", false},
|
||||
{" ", false},
|
||||
{"0.0.0.0", false},
|
||||
{"192.168.1.3", false},
|
||||
{"10.0.2.1", false},
|
||||
{"169.112.53.4", false},
|
||||
{"foobar.com", true},
|
||||
{"sub.foobar.com", true},
|
||||
} {
|
||||
if HostQualifies(test.host) && !test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
|
||||
}
|
||||
if !HostQualifies(test.host) && test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigQualifies(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
cfg server.Config
|
||||
expect bool
|
||||
}{
|
||||
{server.Config{Host: ""}, false},
|
||||
{server.Config{Host: "localhost"}, false},
|
||||
{server.Config{Host: "123.44.3.21"}, false},
|
||||
{server.Config{Host: "example.com"}, true},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false},
|
||||
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true},
|
||||
{server.Config{Host: "example.com", Scheme: "http"}, false},
|
||||
{server.Config{Host: "example.com", Port: "80"}, false},
|
||||
{server.Config{Host: "example.com", Port: "1234"}, true},
|
||||
{server.Config{Host: "example.com", Scheme: "https"}, true},
|
||||
{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false},
|
||||
} {
|
||||
if test.expect && !ConfigQualifies(test.cfg) {
|
||||
t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg)
|
||||
}
|
||||
if !test.expect && ConfigQualifies(test.cfg) {
|
||||
t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(server.Config{
|
||||
Host: "example.com",
|
||||
BindHost: "93.184.216.34",
|
||||
Port: "1234",
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected {
|
||||
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Port, "80"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.Middleware == nil || len(cfg.Middleware) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
|
||||
}
|
||||
|
||||
handler, ok := cfg.Middleware[0](nil).(redirect.Redirect)
|
||||
if !ok {
|
||||
t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
|
||||
}
|
||||
if len(handler.Rules) != 1 {
|
||||
t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
|
||||
}
|
||||
|
||||
// Check redirect rule for correctness
|
||||
if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
|
||||
t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
|
||||
}
|
||||
|
||||
// browsers can infer a default port from scheme, so make sure the port
|
||||
// doesn't get added in explicitly for default ports like 443 for https.
|
||||
cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"})
|
||||
handler, _ = cfg.Middleware[0](nil).(redirect.Redirect)
|
||||
if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected {
|
||||
t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCertResource(t *testing.T) {
|
||||
storage = Storage("./le_test_save")
|
||||
defer func() {
|
||||
err := os.RemoveAll(string(storage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
certContents := "certificate"
|
||||
keyContents := "private key"
|
||||
metaContents := `{
|
||||
"domain": "example.com",
|
||||
"certUrl": "https://example.com/cert",
|
||||
"certStableUrl": "https://example.com/cert/stable"
|
||||
}`
|
||||
|
||||
cert := acme.CertificateResource{
|
||||
Domain: domain,
|
||||
CertURL: "https://example.com/cert",
|
||||
CertStableURL: "https://example.com/cert/stable",
|
||||
PrivateKey: []byte(keyContents),
|
||||
Certificate: []byte(certContents),
|
||||
}
|
||||
|
||||
err := saveCertResource(cert)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading certificate file, got: %v", err)
|
||||
}
|
||||
if string(certFile) != certContents {
|
||||
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
|
||||
}
|
||||
|
||||
keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading private key file, got: %v", err)
|
||||
}
|
||||
if string(keyFile) != keyContents {
|
||||
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
|
||||
}
|
||||
|
||||
metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error reading meta file, got: %v", err)
|
||||
}
|
||||
if string(metaFile) != metaContents {
|
||||
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingCertAndKey(t *testing.T) {
|
||||
storage = Storage("./le_test_existing")
|
||||
defer func() {
|
||||
err := os.RemoveAll(string(storage))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
|
||||
}
|
||||
}()
|
||||
|
||||
domain := "example.com"
|
||||
|
||||
if existingCertAndKey(domain) {
|
||||
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
|
||||
}
|
||||
|
||||
err := saveCertResource(acme.CertificateResource{
|
||||
Domain: domain,
|
||||
PrivateKey: []byte("key"),
|
||||
Certificate: []byte("cert"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
|
||||
if !existingCertAndKey(domain) {
|
||||
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHasOtherPort(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", Port: "80"},
|
||||
{Host: "sub1.example.com", Port: "80"},
|
||||
{Host: "sub1.example.com", Port: "443"},
|
||||
}
|
||||
|
||||
if hostHasOtherPort(configs, 0, "80") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
|
||||
}
|
||||
if hostHasOtherPort(configs, 0, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
|
||||
}
|
||||
if !hostHasOtherPort(configs, 1, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePlaintextRedirects(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
// Happy path = standard redirect from 80 to 443
|
||||
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Host on port 80 already defined; don't change it (no redirect)
|
||||
{Host: "sub1.example.com", Port: "80", Scheme: "http"},
|
||||
{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Redirect from port 80 to port 5000 in this case
|
||||
{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}},
|
||||
|
||||
// Can redirect from 80 to either 443 or 5001, but choose 443
|
||||
{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}},
|
||||
{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}},
|
||||
}
|
||||
|
||||
result := MakePlaintextRedirects(configs)
|
||||
expectedRedirCount := 3
|
||||
|
||||
if len(result) != len(configs)+expectedRedirCount {
|
||||
t.Errorf("Expected %d redirect(s) to be added, but got %d",
|
||||
expectedRedirCount, len(result)-len(configs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableTLS(t *testing.T) {
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
EnableTLS(configs, false)
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
}
|
||||
if configs[1].TLS.Enabled {
|
||||
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupConfigsByEmail(t *testing.T) {
|
||||
if groupConfigsByEmail([]server.Config{}, false) == nil {
|
||||
t.Errorf("With empty input, returned map was nil, but expected non-nil map")
|
||||
}
|
||||
|
||||
configs := []server.Config{
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
|
||||
{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
|
||||
{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
|
||||
{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed
|
||||
}
|
||||
DefaultEmail = "test@example.com"
|
||||
|
||||
groups := groupConfigsByEmail(configs, true)
|
||||
|
||||
if groups == nil {
|
||||
t.Fatalf("Returned map was nil, but expected values")
|
||||
}
|
||||
|
||||
if len(groups) != 2 {
|
||||
t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups)
|
||||
}
|
||||
if len(groups["foo@bar"]) != 2 {
|
||||
t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"])
|
||||
}
|
||||
if len(groups[DefaultEmail]) != 3 {
|
||||
t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkQualified(t *testing.T) {
|
||||
// TODO: TestConfigQualifies and this test share the same config list...
|
||||
configs := []server.Config{
|
||||
{Host: ""},
|
||||
{Host: "localhost"},
|
||||
{Host: "123.44.3.21"},
|
||||
{Host: "example.com"},
|
||||
{Host: "example.com", TLS: server.TLSConfig{Manual: true}},
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}},
|
||||
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}},
|
||||
{Host: "example.com", Scheme: "http"},
|
||||
{Host: "example.com", Port: "80"},
|
||||
{Host: "example.com", Port: "1234"},
|
||||
{Host: "example.com", Scheme: "https"},
|
||||
{Host: "example.com", Port: "80", Scheme: "https"},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
MarkQualified(configs)
|
||||
|
||||
count := 0
|
||||
for _, cfg := range configs {
|
||||
if cfg.TLS.Managed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != expectedManagedCount {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
|
@ -1,355 +0,0 @@
|
|||
package https
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Setup sets up the TLS configuration and installs certificates that
|
||||
// are specified by the user in the config file. All the automatic HTTPS
|
||||
// stuff comes later outside of this function.
|
||||
func Setup(c *setup.Controller) (middleware.Middleware, error) {
|
||||
if c.Port == "80" || c.Scheme == "http" {
|
||||
c.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address())
|
||||
return nil, nil
|
||||
}
|
||||
c.TLS.Enabled = true
|
||||
|
||||
for c.Next() {
|
||||
var certificateFile, keyFile, loadDir, maxCerts string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 1:
|
||||
c.TLS.LetsEncryptEmail = args[0]
|
||||
|
||||
// user can force-disable managed TLS this way
|
||||
if c.TLS.LetsEncryptEmail == "off" {
|
||||
c.TLS.Enabled = false
|
||||
return nil, nil
|
||||
}
|
||||
case 2:
|
||||
certificateFile = args[0]
|
||||
keyFile = args[1]
|
||||
c.TLS.Manual = true
|
||||
}
|
||||
|
||||
// Optional block with extra parameters
|
||||
var hadBlock bool
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
switch c.Val() {
|
||||
case "key_type":
|
||||
arg := c.RemainingArgs()
|
||||
value, ok := supportedKeyTypes[strings.ToUpper(arg[0])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong KeyType name or KeyType not supported '%s'", c.Val())
|
||||
}
|
||||
KeyType = value
|
||||
case "protocols":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value, ok := supportedProtocols[strings.ToLower(args[0])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMinVersion = value
|
||||
value, ok = supportedProtocols[strings.ToLower(args[1])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMaxVersion = value
|
||||
case "ciphers":
|
||||
for c.NextArg() {
|
||||
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
|
||||
}
|
||||
case "clients":
|
||||
clientCertList := c.RemainingArgs()
|
||||
if len(clientCertList) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
listStart, mustProvideCA := 1, true
|
||||
switch clientCertList[0] {
|
||||
case "request":
|
||||
c.TLS.ClientAuth = tls.RequestClientCert
|
||||
mustProvideCA = false
|
||||
case "require":
|
||||
c.TLS.ClientAuth = tls.RequireAnyClientCert
|
||||
mustProvideCA = false
|
||||
case "verify_if_given":
|
||||
c.TLS.ClientAuth = tls.VerifyClientCertIfGiven
|
||||
default:
|
||||
c.TLS.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
listStart = 0
|
||||
}
|
||||
if mustProvideCA && len(clientCertList) <= listStart {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
c.TLS.ClientCerts = clientCertList[listStart:]
|
||||
case "load":
|
||||
c.Args(&loadDir)
|
||||
c.TLS.Manual = true
|
||||
case "max_certs":
|
||||
c.Args(&maxCerts)
|
||||
c.TLS.OnDemand = true
|
||||
default:
|
||||
return nil, c.Errf("Unknown keyword '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
|
||||
// tls requires at least one argument if a block is not opened
|
||||
if len(args) == 0 && !hadBlock {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
// set certificate limit if on-demand TLS is enabled
|
||||
if maxCerts != "" {
|
||||
maxCertsNum, err := strconv.Atoi(maxCerts)
|
||||
if err != nil || maxCertsNum < 1 {
|
||||
return nil, c.Err("max_certs must be a positive integer")
|
||||
}
|
||||
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
|
||||
onDemandMaxIssue = int32(maxCertsNum)
|
||||
}
|
||||
}
|
||||
|
||||
// don't try to load certificates unless we're supposed to
|
||||
if !c.TLS.Enabled || !c.TLS.Manual {
|
||||
continue
|
||||
}
|
||||
|
||||
// load a single certificate and key, if specified
|
||||
if certificateFile != "" && keyFile != "" {
|
||||
err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
|
||||
}
|
||||
|
||||
// load a directory of certificates, if specified
|
||||
if loadDir != "" {
|
||||
err := loadCertsInDir(c, loadDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultTLSParams(c.Config)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// loadCertsInDir loads all the certificates/keys in dir, as long as
|
||||
// the file ends with .pem. This method of loading certificates is
|
||||
// modeled after haproxy, which expects the certificate and key to
|
||||
// be bundled into the same file:
|
||||
// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
|
||||
//
|
||||
// This function may write to the log as it walks the directory tree.
|
||||
func loadCertsInDir(c *setup.Controller, dir string) error {
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
|
||||
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
|
||||
var foundKey bool // use only the first key in the file
|
||||
|
||||
bundle, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
// Decode next block so we can see what type it is
|
||||
var derBlock *pem.Block
|
||||
derBlock, bundle = pem.Decode(bundle)
|
||||
if derBlock == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if derBlock.Type == "CERTIFICATE" {
|
||||
// Re-encode certificate as PEM, appending to certificate chain
|
||||
pem.Encode(certBuilder, derBlock)
|
||||
} else if derBlock.Type == "EC PARAMETERS" {
|
||||
// EC keys generated from openssl can be composed of two blocks:
|
||||
// parameters and key (parameter block should come first)
|
||||
if !foundKey {
|
||||
// Encode parameters
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
|
||||
// Key must immediately follow
|
||||
derBlock, bundle = pem.Decode(bundle)
|
||||
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
|
||||
return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
|
||||
}
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
foundKey = true
|
||||
}
|
||||
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
|
||||
// RSA key
|
||||
if !foundKey {
|
||||
pem.Encode(keyBuilder, derBlock)
|
||||
foundKey = true
|
||||
}
|
||||
} else {
|
||||
return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
|
||||
}
|
||||
}
|
||||
|
||||
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
|
||||
if len(certPEMBytes) == 0 {
|
||||
return c.Errf("%s: failed to parse PEM data", path)
|
||||
}
|
||||
if len(keyPEMBytes) == 0 {
|
||||
return c.Errf("%s: no private key block found", path)
|
||||
}
|
||||
|
||||
err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
|
||||
if err != nil {
|
||||
return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err)
|
||||
}
|
||||
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
|
||||
// and server preferences of a server.Config if they were not previously set
|
||||
// (it does not overwrite; only fills in missing values). It will also set the
|
||||
// port to 443 if not already set, TLS is enabled, TLS is manual, and the host
|
||||
// does not equal localhost.
|
||||
func setDefaultTLSParams(c *server.Config) {
|
||||
// If no ciphers provided, use default list
|
||||
if len(c.TLS.Ciphers) == 0 {
|
||||
c.TLS.Ciphers = defaultCiphers
|
||||
}
|
||||
|
||||
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
|
||||
// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
|
||||
c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...)
|
||||
|
||||
// Set default protocol min and max versions - must balance compatibility and security
|
||||
if c.TLS.ProtocolMinVersion == 0 {
|
||||
c.TLS.ProtocolMinVersion = tls.VersionTLS10
|
||||
}
|
||||
if c.TLS.ProtocolMaxVersion == 0 {
|
||||
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
// Prefer server cipher suites
|
||||
c.TLS.PreferServerCipherSuites = true
|
||||
|
||||
// Default TLS port is 443; only use if port is not manually specified,
|
||||
// TLS is enabled, and the host is not localhost
|
||||
if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" {
|
||||
c.Port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// Map of supported key types
|
||||
var supportedKeyTypes = map[string]acme.KeyType{
|
||||
"P384": acme.EC384,
|
||||
"P256": acme.EC256,
|
||||
"RSA8192": acme.RSA8192,
|
||||
"RSA4096": acme.RSA4096,
|
||||
"RSA2048": acme.RSA2048,
|
||||
}
|
||||
|
||||
// Map of supported protocols.
|
||||
// SSLv3 will be not supported in future release.
|
||||
// HTTP/2 only supports TLS 1.2 and higher.
|
||||
var supportedProtocols = map[string]uint16{
|
||||
"ssl3.0": tls.VersionSSL30,
|
||||
"tls1.0": tls.VersionTLS10,
|
||||
"tls1.1": tls.VersionTLS11,
|
||||
"tls1.2": tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Map of supported ciphers, used only for parsing config.
|
||||
//
|
||||
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
|
||||
// including all but two of the suites below (the two GCM suites).
|
||||
// See https://http2.github.io/http2-spec/#BadCipherSuites
|
||||
//
|
||||
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
|
||||
// it is always added (even though it is not technically a cipher suite).
|
||||
//
|
||||
// This map, like any map, is NOT ORDERED. Do not range over this map.
|
||||
var supportedCiphersMap = map[string]uint16{
|
||||
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of supported cipher suites in descending order of preference.
|
||||
// Ordering is very important! Getting the wrong order will break
|
||||
// mainstream clients, especially with HTTP/2.
|
||||
//
|
||||
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
|
||||
// added manually.
|
||||
var supportedCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of all the ciphers we want to use by default
|
||||
var defaultCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
}
|
7
caddy/main.go
Normal file
7
caddy/main.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "github.com/mholt/caddy/caddy/caddymain"
|
||||
|
||||
func main() {
|
||||
caddymain.Run()
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexerTestCase struct {
|
||||
input string
|
||||
expected []token
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testCases := []lexerTestCase{
|
||||
{
|
||||
input: `host:123`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123
|
||||
|
||||
directive`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 3, text: "directive"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
directive
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 2, text: "directive"},
|
||||
{line: 3, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 { directive }`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 1, text: "directive"},
|
||||
{line: 1, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
#comment
|
||||
directive
|
||||
# comment
|
||||
foobar # another comment
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 3, text: "directive"},
|
||||
{line: 5, text: "foobar"},
|
||||
{line: 6, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `a "quoted value" b
|
||||
foobar`,
|
||||
expected: []token{
|
||||
{line: 1, text: "a"},
|
||||
{line: 1, text: "quoted value"},
|
||||
{line: 1, text: "b"},
|
||||
{line: 2, text: "foobar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted \"value\" inside" B`,
|
||||
expected: []token{
|
||||
{line: 1, text: "A"},
|
||||
{line: 1, text: `quoted "value" inside`},
|
||||
{line: 1, text: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `don't\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\\escape"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
break inside" {
|
||||
foobar
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "A"},
|
||||
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||
{line: 2, text: "{"},
|
||||
{line: 3, text: "foobar"},
|
||||
{line: 4, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"C:\php\php-cgi.exe"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `C:\php\php-cgi.exe`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `empty "" string`,
|
||||
expected: []token{
|
||||
{line: 1, text: `empty`},
|
||||
{line: 1, text: ``},
|
||||
{line: 1, text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "skip those\r\nCR characters",
|
||||
expected: []token{
|
||||
{line: 1, text: "skip"},
|
||||
{line: 1, text: "those"},
|
||||
{line: 2, text: "CR"},
|
||||
{line: 2, text: "characters"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := tokenize(testCase.input)
|
||||
lexerCompare(t, i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(input string) (tokens []token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lexerCompare(t *testing.T, n int, expected, actual []token) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||
}
|
||||
|
||||
for i := 0; i < len(actual) && i < len(expected); i++ {
|
||||
if actual[i].line != expected[i].line {
|
||||
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
||||
n, i, expected[i].text, expected[i].line, actual[i].line)
|
||||
break
|
||||
}
|
||||
if actual[i].text != expected[i].text {
|
||||
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
|
||||
n, i, expected[i].text, actual[i].text)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// Package parse provides facilities for parsing configuration files.
|
||||
package parse
|
||||
|
||||
import "io"
|
||||
|
||||
// ServerBlocks parses the input just enough to organize tokens,
|
||||
// in order, by server block. No further parsing is performed.
|
||||
// If checkDirectives is true, only valid directives will be allowed
|
||||
// otherwise we consider it a parse error. Server blocks are returned
|
||||
// in the order in which they appear.
|
||||
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input)}
|
||||
p.checkDirectives = checkDirectives
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ValidDirectives is a set of directives that are valid (unordered). Populated
|
||||
// by config package's init function.
|
||||
var ValidDirectives = make(map[string]struct{})
|
|
@ -1,22 +0,0 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,480 +0,0 @@
|
|||
package parse
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "", "localhost", "", false},
|
||||
{`LOCALHOST`, "", "localhost", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", false},
|
||||
{`LOCALHOST:1234`, "", "localhost", "1234", false},
|
||||
{`localhost:`, "", "localhost", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false},
|
||||
{`:1234`, "", "", "1234", false},
|
||||
{`[::1]`, "", "::1", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", false},
|
||||
{`:`, "", "", "", false},
|
||||
{`localhost:http`, "http", "localhost", "80", false},
|
||||
{`localhost:https`, "https", "localhost", "443", false},
|
||||
{`:http`, "http", "", "80", false},
|
||||
{`:https`, "https", "", "443", false},
|
||||
{`http://localhost:https`, "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", false},
|
||||
{`https://localhost`, "https", "localhost", "443", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", false},
|
||||
{`http://[::1]`, "http", "::1", "80", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", false},
|
||||
{`http://LOCALHOST:1234`, "http", "localhost", "1234", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", false},
|
||||
{``, "", "", "", false},
|
||||
{`::1`, "", "::1", "", true},
|
||||
{`localhost::`, "", "localhost::", "", true},
|
||||
{`#$%@`, "", "#$%@", "", true},
|
||||
} {
|
||||
actual, err := standardAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
|
||||
}
|
||||
|
||||
if actual.Scheme != test.scheme {
|
||||
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != test.host {
|
||||
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
|
||||
}
|
||||
if actual.Port != test.port {
|
||||
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses []address
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []address{
|
||||
{"localhost:1234", "", "localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []address{
|
||||
{"localhost:1234", "", "localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
{"https://localhost", "https", "localhost", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []address{
|
||||
{"http://localhost", "http", "localhost", "80"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []address{
|
||||
{"host1:80", "", "host1", "80"},
|
||||
{"http://host2.com", "http", "host2.com", "80"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []address{
|
||||
{"http://host1.com", "http", "host1.com", "80"},
|
||||
{"http://host2.com", "http", "host2.com", "80"},
|
||||
{"https://host3.com", "https", "host3.com", "443"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []address{
|
||||
{"http://host1.com:1234", "http", "host1.com", "1234"},
|
||||
{"https://host2.com", "https", "host2.com", "443"},
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []address{
|
||||
{"127.0.0.1", "", "127.0.0.1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
unknown_directive`, true, []address{
|
||||
{"127.0.0.1", "", "127.0.0.1", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import import_test1.txt`, false, []address{
|
||||
{"localhost", "", "localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import import_test2.txt`, false, []address{
|
||||
{"host1", "", "host1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`import not_found.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []address{}, map[string]int{}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Addresses) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d addresses, got %d",
|
||||
i, len(test.addresses), len(result.Addresses))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Addresses {
|
||||
if addr.Host != test.addresses[j].Host {
|
||||
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j].Port {
|
||||
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses [][]address // addresses per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]address{
|
||||
{{"localhost", "", "localhost", ""}},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}},
|
||||
{{"localhost:2015", "", "localhost", "2015"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]address{
|
||||
{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]address{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]address{
|
||||
{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}},
|
||||
{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}},
|
||||
}},
|
||||
|
||||
{`import import_glob*.txt`, false, [][]address{
|
||||
{{"glob0.host0", "", "glob0.host0", ""}},
|
||||
{{"glob0.host1", "", "glob0.host1", ""}},
|
||||
{{"glob1.host0", "", "glob1.host0", ""}},
|
||||
{{"glob2.host0", "", "glob2.host0", ""}},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.addresses), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Addresses) != len(test.addresses[j]) {
|
||||
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
|
||||
i, len(test.addresses[j]), j, len(block.Addresses))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Addresses {
|
||||
if addr.Host != test.addresses[j][k].Host {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j][k].Port {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual {
|
||||
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func setupParseTests() {
|
||||
// Set up some bogus directives for testing
|
||||
ValidDirectives = map[string]struct{}{
|
||||
"dir1": {},
|
||||
"dir2": {},
|
||||
"dir3": {},
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
|
||||
return p
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// +build !windows
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/caddy/https"
|
||||
)
|
||||
|
||||
// Restart restarts the entire application; gracefully with zero
|
||||
// downtime if on a POSIX-compatible system, or forcefully if on
|
||||
// Windows but with imperceptibly-short downtime.
|
||||
//
|
||||
// The behavior can be controlled by the RestartMode variable,
|
||||
// where "inproc" will restart forcefully in process same as
|
||||
// Windows on a POSIX-compatible system.
|
||||
//
|
||||
// The restarted application will use newCaddyfile as its input
|
||||
// configuration. If newCaddyfile is nil, the current (existing)
|
||||
// Caddyfile configuration will be used.
|
||||
//
|
||||
// Note: The process must exist in the same place on the disk in
|
||||
// order for this to work. Thus, multiple graceful restarts don't
|
||||
// work if executing with `go run`, since the binary is cleaned up
|
||||
// when `go run` sees the initial parent process exit.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
log.Println("[INFO] Restarting")
|
||||
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
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())
|
||||
}
|
||||
|
||||
// Add file descriptors of all the sockets for new instance
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
restartFds[s.Addr] = s.ListenerFd()
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
return restartInProc(newCaddyfile)
|
||||
}
|
||||
|
||||
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(filepath.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
|
||||
if err != nil {
|
||||
return errors.New("loading Caddyfile: " + err.Error())
|
||||
}
|
||||
|
||||
// first mark the configs that are qualified for managed TLS
|
||||
https.MarkQualified(configs)
|
||||
|
||||
// since we group by bind address to obtain certs, we must call
|
||||
// EnableTLS to make sure the port is set properly first
|
||||
// (can ignore error since we aren't actually using the certs)
|
||||
https.EnableTLS(configs, false)
|
||||
|
||||
// find out if we can let the acme package start its own challenge listener
|
||||
// on port 80
|
||||
var proxyACME bool
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
_, port, _ := net.SplitHostPort(s.Addr)
|
||||
if port == "80" {
|
||||
proxyACME = true
|
||||
break
|
||||
}
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
// place certs on the disk
|
||||
err = https.ObtainCerts(configs, false, proxyACME)
|
||||
if err != nil {
|
||||
return errors.New("obtaining certs: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import "log"
|
||||
|
||||
// Restart restarts Caddy forcefully using newCaddyfile,
|
||||
// or, if nil, the current/existing Caddyfile is reused.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
log.Println("[INFO] Restarting")
|
||||
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
return restartInProc(newCaddyfile)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package caddy
|
||||
|
||||
import "log"
|
||||
|
||||
// restartInProc restarts Caddy forcefully in process using newCaddyfile.
|
||||
func restartInProc(newCaddyfile Input) error {
|
||||
wg.Add(1) // barrier so Wait() doesn't unblock
|
||||
defer wg.Done()
|
||||
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caddyfileMu.Lock()
|
||||
oldCaddyfile := caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
err = Start(newCaddyfile)
|
||||
if err != nil {
|
||||
// revert to old Caddyfile
|
||||
if oldErr := Start(oldCaddyfile); oldErr != nil {
|
||||
log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package setup
|
||||
|
||||
import "github.com/mholt/caddy/middleware"
|
||||
|
||||
// BindHost sets the host to bind the listener to.
|
||||
func BindHost(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.Args(&c.BindHost) {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Controller is given to the setup function of middlewares which
|
||||
// gives them access to be able to read tokens and set config. Each
|
||||
// virtualhost gets their own server config and dispenser.
|
||||
type Controller struct {
|
||||
*server.Config
|
||||
parse.Dispenser
|
||||
|
||||
// OncePerServerBlock is a function that executes f
|
||||
// exactly once per server block, no matter how many
|
||||
// hosts are associated with it. If it is the first
|
||||
// time, the function f is executed immediately
|
||||
// (not deferred) and may return an error which is
|
||||
// returned by OncePerServerBlock.
|
||||
OncePerServerBlock func(f func() error) error
|
||||
|
||||
// ServerBlockIndex is the 0-based index of the
|
||||
// server block as it appeared in the input.
|
||||
ServerBlockIndex int
|
||||
|
||||
// ServerBlockHostIndex is the 0-based index of this
|
||||
// host as it appeared in the input at the head of the
|
||||
// server block.
|
||||
ServerBlockHostIndex int
|
||||
|
||||
// ServerBlockHosts is a list of hosts that are
|
||||
// associated with this server block. All these
|
||||
// hosts, consequently, share the same tokens.
|
||||
ServerBlockHosts []string
|
||||
|
||||
// ServerBlockStorage is used by a directive's
|
||||
// setup function to persist state between all
|
||||
// the hosts on a server block.
|
||||
ServerBlockStorage interface{}
|
||||
}
|
||||
|
||||
// NewTestController creates a new *Controller for
|
||||
// the input specified, with a filename of "Testfile".
|
||||
// The Config is bare, consisting only of a Root of cwd.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience. Does not initialize
|
||||
// the server-block-related fields.
|
||||
func NewTestController(input string) *Controller {
|
||||
return &Controller{
|
||||
Config: &server.Config{
|
||||
Root: ".",
|
||||
},
|
||||
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
return f()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// middleware.Middleware functions so that the assignment
|
||||
// to the Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
func SameNext(next1, next2 middleware.Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
stdexpvar "expvar"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/expvar"
|
||||
)
|
||||
|
||||
// ExpVar configures a new ExpVar middleware instance.
|
||||
func ExpVar(c *Controller) (middleware.Middleware, error) {
|
||||
resource, err := expVarParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// publish any extra information/metrics we may want to capture
|
||||
publishExtraVars()
|
||||
|
||||
expvar := expvar.ExpVar{Resource: resource}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
expvar.Next = next
|
||||
return expvar
|
||||
}, nil
|
||||
}
|
||||
|
||||
func expVarParse(c *Controller) (expvar.Resource, error) {
|
||||
var resource expvar.Resource
|
||||
var err error
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
resource = expvar.Resource(defaultExpvarPath)
|
||||
case 1:
|
||||
resource = expvar.Resource(args[0])
|
||||
default:
|
||||
return resource, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
return resource, err
|
||||
}
|
||||
|
||||
func publishExtraVars() {
|
||||
// By using sync.Once instead of an init() function, we don't clutter
|
||||
// the app's expvar export unnecessarily, or risk colliding with it.
|
||||
publishOnce.Do(func() {
|
||||
stdexpvar.Publish("Goroutines", stdexpvar.Func(func() interface{} {
|
||||
return runtime.NumGoroutine()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
var publishOnce sync.Once // publishing variables should only be done once
|
||||
var defaultExpvarPath = "/debug/vars"
|
|
@ -1,39 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/expvar"
|
||||
)
|
||||
|
||||
func TestExpvar(t *testing.T) {
|
||||
c := NewTestController(`expvar`)
|
||||
mid, err := ExpVar(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
c = NewTestController(`expvar /d/v`)
|
||||
mid, err = ExpVar(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(expvar.ExpVar)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler)
|
||||
}
|
||||
if myHandler.Resource != "/d/v" {
|
||||
t.Errorf("Expected /d/v as expvar resource")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
// Ext configures a new instance of 'extensions' middleware for clean URLs.
|
||||
func Ext(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
exts, err := extParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return extensions.Ext{
|
||||
Next: next,
|
||||
Extensions: exts,
|
||||
Root: root,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extParse sets up an instance of extension middleware
|
||||
// from a middleware controller and returns a list of extensions.
|
||||
func extParse(c *Controller) ([]string, error) {
|
||||
var exts []string
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
if !c.NextArg() {
|
||||
return exts, c.ArgErr()
|
||||
}
|
||||
exts = append(exts, c.Val())
|
||||
|
||||
// Tack on any other extensions that may have been listed
|
||||
exts = append(exts, c.RemainingArgs()...)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
// resourceExists returns true if the file specified at
|
||||
// root + path exists; false otherwise.
|
||||
func resourceExists(root, path string) bool {
|
||||
_, err := os.Stat(filepath.Join(root, path))
|
||||
// technically we should use os.IsNotExist(err)
|
||||
// but we don't handle any other kinds of errors anyway
|
||||
return err == nil
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
// Internal configures a new Internal middleware instance.
|
||||
func Internal(c *Controller) (middleware.Middleware, error) {
|
||||
paths, err := internalParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return inner.Internal{Next: next, Paths: paths}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func internalParse(c *Controller) ([]string, error) {
|
||||
var paths []string
|
||||
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return paths, c.ArgErr()
|
||||
}
|
||||
paths = append(paths, c.Val())
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/pprof"
|
||||
)
|
||||
|
||||
//PProf returns a new instance of a pprof handler. It accepts no arguments or options.
|
||||
func PProf(c *Controller) (middleware.Middleware, error) {
|
||||
found := false
|
||||
for c.Next() {
|
||||
if found {
|
||||
return nil, c.Err("pprof can only be specified once")
|
||||
}
|
||||
if len(c.RemainingArgs()) != 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if c.NextBlock() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
found = true
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return &pprof.Handler{Next: next, Mux: pprof.NewMux()}
|
||||
}, nil
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/proxy"
|
||||
)
|
||||
|
||||
// Proxy configures a new Proxy middleware instance.
|
||||
func Proxy(c *Controller) (middleware.Middleware, error) {
|
||||
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return proxy.Proxy{Next: next, Upstreams: upstreams}
|
||||
}, nil
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// Root sets up the root file path of the server.
|
||||
func Root(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
c.Root = c.Val()
|
||||
}
|
||||
|
||||
// Check if root path exists
|
||||
_, err := os.Stat(c.Root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Allow this, because the folder might appear later.
|
||||
// But make sure the user knows!
|
||||
log.Printf("[WARNING] Root path does not exist: %s", c.Root)
|
||||
} else {
|
||||
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
1
caddy/setup/testdata/blog/first_post.md
vendored
1
caddy/setup/testdata/blog/first_post.md
vendored
|
@ -1 +0,0 @@
|
|||
# Test h1
|
10
caddy/setup/testdata/tpl_with_include.html
vendored
10
caddy/setup/testdata/tpl_with_include.html
vendored
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Doc.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{.Include "header.html"}}
|
||||
{{.Doc.body}}
|
||||
</body>
|
||||
</html>
|
58
caddy_test.go
Normal file
58
caddy_test.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package caddy
|
||||
|
||||
import "testing"
|
||||
|
||||
/*
|
||||
// TODO
|
||||
func TestCaddyStartStop(t *testing.T) {
|
||||
caddyfile := "localhost:1984"
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
_, err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting, iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(2 * time.Second),
|
||||
}
|
||||
resp, err := client.Get("http://localhost:1984")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Error stopping, iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func TestIsLoopback(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{"example.com", false},
|
||||
{"localhost", true},
|
||||
{"localhost:1234", true},
|
||||
{"localhost:", true},
|
||||
{"127.0.0.1", true},
|
||||
{"127.0.0.1:443", true},
|
||||
{"127.0.1.5", true},
|
||||
{"10.0.0.5", false},
|
||||
{"12.7.0.1", false},
|
||||
{"[::1]", true},
|
||||
{"[::1]:1234", true},
|
||||
{"::1", true},
|
||||
{"::", false},
|
||||
{"[::]", false},
|
||||
{"local", false},
|
||||
} {
|
||||
if got, want := IsLoopback(test.input), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -12,7 +12,7 @@ import (
|
|||
// some really convenient methods.
|
||||
type Dispenser struct {
|
||||
filename string
|
||||
tokens []token
|
||||
tokens []Token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func NewDispenser(filename string, input io.Reader) Dispenser {
|
|||
}
|
||||
|
||||
// NewDispenserTokens returns a Dispenser filled with the given tokens.
|
||||
func NewDispenserTokens(filename string, tokens []token) Dispenser {
|
||||
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
|
@ -59,8 +59,8 @@ func (d *Dispenser) NextArg() bool {
|
|||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].file == d.tokens[d.cursor+1].file &&
|
||||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line {
|
||||
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
|
@ -80,8 +80,8 @@ func (d *Dispenser) NextLine() bool {
|
|||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file ||
|
||||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) {
|
||||
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
|
||||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ func (d *Dispenser) Val() string {
|
|||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].text
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token. If there is no token
|
||||
|
@ -140,7 +140,7 @@ func (d *Dispenser) Line() int {
|
|||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].line
|
||||
return d.tokens[d.cursor].Line
|
||||
}
|
||||
|
||||
// File gets the filename of the current token. If there is no token loaded,
|
||||
|
@ -149,7 +149,7 @@ func (d *Dispenser) File() string {
|
|||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return d.filename
|
||||
}
|
||||
if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" {
|
||||
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
|
||||
return tokenFilename
|
||||
}
|
||||
return d.filename
|
||||
|
@ -233,7 +233,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
|||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].text, "\n")
|
||||
return strings.Count(d.tokens[tknIdx].Text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
|
@ -246,6 +246,6 @@ func (d *Dispenser) isNewLine() bool {
|
|||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file ||
|
||||
d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"reflect"
|
|
@ -4,31 +4,26 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
)
|
||||
|
||||
const filename = "Caddyfile"
|
||||
|
||||
// ToJSON converts caddyfile to its JSON representation.
|
||||
func ToJSON(caddyfile []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
var j EncodedCaddyfile
|
||||
|
||||
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
|
||||
serverBlocks, err := ServerBlocks(filename, bytes.NewReader(caddyfile), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
block := ServerBlock{Body: [][]interface{}{}}
|
||||
|
||||
// Fill up host list
|
||||
for _, host := range sb.HostList() {
|
||||
block.Hosts = append(block.Hosts, standardizeScheme(host))
|
||||
block := EncodedServerBlock{
|
||||
Keys: sb.Keys,
|
||||
Body: [][]interface{}{},
|
||||
}
|
||||
|
||||
// Extract directives deterministically by sorting them
|
||||
|
@ -40,7 +35,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
|
|||
|
||||
// Convert each directive's tokens into our JSON structure
|
||||
for _, dir := range directives {
|
||||
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
disp := NewDispenserTokens(filename, sb.Tokens[dir])
|
||||
for disp.Next() {
|
||||
block.Body = append(block.Body, constructLine(&disp))
|
||||
}
|
||||
|
@ -62,7 +57,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
|
|||
// but only one line at a time, to be used at the top-level of
|
||||
// a server block only (where the first token on each line is a
|
||||
// directive) - not to be used at any other nesting level.
|
||||
func constructLine(d *parse.Dispenser) []interface{} {
|
||||
func constructLine(d *Dispenser) []interface{} {
|
||||
var args []interface{}
|
||||
|
||||
args = append(args, d.Val())
|
||||
|
@ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} {
|
|||
// constructBlock recursively processes tokens into a
|
||||
// JSON-encodable structure. To be used in a directive's
|
||||
// block. Goes to end of block.
|
||||
func constructBlock(d *parse.Dispenser) [][]interface{} {
|
||||
func constructBlock(d *Dispenser) [][]interface{} {
|
||||
block := [][]interface{}{}
|
||||
|
||||
for d.Next() {
|
||||
|
@ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} {
|
|||
|
||||
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
|
||||
func FromJSON(jsonBytes []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
var j EncodedCaddyfile
|
||||
var result string
|
||||
|
||||
err := json.Unmarshal(jsonBytes, &j)
|
||||
|
@ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) {
|
|||
if sbPos > 0 {
|
||||
result += "\n\n"
|
||||
}
|
||||
for i, host := range sb.Hosts {
|
||||
for i, key := range sb.Keys {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += standardizeScheme(host)
|
||||
//result += standardizeScheme(key)
|
||||
result += key
|
||||
}
|
||||
result += jsonToText(sb.Body, 1)
|
||||
}
|
||||
|
@ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string {
|
|||
return result
|
||||
}
|
||||
|
||||
// TODO: Will this function come in handy somewhere else?
|
||||
/*
|
||||
// standardizeScheme turns an address like host:https into https://host,
|
||||
// or "host:" into "host".
|
||||
func standardizeScheme(addr string) string {
|
||||
|
@ -174,12 +172,13 @@ func standardizeScheme(addr string) string {
|
|||
}
|
||||
return strings.TrimSuffix(addr, ":")
|
||||
}
|
||||
*/
|
||||
|
||||
// Caddyfile encapsulates a slice of ServerBlocks.
|
||||
type Caddyfile []ServerBlock
|
||||
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
|
||||
type EncodedCaddyfile []EncodedServerBlock
|
||||
|
||||
// ServerBlock represents a server block.
|
||||
type ServerBlock struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
// EncodedServerBlock represents a server block ripe for encoding.
|
||||
type EncodedServerBlock struct {
|
||||
Keys []string `json:"keys"`
|
||||
Body [][]interface{} `json:"body"`
|
||||
}
|
|
@ -9,7 +9,7 @@ var tests = []struct {
|
|||
caddyfile: `foo {
|
||||
root /bar
|
||||
}`,
|
||||
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
|
||||
json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
|
||||
},
|
||||
{ // 1
|
||||
caddyfile: `host1, host2 {
|
||||
|
@ -17,7 +17,7 @@ var tests = []struct {
|
|||
def
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
|
||||
json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
|
||||
},
|
||||
{ // 2
|
||||
caddyfile: `host1, host2 {
|
||||
|
@ -26,58 +26,58 @@ var tests = []struct {
|
|||
jkl
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
|
||||
json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
|
||||
},
|
||||
{ // 3
|
||||
caddyfile: `host1:1234, host2:5678 {
|
||||
dir abc {
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
|
||||
json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
|
||||
},
|
||||
{ // 4
|
||||
caddyfile: `host {
|
||||
foo "bar baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
|
||||
},
|
||||
{ // 5
|
||||
caddyfile: `host, host:80 {
|
||||
foo "bar \"baz\""
|
||||
}`,
|
||||
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
|
||||
json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
|
||||
},
|
||||
{ // 6
|
||||
caddyfile: `host {
|
||||
foo "bar
|
||||
baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`,
|
||||
},
|
||||
{ // 7
|
||||
caddyfile: `host {
|
||||
dir 123 4.56 true
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
json: `[{"keys":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
},
|
||||
{ // 8
|
||||
caddyfile: `http://host, https://host {
|
||||
}`,
|
||||
json: `[{"hosts":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
json: `[{"keys":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
},
|
||||
{ // 9
|
||||
caddyfile: `host {
|
||||
dir1 a b
|
||||
dir2 c d
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
|
||||
},
|
||||
{ // 10
|
||||
caddyfile: `host {
|
||||
dir a b
|
||||
dir c d
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
|
||||
},
|
||||
{ // 11
|
||||
caddyfile: `host {
|
||||
|
@ -87,7 +87,7 @@ baz"
|
|||
d
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
|
||||
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
|
||||
},
|
||||
{ // 12
|
||||
caddyfile: `host1 {
|
||||
|
@ -97,7 +97,7 @@ baz"
|
|||
host2 {
|
||||
dir2
|
||||
}`,
|
||||
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
|
||||
json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -125,17 +125,19 @@ func TestFromJSON(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Will these tests come in handy somewhere else?
|
||||
/*
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
// host:https should be converted to https://host
|
||||
output, err := ToJSON([]byte(`host:https`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"hosts":["https://host"],"body":[]}]`, string(output); expected != actual {
|
||||
if expected, actual := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
|
||||
output, err = FromJSON([]byte(`[{"hosts":["https://host"],"body":[]}]`))
|
||||
output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected, actual := `[{"hosts":["host"],"body":[]}]`, string(output); expected != actual {
|
||||
if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual {
|
||||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
output, err = FromJSON([]byte(`[{"hosts":["host:"],"body":[]}]`))
|
||||
output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) {
|
|||
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -1,4 +1,4 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -13,15 +13,15 @@ type (
|
|||
// in quotes if it contains whitespace.
|
||||
lexer struct {
|
||||
reader *bufio.Reader
|
||||
token token
|
||||
token Token
|
||||
line int
|
||||
}
|
||||
|
||||
// token represents a single parsable unit.
|
||||
token struct {
|
||||
file string
|
||||
line int
|
||||
text string
|
||||
// Token represents a single parsable unit.
|
||||
Token struct {
|
||||
File string
|
||||
Line int
|
||||
Text string
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -47,7 +47,7 @@ func (l *lexer) next() bool {
|
|||
var comment, quoted, escaped bool
|
||||
|
||||
makeToken := func() bool {
|
||||
l.token.text = string(val)
|
||||
l.token.Text = string(val)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ func (l *lexer) next() bool {
|
|||
}
|
||||
|
||||
if len(val) == 0 {
|
||||
l.token = token{line: l.line}
|
||||
l.token = Token{Line: l.line}
|
||||
if ch == '"' {
|
||||
quoted = true
|
||||
continue
|
165
caddyfile/lexer_test.go
Normal file
165
caddyfile/lexer_test.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package caddyfile
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexerTestCase struct {
|
||||
input string
|
||||
expected []Token
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testCases := []lexerTestCase{
|
||||
{
|
||||
input: `host:123`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123
|
||||
|
||||
directive`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 3, Text: "directive"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
directive
|
||||
}`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 2, Text: "directive"},
|
||||
{Line: 3, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 { directive }`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 1, Text: "directive"},
|
||||
{Line: 1, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
#comment
|
||||
directive
|
||||
# comment
|
||||
foobar # another comment
|
||||
}`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "host:123"},
|
||||
{Line: 1, Text: "{"},
|
||||
{Line: 3, Text: "directive"},
|
||||
{Line: 5, Text: "foobar"},
|
||||
{Line: 6, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `a "quoted value" b
|
||||
foobar`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "a"},
|
||||
{Line: 1, Text: "quoted value"},
|
||||
{Line: 1, Text: "b"},
|
||||
{Line: 2, Text: "foobar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted \"value\" inside" B`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "A"},
|
||||
{Line: 1, Text: `quoted "value" inside`},
|
||||
{Line: 1, Text: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `don't\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\\escape"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
break inside" {
|
||||
foobar
|
||||
}`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "A"},
|
||||
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||
{Line: 2, Text: "{"},
|
||||
{Line: 3, Text: "foobar"},
|
||||
{Line: 4, Text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"C:\php\php-cgi.exe"`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `C:\php\php-cgi.exe`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `empty "" string`,
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `empty`},
|
||||
{Line: 1, Text: ``},
|
||||
{Line: 1, Text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "skip those\r\nCR characters",
|
||||
expected: []Token{
|
||||
{Line: 1, Text: "skip"},
|
||||
{Line: 1, Text: "those"},
|
||||
{Line: 2, Text: "CR"},
|
||||
{Line: 2, Text: "characters"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := tokenize(testCase.input)
|
||||
lexerCompare(t, i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(input string) (tokens []Token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||
}
|
||||
|
||||
for i := 0; i < len(actual) && i < len(expected); i++ {
|
||||
if actual[i].Line != expected[i].Line {
|
||||
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
||||
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
|
||||
break
|
||||
}
|
||||
if actual[i].Text != expected[i].Text {
|
||||
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
|
||||
n, i, expected[i].Text, actual[i].Text)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,41 @@
|
|||
package parse
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ServerBlocks parses the input just enough to group tokens,
|
||||
// in order, by server block. No further parsing is performed.
|
||||
// Server blocks are returned in the order in which they appear.
|
||||
// Directives that do not appear in validDirectives will cause
|
||||
// an error. If you do not want to check for valid directives,
|
||||
// pass in nil instead.
|
||||
func ServerBlocks(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []Token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
Dispenser
|
||||
block ServerBlock // current server block being parsed
|
||||
validDirectives []string // a directive must be valid or it's an error
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
checkDirectives bool // if true, directives must be known
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
|
@ -23,7 +46,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
|
|||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Addresses) > 0 {
|
||||
if len(p.block.Keys) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +55,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
|
|||
}
|
||||
|
||||
func (p *parser) parseOne() error {
|
||||
p.block = ServerBlock{Tokens: make(map[string][]token)}
|
||||
p.block = ServerBlock{Tokens: make(map[string][]Token)}
|
||||
|
||||
err := p.begin()
|
||||
if err != nil {
|
||||
|
@ -89,7 +112,7 @@ func (p *parser) addresses() error {
|
|||
break
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed "" in Caddyfile
|
||||
if tkn != "" { // empty token possible if user typed ""
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
if tkn[len(tkn)-1] == ',' {
|
||||
|
@ -99,13 +122,7 @@ func (p *parser) addresses() error {
|
|||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
// Parse and save this address
|
||||
addr, err := standardAddress(tkn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.block.Addresses = append(p.block.Addresses, addr)
|
||||
p.block.Keys = append(p.block.Keys, tkn)
|
||||
}
|
||||
|
||||
// Advance token and possibly break out of loop or return error
|
||||
|
@ -207,7 +224,7 @@ func (p *parser) doImport() error {
|
|||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
|
||||
// collect all the imported tokens
|
||||
var importedTokens []token
|
||||
var importedTokens []Token
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
|
@ -226,7 +243,7 @@ func (p *parser) doImport() error {
|
|||
|
||||
// doSingleImport lexes the individual file at importFile and returns
|
||||
// its tokens or an error, if any.
|
||||
func (p *parser) doSingleImport(importFile string) ([]token, error) {
|
||||
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
file, err := os.Open(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not import %s: %v", importFile, err)
|
||||
|
@ -237,7 +254,7 @@ func (p *parser) doSingleImport(importFile string) ([]token, error) {
|
|||
// Tack the filename onto these tokens so errors show the imported file's name
|
||||
filename := filepath.Base(importFile)
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].file = filename
|
||||
importedTokens[i].File = filename
|
||||
}
|
||||
|
||||
return importedTokens, nil
|
||||
|
@ -253,10 +270,9 @@ func (p *parser) directive() error {
|
|||
dir := p.Val()
|
||||
nesting := 0
|
||||
|
||||
if p.checkDirectives {
|
||||
if _, ok := ValidDirectives[dir]; !ok {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
|
||||
if !p.validDirective(dir) {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
|
@ -273,7 +289,7 @@ func (p *parser) directive() error {
|
|||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
}
|
||||
p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text)
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
}
|
||||
|
||||
|
@ -305,63 +321,17 @@ func (p *parser) closeCurlyBrace() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// standardAddress parses an address string into a structured format with separate
|
||||
// scheme, host, and port portions, as well as the original input string.
|
||||
func standardAddress(str string) (address, error) {
|
||||
var scheme string
|
||||
var err error
|
||||
|
||||
// first check for scheme and strip it off
|
||||
input := str
|
||||
if strings.HasPrefix(str, "https://") {
|
||||
scheme = "https"
|
||||
str = str[8:]
|
||||
} else if strings.HasPrefix(str, "http://") {
|
||||
scheme = "http"
|
||||
str = str[7:]
|
||||
// validDirective returns true if dir is in p.validDirectives.
|
||||
func (p *parser) validDirective(dir string) bool {
|
||||
if p.validDirectives == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// separate host and port
|
||||
host, port, err := net.SplitHostPort(str)
|
||||
if err != nil {
|
||||
host, port, err = net.SplitHostPort(str + ":")
|
||||
if err != nil {
|
||||
host = str
|
||||
for _, d := range p.validDirectives {
|
||||
if d == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// "The host subcomponent is case-insensitive." (RFC 3986)
|
||||
host = strings.ToLower(host)
|
||||
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
} else if scheme == "https" {
|
||||
port = "443"
|
||||
}
|
||||
}
|
||||
|
||||
// repeated or conflicting scheme is confusing, so error
|
||||
if scheme != "" && (port == "http" || port == "https") {
|
||||
return address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (scheme == "http" && port == "443") || (scheme == "https" && port == "80") {
|
||||
return address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
// standardize http and https ports to their respective port numbers
|
||||
if port == "http" {
|
||||
scheme = "http"
|
||||
port = "80"
|
||||
} else if port == "https" {
|
||||
scheme = "https"
|
||||
port = "443"
|
||||
}
|
||||
|
||||
return address{Original: input, Scheme: scheme, Host: host, Port: port}, err
|
||||
return false
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces environment variables that appear in the token
|
||||
|
@ -389,27 +359,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string {
|
|||
return s
|
||||
}
|
||||
|
||||
type (
|
||||
// ServerBlock associates tokens with a list of addresses
|
||||
// and groups tokens by directive name.
|
||||
ServerBlock struct {
|
||||
Addresses []address
|
||||
Tokens map[string][]token
|
||||
}
|
||||
|
||||
address struct {
|
||||
Original, Scheme, Host, Port string
|
||||
}
|
||||
)
|
||||
|
||||
// HostList converts the list of addresses that are
|
||||
// associated with this server block into a slice of
|
||||
// strings, where each address is as it was originally
|
||||
// read from the input.
|
||||
func (sb ServerBlock) HostList() []string {
|
||||
sbHosts := make([]string, len(sb.Addresses))
|
||||
for j, addr := range sb.Addresses {
|
||||
sbHosts[j] = addr.Original
|
||||
}
|
||||
return sbHosts
|
||||
// ServerBlock associates any number of keys (usually addresses
|
||||
// of some sort) with tokens (grouped by directive name).
|
||||
type ServerBlock struct {
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
399
caddyfile/parse_test.go
Normal file
399
caddyfile/parse_test.go
Normal file
|
@ -0,0 +1,399 @@
|
|||
package caddyfile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].Text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(t *testing.T) {
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys []string
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []string{
|
||||
"localhost:1234",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []string{
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []string{
|
||||
"http://localhost",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []string{
|
||||
"host1:80",
|
||||
"http://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []string{
|
||||
"http://host1.com",
|
||||
"http://host2.com",
|
||||
"https://host3.com",
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []string{
|
||||
"http://host1.com:1234",
|
||||
"https://host2.com",
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []string{
|
||||
"127.0.0.1",
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import testdata/import_test1.txt`, false, []string{
|
||||
"localhost",
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test2.txt`, false, []string{
|
||||
"host1",
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
|
||||
|
||||
{`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []string{}, map[string]int{}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Keys) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d keys, got %d",
|
||||
i, len(test.keys), len(result.Keys))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Keys {
|
||||
if addr != test.keys[j] {
|
||||
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
|
||||
i, j, test.keys[j], addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
keys [][]string // keys per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]string{
|
||||
{"localhost"},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]string{
|
||||
{"localhost:1234"},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]string{
|
||||
{"localhost:1234"},
|
||||
{"localhost:2015"},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]string{
|
||||
{"localhost:1234", "http://host2"},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]string{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]string{
|
||||
{"http://host1.com", "http://host2.com"},
|
||||
{"https://host3.com", "https://host4.com"},
|
||||
}},
|
||||
|
||||
{`import testdata/import_glob*.txt`, false, [][]string{
|
||||
{"glob0.host0"},
|
||||
{"glob0.host1"},
|
||||
{"glob1.host0"},
|
||||
{"glob2.host0"},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.keys) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.keys), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Keys) != len(test.keys[j]) {
|
||||
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
|
||||
i, len(test.keys[j]), j, len(block.Keys))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Keys {
|
||||
if addr != test.keys[j][k] {
|
||||
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
|
||||
i, j, k, test.keys[j][k], addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentReplacement(t *testing.T) {
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
blocks, _ := p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// windows-style var and unix style in same token
|
||||
p = testParser(`{%ADDRESS%}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// reverse order
|
||||
p = testParser(`{$ADDRESS}:{%PORT%}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// env var in server block body as argument
|
||||
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// combined windows env vars in argument
|
||||
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed env var (windows)
|
||||
p = testParser(":1234\ndir1 {%ADDRESS}")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
|
||||
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// malformed (non-existent) env var (unix)
|
||||
p = testParser(`:{$PORT$}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// in quoted field
|
||||
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Test", buf)}
|
||||
return p
|
||||
}
|
|
@ -13,7 +13,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// BasicAuth is middleware to protect resources with a username and password.
|
||||
|
@ -22,12 +22,12 @@ import (
|
|||
// security of HTTP Basic Auth is disputed. Use discretion when deciding
|
||||
// what to protect with BasicAuth.
|
||||
type BasicAuth struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
SiteRoot string
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
var hasAuth bool
|
||||
|
@ -35,7 +35,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
|||
|
||||
for _, rule := range a.Rules {
|
||||
for _, res := range rule.Resources {
|
||||
if !middleware.Path(r.URL.Path).Matches(res) {
|
||||
if !httpserver.Path(r.URL.Path).Matches(res) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -10,13 +10,12 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
|
||||
rw := BasicAuth{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}},
|
||||
},
|
||||
|
@ -67,7 +66,7 @@ func TestBasicAuth(t *testing.T) {
|
|||
|
||||
func TestMultipleOverlappingRules(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
|
||||
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
|
|
@ -1,43 +1,55 @@
|
|||
package setup
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// BasicAuth configures a new BasicAuth middleware instance.
|
||||
func BasicAuth(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "basicauth",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new BasicAuth middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
root := cfg.Root
|
||||
|
||||
rules, err := basicAuthParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
basic := basicauth.BasicAuth{Rules: rules}
|
||||
basic := BasicAuth{Rules: rules}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
basic.Next = next
|
||||
basic.SiteRoot = root
|
||||
return basic
|
||||
}, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
var rules []basicauth.Rule
|
||||
func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
|
||||
var err error
|
||||
for c.Next() {
|
||||
var rule basicauth.Rule
|
||||
var rule Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule.Username = args[0]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
|
||||
|
@ -50,7 +62,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
|||
case 3:
|
||||
rule.Resources = append(rule.Resources, args[0])
|
||||
rule.Username = args[1]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
default:
|
||||
|
@ -63,10 +75,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
|||
return rules, nil
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
|
||||
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
return basicauth.PlainMatcher(passw), nil
|
||||
return PlainMatcher(passw), nil
|
||||
}
|
||||
|
||||
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package setup
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -7,27 +7,27 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
c := NewTestController(`basicauth user pwd`)
|
||||
|
||||
mid, err := BasicAuth(c)
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`basicauth user pwd`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(basicauth.BasicAuth)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(BasicAuth)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
@ -54,41 +54,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
|||
input string
|
||||
shouldErr bool
|
||||
password string
|
||||
expected []basicauth.Rule
|
||||
expected []Rule
|
||||
}{
|
||||
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{`basicauth user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
}`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{`basicauth /resource user pwd`, false, "pwd", []Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
{`basicauth /res1 user1 pwd1
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []Rule{
|
||||
{Username: "user1", Resources: []string{"/res1"}},
|
||||
{Username: "user2", Resources: []string{"/res2"}},
|
||||
}},
|
||||
{`basicauth user`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth user`, true, "", []Rule{}},
|
||||
{`basicauth`, true, "", []Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []Rule{}},
|
||||
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
|
||||
{Username: "sha1"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := basicAuthParse(c)
|
||||
actual, err := basicAuthParse(caddy.NewTestController(test.input))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
25
caddyhttp/bind/bind.go
Normal file
25
caddyhttp/bind/bind.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "bind",
|
||||
ServerType: "http",
|
||||
Action: setupBind,
|
||||
})
|
||||
}
|
||||
|
||||
func setupBind(c *caddy.Controller) error {
|
||||
config := httpserver.GetConfig(c.Key)
|
||||
for c.Next() {
|
||||
if !c.Args(&config.ListenHost) {
|
||||
return c.ArgErr()
|
||||
}
|
||||
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
}
|
||||
return nil
|
||||
}
|
23
caddyhttp/bind/bind_test.go
Normal file
23
caddyhttp/bind/bind_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package bind
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetupBind(t *testing.T) {
|
||||
err := setupBind(caddy.NewTestController(`bind 1.2.3.4`))
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
|
||||
cfg := httpserver.GetConfig("")
|
||||
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
|
@ -16,13 +16,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
// Browse is an http.Handler that can show a file listing when
|
||||
// directories in the given paths are specified.
|
||||
type Browse struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Configs []Config
|
||||
IgnoreIndexes bool
|
||||
}
|
||||
|
@ -67,7 +68,7 @@ type Listing struct {
|
|||
// Optional custom variables for use in browse templates
|
||||
User interface{}
|
||||
|
||||
middleware.Context
|
||||
httpserver.Context
|
||||
}
|
||||
|
||||
// BreadcrumbMap returns l.Path where every element is a map
|
||||
|
@ -195,7 +196,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listin
|
|||
for _, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
for _, indexName := range middleware.IndexPages {
|
||||
for _, indexName := range staticfiles.IndexPages {
|
||||
if name == indexName {
|
||||
hasIndexFile = true
|
||||
break
|
||||
|
@ -237,7 +238,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|||
var bc *Config
|
||||
// See if there's a browse configuration to match the path
|
||||
for i := range b.Configs {
|
||||
if middleware.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
|
||||
if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
|
||||
bc = &b.Configs[i]
|
||||
goto inScope
|
||||
}
|
||||
|
@ -370,7 +371,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
|
|||
if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
listing.Context = middleware.Context{
|
||||
listing.Context = httpserver.Context{
|
||||
Root: bc.Root,
|
||||
Req: r,
|
||||
URL: r.URL,
|
|
@ -12,20 +12,9 @@ import (
|
|||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// "sort" package has "IsSorted" function, but no "IsReversed";
|
||||
func isReversed(data sort.Interface) bool {
|
||||
n := data.Len()
|
||||
for i := n - 1; i > 0; i-- {
|
||||
if !data.Less(i, i-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
// making up []fileInfo with bogus values;
|
||||
// to be used to make up our "listing"
|
||||
|
@ -111,7 +100,7 @@ func TestBrowseHTTPMethods(t *testing.T) {
|
|||
}
|
||||
|
||||
b := Browse{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
|
||||
}),
|
||||
Configs: []Config{
|
||||
|
@ -149,7 +138,7 @@ func TestBrowseTemplate(t *testing.T) {
|
|||
}
|
||||
|
||||
b := Browse{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
}),
|
||||
|
@ -202,9 +191,8 @@ func TestBrowseTemplate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBrowseJson(t *testing.T) {
|
||||
|
||||
b := Browse{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
}),
|
||||
|
@ -354,3 +342,14 @@ func TestBrowseJson(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "sort" package has "IsSorted" function, but no "IsReversed";
|
||||
func isReversed(data sort.Interface) bool {
|
||||
n := data.Len()
|
||||
for i := n - 1; i > 0; i-- {
|
||||
if !data.Less(i, i-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package setup
|
||||
package browse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,32 +6,44 @@ import (
|
|||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Browse configures a new Browse middleware instance.
|
||||
func Browse(c *Controller) (middleware.Middleware, error) {
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "browse",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new Browse middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := browseParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
browse := browse.Browse{
|
||||
b := Browse{
|
||||
Configs: configs,
|
||||
IgnoreIndexes: false,
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
browse.Next = next
|
||||
return browse
|
||||
}, nil
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
b.Next = next
|
||||
return b
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func browseParse(c *Controller) ([]browse.Config, error) {
|
||||
var configs []browse.Config
|
||||
func browseParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
appendCfg := func(bc browse.Config) error {
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
|
||||
appendCfg := func(bc Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == bc.PathScope {
|
||||
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
|
||||
|
@ -42,7 +54,7 @@ func browseParse(c *Controller) ([]browse.Config, error) {
|
|||
}
|
||||
|
||||
for c.Next() {
|
||||
var bc browse.Config
|
||||
var bc Config
|
||||
|
||||
// First argument is directory to allow browsing; default is site root
|
||||
if c.NextArg() {
|
||||
|
@ -50,7 +62,7 @@ func browseParse(c *Controller) ([]browse.Config, error) {
|
|||
} else {
|
||||
bc.PathScope = "/"
|
||||
}
|
||||
bc.Root = http.Dir(c.Root)
|
||||
bc.Root = http.Dir(cfg.Root)
|
||||
theRoot, err := bc.Root.Open("/") // catch a missing path early
|
||||
if err != nil {
|
||||
return configs, err
|
|
@ -1,4 +1,4 @@
|
|||
package setup
|
||||
package browse
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
@ -8,12 +8,13 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBrowse(t *testing.T) {
|
||||
|
||||
tempDirPath, err := getTempDirPath()
|
||||
func TestSetup(t *testing.T) {
|
||||
tempDirPath := os.TempDir()
|
||||
_, err := os.Stat(tempDirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
|
@ -35,7 +36,7 @@ func TestBrowse(t *testing.T) {
|
|||
// test case #0 tests handling of multiple pathscopes
|
||||
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
|
||||
|
||||
// test case #1 tests instantiation of browse.Config with default values
|
||||
// test case #1 tests instantiation of Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detectaction of custom template
|
||||
|
@ -48,14 +49,16 @@ func TestBrowse(t *testing.T) {
|
|||
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
|
||||
} {
|
||||
|
||||
recievedFunc, err := Browse(NewTestController(test.input))
|
||||
err := setup(caddy.NewTestController(test.input))
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test case #%d recieved an error of %v", i, err)
|
||||
}
|
||||
if test.expectedPathScope == nil {
|
||||
continue
|
||||
}
|
||||
recievedConfigs := recievedFunc(nil).(browse.Browse).Configs
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
mid := mids[len(mids)-1]
|
||||
recievedConfigs := mid(nil).(Browse).Configs
|
||||
for j, config := range recievedConfigs {
|
||||
if config.PathScope != test.expectedPathScope[j] {
|
||||
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
|
29
caddyhttp/caddyhttp.go
Normal file
29
caddyhttp/caddyhttp.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package caddyhttp
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
|
||||
// plug in the standard directives
|
||||
_ "github.com/mholt/caddy/caddyhttp/basicauth"
|
||||
_ "github.com/mholt/caddy/caddyhttp/bind"
|
||||
_ "github.com/mholt/caddy/caddyhttp/browse"
|
||||
_ "github.com/mholt/caddy/caddyhttp/errors"
|
||||
_ "github.com/mholt/caddy/caddyhttp/expvar"
|
||||
_ "github.com/mholt/caddy/caddyhttp/extensions"
|
||||
_ "github.com/mholt/caddy/caddyhttp/fastcgi"
|
||||
_ "github.com/mholt/caddy/caddyhttp/gzip"
|
||||
_ "github.com/mholt/caddy/caddyhttp/header"
|
||||
_ "github.com/mholt/caddy/caddyhttp/internalsrv"
|
||||
_ "github.com/mholt/caddy/caddyhttp/log"
|
||||
_ "github.com/mholt/caddy/caddyhttp/markdown"
|
||||
_ "github.com/mholt/caddy/caddyhttp/mime"
|
||||
_ "github.com/mholt/caddy/caddyhttp/pprof"
|
||||
_ "github.com/mholt/caddy/caddyhttp/proxy"
|
||||
_ "github.com/mholt/caddy/caddyhttp/redirect"
|
||||
_ "github.com/mholt/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/mholt/caddy/caddyhttp/root"
|
||||
_ "github.com/mholt/caddy/caddyhttp/templates"
|
||||
_ "github.com/mholt/caddy/caddyhttp/websocket"
|
||||
_ "github.com/mholt/caddy/startupshutdown"
|
||||
)
|
|
@ -11,16 +11,25 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "errors",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorHandler handles HTTP errors (and errors from other middleware).
|
||||
type ErrorHandler struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
ErrorPages map[int]string // map of status code to filename
|
||||
LogFile string
|
||||
Log *log.Logger
|
||||
LogRoller *middleware.LogRoller
|
||||
LogRoller *httpserver.LogRoller
|
||||
Debug bool // if true, errors are written out to client rather than to a log
|
||||
}
|
||||
|
||||
|
@ -31,13 +40,12 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
|||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
|
||||
|
||||
if h.Debug {
|
||||
// Write error to response instead of to log
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprintln(w, errMsg)
|
||||
return 0, err // returning < 400 signals that a response has been written
|
||||
return 0, err // returning 0 signals that a response has been written
|
||||
}
|
||||
h.Log.Println(errMsg)
|
||||
}
|
||||
|
@ -54,18 +62,15 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
|||
// code. If there is an error serving the error page, a plaintext error
|
||||
// message is written instead, and the extra error is logged.
|
||||
func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) {
|
||||
defaultBody := fmt.Sprintf("%d %s", code, http.StatusText(code))
|
||||
|
||||
// See if an error page for this status code was specified
|
||||
if pagePath, ok := h.ErrorPages[code]; ok {
|
||||
|
||||
// Try to open it
|
||||
errorPage, err := os.Open(pagePath)
|
||||
if err != nil {
|
||||
// An additional error handling an error... <insert grumpy cat here>
|
||||
h.Log.Printf("%s [NOTICE %d %s] could not load error page: %v",
|
||||
time.Now().Format(timeFormat), code, r.URL.String(), err)
|
||||
http.Error(w, defaultBody, code)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
return
|
||||
}
|
||||
defer errorPage.Close()
|
||||
|
@ -79,14 +84,14 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
|
|||
// Epic fail... sigh.
|
||||
h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v",
|
||||
time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err)
|
||||
http.Error(w, defaultBody, code)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Default error response
|
||||
http.Error(w, defaultBody, code)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -125,9 +130,7 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
|||
// Write error and stack trace to the response rather than to a log
|
||||
var stackBuf [4096]byte
|
||||
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "%s\n\n%s", panicMsg, stack)
|
||||
httpserver.WriteTextResponse(w, http.StatusInternalServerError, fmt.Sprintf("%s\n\n%s", panicMsg, stack))
|
||||
} else {
|
||||
// Currently we don't use the function name, since file:line is more conventional
|
||||
h.Log.Printf(panicMsg)
|
|
@ -13,7 +13,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
|
@ -44,7 +44,7 @@ func TestErrors(t *testing.T) {
|
|||
|
||||
testErr := errors.New("test error")
|
||||
tests := []struct {
|
||||
next middleware.Handler
|
||||
next httpserver.Handler
|
||||
expectedCode int
|
||||
expectedBody string
|
||||
expectedLog string
|
||||
|
@ -124,7 +124,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
|||
eh := ErrorHandler{
|
||||
ErrorPages: make(map[int]string),
|
||||
Debug: true,
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
panic(panicMsg)
|
||||
}),
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
|||
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, "[PANIC /] middleware/errors/errors_test.go") {
|
||||
if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") {
|
||||
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, panicMsg) {
|
||||
|
@ -157,8 +157,8 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func genErrorHandler(status int, err error, body string) middleware.Handler {
|
||||
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
func genErrorHandler(status int, err error, body string) httpserver.Handler {
|
||||
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if len(body) > 0 {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
fmt.Fprint(w, body)
|
|
@ -1,4 +1,4 @@
|
|||
package setup
|
||||
package errors
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
@ -8,19 +8,19 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Errors configures a new errors middleware instance.
|
||||
func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
// setup configures a new errors middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
handler, err := errorsParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the log file for writing when the server starts
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
c.OnStartup(func() error {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
|
@ -62,17 +62,21 @@ func Errors(c *Controller) (middleware.Middleware, error) {
|
|||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
}, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
// Very important that we make a pointer because the Startup
|
||||
func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
|
||||
// Very important that we make a pointer because the startup
|
||||
// function that opens the log file must have access to the
|
||||
// same instance of the handler, not a copy.
|
||||
handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
handler := &ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
|
@ -94,7 +98,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
|||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
c.IncrNest()
|
||||
logRoller, err := parseRoller(c)
|
||||
logRoller, err := httpserver.ParseRoller(c)
|
||||
if err != nil {
|
||||
return hadBlock, err
|
||||
}
|
||||
|
@ -104,7 +108,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
|||
}
|
||||
} else {
|
||||
// Error page; ensure it exists
|
||||
where = filepath.Join(c.Root, where)
|
||||
where = filepath.Join(cfg.Root, where)
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
|
|
@ -1,27 +1,24 @@
|
|||
package setup
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
c := NewTestController(`errors`)
|
||||
mid, err := Errors(c)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`errors`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middlewares, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(*errors.ErrorHandler)
|
||||
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(*ErrorHandler)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
|
@ -32,53 +29,53 @@ func TestErrors(t *testing.T) {
|
|||
if myHandler.LogRoller != nil {
|
||||
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
// Test Startup function
|
||||
if len(c.Startup) == 0 {
|
||||
t.Fatal("Expected 1 startup function, had 0")
|
||||
}
|
||||
c.Startup[0]()
|
||||
if myHandler.Log == nil {
|
||||
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
}
|
||||
// Test Startup function -- TODO
|
||||
// if len(c.Startup) == 0 {
|
||||
// t.Fatal("Expected 1 startup function, had 0")
|
||||
// }
|
||||
// c.Startup[0]()
|
||||
// if myHandler.Log == nil {
|
||||
// t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
// }
|
||||
}
|
||||
|
||||
func TestErrorsParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler errors.ErrorHandler
|
||||
expectedErrorHandler ErrorHandler
|
||||
}{
|
||||
{`errors`, false, errors.ErrorHandler{
|
||||
{`errors`, false, ErrorHandler{
|
||||
LogFile: "",
|
||||
}},
|
||||
{`errors errors.txt`, false, errors.ErrorHandler{
|
||||
{`errors errors.txt`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
}},
|
||||
{`errors visible`, false, errors.ErrorHandler{
|
||||
{`errors visible`, false, ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log visible }`, false, errors.ErrorHandler{
|
||||
{`errors { log visible }`, false, ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
}`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
500: "500.html",
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
LogRoller: &middleware.LogRoller{
|
||||
LogRoller: &httpserver.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
|
@ -92,13 +89,13 @@ func TestErrorsParse(t *testing.T) {
|
|||
}
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
}`, false, ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
LogRoller: &middleware.LogRoller{
|
||||
LogRoller: &httpserver.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
|
@ -107,8 +104,7 @@ func TestErrorsParse(t *testing.T) {
|
|||
}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputErrorsRules)
|
||||
actualErrorsRule, err := errorsParse(c)
|
||||
actualErrorsRule, err := errorsParse(caddy.NewTestController(test.inputErrorsRules))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
|
@ -5,23 +5,22 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// ExpVar is a simple struct to hold expvar's configuration
|
||||
type ExpVar struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Resource Resource
|
||||
}
|
||||
|
||||
// ServeHTTP handles requests to expvar's configured entry point with
|
||||
// expvar, or passes all other requests up the chain.
|
||||
func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if middleware.Path(r.URL.Path).Matches(string(e.Resource)) {
|
||||
if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) {
|
||||
expvarHandler(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return e.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
|
@ -6,12 +6,12 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestExpVar(t *testing.T) {
|
||||
rw := ExpVar{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Resource: "/d/v",
|
||||
}
|
||||
|
70
caddyhttp/expvar/setup.go
Normal file
70
caddyhttp/expvar/setup.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package expvar
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "expvar",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new ExpVar middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
resource, err := expVarParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// publish any extra information/metrics we may want to capture
|
||||
publishExtraVars()
|
||||
|
||||
ev := ExpVar{Resource: resource}
|
||||
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
ev.Next = next
|
||||
return ev
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expVarParse(c *caddy.Controller) (Resource, error) {
|
||||
var resource Resource
|
||||
var err error
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 0:
|
||||
resource = Resource(defaultExpvarPath)
|
||||
case 1:
|
||||
resource = Resource(args[0])
|
||||
default:
|
||||
return resource, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
return resource, err
|
||||
}
|
||||
|
||||
func publishExtraVars() {
|
||||
// By using sync.Once instead of an init() function, we don't clutter
|
||||
// the app's expvar export unnecessarily, or risk colliding with it.
|
||||
publishOnce.Do(func() {
|
||||
expvar.Publish("Goroutines", expvar.Func(func() interface{} {
|
||||
return runtime.NumGoroutine()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
var publishOnce sync.Once // publishing variables should only be done once
|
||||
var defaultExpvarPath = "/debug/vars"
|
40
caddyhttp/expvar/setup_test.go
Normal file
40
caddyhttp/expvar/setup_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package expvar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`expvar`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
err = setup(caddy.NewTestController(`expvar /d/v`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
mids = httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mids[1](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(ExpVar)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler)
|
||||
}
|
||||
if myHandler.Resource != "/d/v" {
|
||||
t.Errorf("Expected /d/v as expvar resource")
|
||||
}
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
|
@ -12,14 +12,14 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Ext can assume an extension from clean URLs.
|
||||
// It tries extensions in the order listed in Extensions.
|
||||
type Ext struct {
|
||||
// Next handler in the chain
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
|
||||
// Path to ther root of the site
|
||||
Root string
|
||||
|
@ -28,7 +28,7 @@ type Ext struct {
|
|||
Extensions []string
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
// ServeHTTP implements the httpserver.Handler interface.
|
||||
func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
urlpath := strings.TrimSuffix(r.URL.Path, "/")
|
||||
if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' {
|
54
caddyhttp/extensions/setup.go
Normal file
54
caddyhttp/extensions/setup.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "ext",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new instance of 'extensions' middleware for clean URLs.
|
||||
func setup(c *caddy.Controller) error {
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
root := cfg.Root
|
||||
|
||||
exts, err := extParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Ext{
|
||||
Next: next,
|
||||
Extensions: exts,
|
||||
Root: root,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extParse sets up an instance of extension middleware
|
||||
// from a middleware controller and returns a list of extensions.
|
||||
func extParse(c *caddy.Controller) ([]string, error) {
|
||||
var exts []string
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
if !c.NextArg() {
|
||||
return exts, c.ArgErr()
|
||||
}
|
||||
exts = append(exts, c.Val())
|
||||
|
||||
// Tack on any other extensions that may have been listed
|
||||
exts = append(exts, c.RemainingArgs()...)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
|
@ -1,26 +1,25 @@
|
|||
package setup
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
c := NewTestController(`ext .html .htm .php`)
|
||||
|
||||
mid, err := Ext(c)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`ext .html .htm .php`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
t.Fatalf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, had 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(extensions.Ext)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Ext)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
|
||||
|
@ -35,7 +34,7 @@ func TestExt(t *testing.T) {
|
|||
if myHandler.Extensions[2] != ".php" {
|
||||
t.Errorf("Expected .php in the list of Extensions")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
|
@ -52,8 +51,7 @@ func TestExtParse(t *testing.T) {
|
|||
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputExts)
|
||||
actualExts, err := extParse(c)
|
||||
actualExts, err := extParse(caddy.NewTestController(test.inputExts))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
14
middleware/fastcgi/fastcgi.go → caddyhttp/fastcgi/fastcgi.go
Executable file → Normal file
14
middleware/fastcgi/fastcgi.go → caddyhttp/fastcgi/fastcgi.go
Executable file → Normal file
|
@ -13,12 +13,12 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Handler is a middleware type that can handle requests as a FastCGI client.
|
||||
type Handler struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Rules []Rule
|
||||
Root string
|
||||
AbsRoot string // same as root, but absolute path
|
||||
|
@ -31,12 +31,12 @@ type Handler struct {
|
|||
ServerPort string
|
||||
}
|
||||
|
||||
// ServeHTTP satisfies the middleware.Handler interface.
|
||||
// ServeHTTP satisfies the httpserver.Handler interface.
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for _, rule := range h.Rules {
|
||||
|
||||
// First requirement: Base path must match and the path must be allowed.
|
||||
if !middleware.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) {
|
||||
if !httpserver.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
|||
|
||||
fpath := r.URL.Path
|
||||
|
||||
if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
|
||||
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
|
||||
fpath = idx
|
||||
// Index file present.
|
||||
// If request path cannot be split, return error.
|
||||
|
@ -305,7 +305,7 @@ func (r Rule) canSplit(path string) bool {
|
|||
// splitPos returns the index where path should be split
|
||||
// based on rule.SplitPath.
|
||||
func (r Rule) splitPos(path string) int {
|
||||
if middleware.CaseSensitivePath {
|
||||
if httpserver.CaseSensitivePath {
|
||||
return strings.Index(path, r.SplitPath)
|
||||
}
|
||||
return strings.Index(strings.ToLower(path), strings.ToLower(r.SplitPath))
|
||||
|
@ -314,7 +314,7 @@ func (r Rule) splitPos(path string) int {
|
|||
// AllowedPath checks if requestPath is not an ignored path.
|
||||
func (r Rule) AllowedPath(requestPath string) bool {
|
||||
for _, ignoredSubPath := range r.IgnoredSubPaths {
|
||||
if middleware.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
|
||||
if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,46 +1,57 @@
|
|||
package setup
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// FastCGI configures a new FastCGI middleware instance.
|
||||
func FastCGI(c *Controller) (middleware.Middleware, error) {
|
||||
absRoot, err := filepath.Abs(c.Root)
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "fastcgi",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// setup configures a new FastCGI middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
absRoot, err := filepath.Abs(cfg.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
rules, err := fastcgiParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return fastcgi.Handler{
|
||||
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Handler{
|
||||
Next: next,
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
Root: cfg.Root,
|
||||
AbsRoot: absRoot,
|
||||
FileSys: http.Dir(c.Root),
|
||||
SoftwareName: c.AppName,
|
||||
SoftwareVersion: c.AppVersion,
|
||||
ServerName: c.Host,
|
||||
ServerPort: c.Port,
|
||||
FileSys: http.Dir(cfg.Root),
|
||||
SoftwareName: caddy.AppName,
|
||||
SoftwareVersion: caddy.AppVersion,
|
||||
ServerName: cfg.Addr.Host,
|
||||
ServerPort: cfg.Addr.Port,
|
||||
}
|
||||
}, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
var rules []fastcgi.Rule
|
||||
func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule fastcgi.Rule
|
||||
var rule Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
|
@ -103,7 +114,7 @@ func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
|||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
|
||||
func fastcgiPreset(name string, rule *Rule) error {
|
||||
switch name {
|
||||
case "php":
|
||||
rule.Ext = ".php"
|
|
@ -1,28 +1,25 @@
|
|||
package setup
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestFastCGI(t *testing.T) {
|
||||
|
||||
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
|
||||
|
||||
mid, err := FastCGI(c)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`fastcgi / 127.0.0.1:9000`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, got 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(fastcgi.Handler)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Handler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type , got: %#v", handler)
|
||||
|
@ -41,11 +38,11 @@ func TestFastcgiParse(t *testing.T) {
|
|||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []fastcgi.Rule
|
||||
expectedFastcgiConfig []Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []fastcgi.Rule{{
|
||||
false, []Rule{{
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
|
@ -55,7 +52,7 @@ func TestFastcgiParse(t *testing.T) {
|
|||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
|
@ -66,7 +63,7 @@ func TestFastcgiParse(t *testing.T) {
|
|||
split .html
|
||||
except /admin /user
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
|
@ -76,8 +73,7 @@ func TestFastcgiParse(t *testing.T) {
|
|||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputFastcgiConfig)
|
||||
actualFastcgiConfigs, err := fastcgiParse(c)
|
||||
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController(test.inputFastcgiConfig))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
|
@ -1,4 +1,4 @@
|
|||
// Package gzip provides a simple middleware layer that performs
|
||||
// Package gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
|
@ -12,15 +12,24 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "gzip",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// Gzip is a middleware type which gzips HTTP responses. It is
|
||||
// imperative that any handler which writes to a gzipped response
|
||||
// specifies the Content-Type, otherwise some clients will assume
|
||||
// application/x-gzip and try to download a file.
|
||||
type Gzip struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Configs []Config
|
||||
}
|
||||
|
||||
|
@ -36,7 +45,6 @@ func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
return g.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
outer:
|
||||
for _, c := range g.Configs {
|
||||
|
||||
|
@ -79,9 +87,7 @@ outer:
|
|||
// to send something back before gzipWriter gets closed at
|
||||
// the return of this method!
|
||||
if status >= 400 {
|
||||
gz.Header().Set("Content-Type", "text/plain") // very necessary
|
||||
gz.WriteHeader(status)
|
||||
fmt.Fprintf(gz, "%d %s", status, http.StatusText(status))
|
||||
httpserver.DefaultErrorFunc(w, r, status)
|
||||
return 0, err
|
||||
}
|
||||
return status, err
|
|
@ -8,11 +8,10 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
|
||||
pathFilter := PathFilter{make(Set)}
|
||||
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
|
||||
for _, p := range badPaths {
|
||||
|
@ -80,9 +79,8 @@ func TestGzipHandler(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func nextFunc(shouldGzip bool) middleware.Handler {
|
||||
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
func nextFunc(shouldGzip bool) httpserver.Handler {
|
||||
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// write a relatively large text file
|
||||
b, err := ioutil.ReadFile("testdata/test.txt")
|
||||
if err != nil {
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// RequestFilter determines if a request should be gzipped.
|
||||
|
@ -15,7 +15,8 @@ type RequestFilter interface {
|
|||
}
|
||||
|
||||
// defaultExtensions is the list of default extensions for which to enable gzipping.
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json", ".md", ".xml", ".svg"}
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp"}
|
||||
|
||||
// DefaultExtFilter creates an ExtFilter with default extensions.
|
||||
func DefaultExtFilter() ExtFilter {
|
||||
|
@ -54,7 +55,7 @@ type PathFilter struct {
|
|||
// is found and true otherwise.
|
||||
func (p PathFilter) ShouldCompress(r *http.Request) bool {
|
||||
return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
|
||||
return middleware.Path(r.URL.Path).Matches(value)
|
||||
return httpserver.Path(r.URL.Path).Matches(value)
|
||||
})
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestLengthFilter(t *testing.T) {
|
||||
|
@ -61,7 +61,7 @@ func TestResponseFilterWriter(t *testing.T) {
|
|||
}}
|
||||
|
||||
for i, ts := range tests {
|
||||
server.Next = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(ts.body)))
|
||||
w.Write([]byte(ts.body))
|
||||
return 200, nil
|
|
@ -1,38 +1,40 @@
|
|||
package setup
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Gzip configures a new gzip middleware instance.
|
||||
func Gzip(c *Controller) (middleware.Middleware, error) {
|
||||
// setup configures a new gzip middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := gzipParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return gzip.Gzip{Next: next, Configs: configs}
|
||||
}, nil
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Gzip{Next: next, Configs: configs}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
var configs []gzip.Config
|
||||
func gzipParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
for c.Next() {
|
||||
config := gzip.Config{}
|
||||
config := Config{}
|
||||
|
||||
// Request Filters
|
||||
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
|
||||
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
|
||||
pathFilter := PathFilter{IgnoredPaths: make(Set)}
|
||||
extFilter := ExtFilter{Exts: make(Set)}
|
||||
|
||||
// Response Filters
|
||||
lengthFilter := gzip.LengthFilter(0)
|
||||
lengthFilter := LengthFilter(0)
|
||||
|
||||
// No extra args expected
|
||||
if len(c.RemainingArgs()) > 0 {
|
||||
|
@ -47,7 +49,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
|||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, e := range exts {
|
||||
if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard && e != "" {
|
||||
if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" {
|
||||
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
|
||||
}
|
||||
extFilter.Exts.Add(e)
|
||||
|
@ -82,18 +84,18 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
|||
} else if length == 0 {
|
||||
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
|
||||
}
|
||||
lengthFilter = gzip.LengthFilter(length)
|
||||
lengthFilter = LengthFilter(length)
|
||||
default:
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
// Request Filters
|
||||
config.RequestFilters = []gzip.RequestFilter{}
|
||||
config.RequestFilters = []RequestFilter{}
|
||||
|
||||
// If ignored paths are specified, put in front to filter with path first
|
||||
if len(pathFilter.IgnoredPaths) > 0 {
|
||||
config.RequestFilters = []gzip.RequestFilter{pathFilter}
|
||||
config.RequestFilters = []RequestFilter{pathFilter}
|
||||
}
|
||||
|
||||
// Then, if extensions are specified, use those to filter.
|
||||
|
@ -101,7 +103,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
|
|||
if len(extFilter.Exts) > 0 {
|
||||
config.RequestFilters = append(config.RequestFilters, extFilter)
|
||||
} else {
|
||||
config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter())
|
||||
config.RequestFilters = append(config.RequestFilters, DefaultExtFilter())
|
||||
}
|
||||
|
||||
// Response Filters
|
|
@ -1,29 +1,29 @@
|
|||
package setup
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
c := NewTestController(`gzip`)
|
||||
|
||||
mid, err := Gzip(c)
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`gzip`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if mids == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(gzip.Gzip)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Gzip)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
|
@ -90,8 +90,7 @@ func TestGzip(t *testing.T) {
|
|||
`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err := gzipParse(c)
|
||||
_, err := gzipParse(caddy.NewTestController(test.input))
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
} else if !test.shouldErr && err != nil {
|
|
@ -1,28 +1,28 @@
|
|||
// Package headers provides middleware that appends headers to
|
||||
// Package header provides middleware that appends headers to
|
||||
// requests based on a set of configuration rules that define
|
||||
// which routes receive which headers.
|
||||
package headers
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Headers is middleware that adds headers to the responses
|
||||
// for requests matching a certain path.
|
||||
type Headers struct {
|
||||
Next middleware.Handler
|
||||
Next httpserver.Handler
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface and serves requests,
|
||||
// ServeHTTP implements the httpserver.Handler interface and serves requests,
|
||||
// setting headers on the response according to the configured rules.
|
||||
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
replacer := middleware.NewReplacer(r, nil, "")
|
||||
replacer := httpserver.NewReplacer(r, nil, "")
|
||||
for _, rule := range h.Rules {
|
||||
if middleware.Path(r.URL.Path).Matches(rule.Path) {
|
||||
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||
for _, header := range rule.Headers {
|
||||
if strings.HasPrefix(header.Name, "-") {
|
||||
w.Header().Del(strings.TrimLeft(header.Name, "-"))
|
|
@ -1,4 +1,4 @@
|
|||
package headers
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -6,10 +6,10 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
func TestHeader(t *testing.T) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not determine hostname: %v", err)
|
||||
|
@ -27,7 +27,7 @@ func TestHeaders(t *testing.T) {
|
|||
{"/b", "Bar", "Removed in /a"},
|
||||
} {
|
||||
he := Headers{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
}),
|
||||
Rules: []Rule{
|
|
@ -1,27 +1,37 @@
|
|||
package setup
|
||||
package header
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Headers configures a new Headers middleware instance.
|
||||
func Headers(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return headers.Headers{Next: next, Rules: rules}
|
||||
}, nil
|
||||
func init() {
|
||||
caddy.RegisterPlugin(caddy.Plugin{
|
||||
Name: "header",
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
var rules []headers.Rule
|
||||
// setup configures a new Headers middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Headers{Next: next, Rules: rules}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func headersParse(c *caddy.Controller) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
|
||||
for c.NextLine() {
|
||||
var head headers.Rule
|
||||
var head Rule
|
||||
var isNewPattern bool
|
||||
|
||||
if !c.NextArg() {
|
||||
|
@ -46,7 +56,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) {
|
|||
for c.NextBlock() {
|
||||
// A block of headers was opened...
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
h := Header{Name: c.Val()}
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
|
@ -57,7 +67,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) {
|
|||
if c.NextArg() {
|
||||
// ... or single header was defined as an argument instead.
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
h := Header{Name: c.Val()}
|
||||
|
||||
h.Value = c.Val()
|
||||
|
|
@ -1,30 +1,31 @@
|
|||
package setup
|
||||
package header
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
c := NewTestController(`header / Foo Bar`)
|
||||
|
||||
mid, err := Headers(c)
|
||||
func TestSetup(t *testing.T) {
|
||||
err := setup(caddy.NewTestController(`header / Foo Bar`))
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
|
||||
mids := httpserver.GetConfig("").Middleware()
|
||||
if len(mids) == 0 {
|
||||
t.Fatal("Expected middleware, had 0 instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(headers.Headers)
|
||||
handler := mids[0](httpserver.EmptyNext)
|
||||
myHandler, ok := handler.(Headers)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
@ -33,17 +34,17 @@ func TestHeadersParse(t *testing.T) {
|
|||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []headers.Rule
|
||||
expected []Rule
|
||||
}{
|
||||
{`header /foo Foo "Bar Baz"`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/foo", Headers: []headers.Header{
|
||||
false, []Rule{
|
||||
{Path: "/foo", Headers: []Header{
|
||||
{Name: "Foo", Value: "Bar Baz"},
|
||||
}},
|
||||
}},
|
||||
{`header /bar { Foo "Bar Baz" Baz Qux }`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/bar", Headers: []headers.Header{
|
||||
false, []Rule{
|
||||
{Path: "/bar", Headers: []Header{
|
||||
{Name: "Foo", Value: "Bar Baz"},
|
||||
{Name: "Baz", Value: "Qux"},
|
||||
}},
|
||||
|
@ -51,8 +52,7 @@ func TestHeadersParse(t *testing.T) {
|
|||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := headersParse(c)
|
||||
actual, err := headersParse(caddy.NewTestController(test.input))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
|
@ -1,4 +1,4 @@
|
|||
package middleware
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue