diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index b49ec37cc..44e2caa6b 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -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`)? diff --git a/caddy/assets/path.go b/assets.go similarity index 57% rename from caddy/assets/path.go rename to assets.go index 46b883b1c..e353af8d3 100644 --- a/caddy/assets/path.go +++ b/assets.go @@ -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") } diff --git a/assets_test.go b/assets_test.go new file mode 100644 index 000000000..193361048 --- /dev/null +++ b/assets_test.go @@ -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", "") +} diff --git a/caddy.go b/caddy.go new file mode 100644 index 000000000..0cd342abc --- /dev/null +++ b/caddy.go @@ -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" +) diff --git a/caddy/assets/path_test.go b/caddy/assets/path_test.go deleted file mode 100644 index 374f813af..000000000 --- a/caddy/assets/path_test.go +++ /dev/null @@ -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) - } -} diff --git a/build.bash b/caddy/build.bash similarity index 94% rename from build.bash rename to caddy/build.bash index ec8a67d56..03c553451 100755 --- a/build.bash +++ b/caddy/build.bash @@ -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 diff --git a/caddy/caddy.go b/caddy/caddy.go deleted file mode 100644 index 1484e1127..000000000 --- a/caddy/caddy.go +++ /dev/null @@ -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 -} diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go deleted file mode 100644 index be40075dc..000000000 --- a/caddy/caddy_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/main.go b/caddy/caddymain/run.go similarity index 51% rename from main.go rename to caddy/caddymain/run.go index abb1b3f39..b1ae18592 100644 --- a/main.go +++ b/caddy/caddymain/run.go @@ -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 diff --git a/caddy/config.go b/caddy/config.go deleted file mode 100644 index c8ea6b4da..000000000 --- a/caddy/config.go +++ /dev/null @@ -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 diff --git a/caddy/config_test.go b/caddy/config_test.go deleted file mode 100644 index f5f0db6c2..000000000 --- a/caddy/config_test.go +++ /dev/null @@ -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, "", 1234}, - {server.Config{Host: "localhost", Port: "80"}, false, false, "", 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, "", 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, "", 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, "", 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) - } - } -} diff --git a/caddy/directives.go b/caddy/directives.go deleted file mode 100644 index 66e123a2d..000000000 --- a/caddy/directives.go +++ /dev/null @@ -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) diff --git a/caddy/directives_test.go b/caddy/directives_test.go deleted file mode 100644 index e37411f1c..000000000 --- a/caddy/directives_test.go +++ /dev/null @@ -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()) - } -} diff --git a/caddy/helpers.go b/caddy/helpers.go deleted file mode 100644 index 2338fff0f..000000000 --- a/caddy/helpers.go +++ /dev/null @@ -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 } diff --git a/caddy/https/crypto.go b/caddy/https/crypto.go deleted file mode 100644 index 7971bda36..000000000 --- a/caddy/https/crypto.go +++ /dev/null @@ -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) -} diff --git a/caddy/https/handler.go b/caddy/https/handler.go deleted file mode 100644 index f3139f54e..000000000 --- a/caddy/https/handler.go +++ /dev/null @@ -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 -} diff --git a/caddy/https/https.go b/caddy/https/https.go deleted file mode 100644 index f9214f149..000000000 --- a/caddy/https/https.go +++ /dev/null @@ -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{} diff --git a/caddy/https/https_test.go b/caddy/https/https_test.go deleted file mode 100644 index 0f118f095..000000000 --- a/caddy/https/https_test.go +++ /dev/null @@ -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) - } -} diff --git a/caddy/https/setup.go b/caddy/https/setup.go deleted file mode 100644 index eebfc62da..000000000 --- a/caddy/https/setup.go +++ /dev/null @@ -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, -} diff --git a/caddy/main.go b/caddy/main.go new file mode 100644 index 000000000..4559be03a --- /dev/null +++ b/caddy/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/mholt/caddy/caddy/caddymain" + +func main() { + caddymain.Run() +} diff --git a/caddy/parse/lexer_test.go b/caddy/parse/lexer_test.go deleted file mode 100644 index f12c7e7dc..000000000 --- a/caddy/parse/lexer_test.go +++ /dev/null @@ -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 - } - } -} diff --git a/caddy/parse/parse.go b/caddy/parse/parse.go deleted file mode 100644 index faef36c28..000000000 --- a/caddy/parse/parse.go +++ /dev/null @@ -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{}) diff --git a/caddy/parse/parse_test.go b/caddy/parse/parse_test.go deleted file mode 100644 index 48746300f..000000000 --- a/caddy/parse/parse_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/caddy/parse/parsing_test.go b/caddy/parse/parsing_test.go deleted file mode 100644 index db7fd3e1b..000000000 --- a/caddy/parse/parsing_test.go +++ /dev/null @@ -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 -} diff --git a/caddy/restart.go b/caddy/restart.go deleted file mode 100644 index afd1b79e8..000000000 --- a/caddy/restart.go +++ /dev/null @@ -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 -} diff --git a/caddy/restart_windows.go b/caddy/restart_windows.go deleted file mode 100644 index d860e9131..000000000 --- a/caddy/restart_windows.go +++ /dev/null @@ -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) -} diff --git a/caddy/restartinproc.go b/caddy/restartinproc.go deleted file mode 100644 index 677857a14..000000000 --- a/caddy/restartinproc.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/bindhost.go b/caddy/setup/bindhost.go deleted file mode 100644 index 363163dcb..000000000 --- a/caddy/setup/bindhost.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/controller.go b/caddy/setup/controller.go deleted file mode 100644 index e31207263..000000000 --- a/caddy/setup/controller.go +++ /dev/null @@ -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) -} diff --git a/caddy/setup/expvar.go b/caddy/setup/expvar.go deleted file mode 100644 index 4d9c353de..000000000 --- a/caddy/setup/expvar.go +++ /dev/null @@ -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" diff --git a/caddy/setup/expvar_test.go b/caddy/setup/expvar_test.go deleted file mode 100644 index 5fb018ce9..000000000 --- a/caddy/setup/expvar_test.go +++ /dev/null @@ -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") - } -} diff --git a/caddy/setup/ext.go b/caddy/setup/ext.go deleted file mode 100644 index bfd4cba55..000000000 --- a/caddy/setup/ext.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/internal.go b/caddy/setup/internal.go deleted file mode 100644 index e83863b80..000000000 --- a/caddy/setup/internal.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/pprof.go b/caddy/setup/pprof.go deleted file mode 100644 index 010485026..000000000 --- a/caddy/setup/pprof.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/proxy.go b/caddy/setup/proxy.go deleted file mode 100644 index 3011cb0e4..000000000 --- a/caddy/setup/proxy.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/root.go b/caddy/setup/root.go deleted file mode 100644 index 5100f6961..000000000 --- a/caddy/setup/root.go +++ /dev/null @@ -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 -} diff --git a/caddy/setup/testdata/blog/first_post.md b/caddy/setup/testdata/blog/first_post.md deleted file mode 100644 index f26583b75..000000000 --- a/caddy/setup/testdata/blog/first_post.md +++ /dev/null @@ -1 +0,0 @@ -# Test h1 diff --git a/caddy/setup/testdata/tpl_with_include.html b/caddy/setup/testdata/tpl_with_include.html deleted file mode 100644 index 95eeae0c8..000000000 --- a/caddy/setup/testdata/tpl_with_include.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -{{.Doc.title}} - - -{{.Include "header.html"}} -{{.Doc.body}} - - diff --git a/caddy_test.go b/caddy_test.go new file mode 100644 index 000000000..51e93c8ac --- /dev/null +++ b/caddy_test.go @@ -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) + } + } +} diff --git a/caddy/parse/dispenser.go b/caddyfile/dispenser.go similarity index 89% rename from caddy/parse/dispenser.go rename to caddyfile/dispenser.go index 08aa6e76d..7beae9f3c 100644 --- a/caddy/parse/dispenser.go +++ b/caddyfile/dispenser.go @@ -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 } diff --git a/caddy/parse/dispenser_test.go b/caddyfile/dispenser_test.go similarity index 99% rename from caddy/parse/dispenser_test.go rename to caddyfile/dispenser_test.go index 20a7ddcac..313e273b0 100644 --- a/caddy/parse/dispenser_test.go +++ b/caddyfile/dispenser_test.go @@ -1,4 +1,4 @@ -package parse +package caddyfile import ( "reflect" diff --git a/caddy/caddyfile/json.go b/caddyfile/json.go similarity index 80% rename from caddy/caddyfile/json.go rename to caddyfile/json.go index e1213c27d..52c7b90fd 100644 --- a/caddy/caddyfile/json.go +++ b/caddyfile/json.go @@ -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"` } diff --git a/caddy/caddyfile/json_test.go b/caddyfile/json_test.go similarity index 60% rename from caddy/caddyfile/json_test.go rename to caddyfile/json_test.go index 2e44ae2a2..97d553c33 100644 --- a/caddy/caddyfile/json_test.go +++ b/caddyfile/json_test.go @@ -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) } } +*/ diff --git a/caddy/parse/lexer.go b/caddyfile/lexer.go similarity index 91% rename from caddy/parse/lexer.go rename to caddyfile/lexer.go index d2939eba2..a3004af14 100644 --- a/caddy/parse/lexer.go +++ b/caddyfile/lexer.go @@ -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 diff --git a/caddyfile/lexer_test.go b/caddyfile/lexer_test.go new file mode 100644 index 000000000..4f8295c42 --- /dev/null +++ b/caddyfile/lexer_test.go @@ -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 + } + } +} diff --git a/caddy/parse/parsing.go b/caddyfile/parse.go similarity index 71% rename from caddy/parse/parsing.go rename to caddyfile/parse.go index 3d4a383cd..b3b851d5c 100644 --- a/caddy/parse/parsing.go +++ b/caddyfile/parse.go @@ -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 } diff --git a/caddyfile/parse_test.go b/caddyfile/parse_test.go new file mode 100644 index 000000000..27d62615a --- /dev/null +++ b/caddyfile/parse_test.go @@ -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 +} diff --git a/caddy/parse/import_glob0.txt b/caddyfile/testdata/import_glob0.txt similarity index 100% rename from caddy/parse/import_glob0.txt rename to caddyfile/testdata/import_glob0.txt diff --git a/caddy/parse/import_glob1.txt b/caddyfile/testdata/import_glob1.txt similarity index 100% rename from caddy/parse/import_glob1.txt rename to caddyfile/testdata/import_glob1.txt diff --git a/caddy/parse/import_glob2.txt b/caddyfile/testdata/import_glob2.txt similarity index 100% rename from caddy/parse/import_glob2.txt rename to caddyfile/testdata/import_glob2.txt diff --git a/caddy/parse/import_test1.txt b/caddyfile/testdata/import_test1.txt similarity index 100% rename from caddy/parse/import_test1.txt rename to caddyfile/testdata/import_test1.txt diff --git a/caddy/parse/import_test2.txt b/caddyfile/testdata/import_test2.txt similarity index 100% rename from caddy/parse/import_test2.txt rename to caddyfile/testdata/import_test2.txt diff --git a/middleware/basicauth/basicauth.go b/caddyhttp/basicauth/basicauth.go similarity index 95% rename from middleware/basicauth/basicauth.go rename to caddyhttp/basicauth/basicauth.go index ebfd0a8e6..c75cc7d1b 100644 --- a/middleware/basicauth/basicauth.go +++ b/caddyhttp/basicauth/basicauth.go @@ -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 } diff --git a/middleware/basicauth/basicauth_test.go b/caddyhttp/basicauth/basicauth_test.go similarity index 96% rename from middleware/basicauth/basicauth_test.go rename to caddyhttp/basicauth/basicauth_test.go index 631aaaed9..182feabf9 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/caddyhttp/basicauth/basicauth_test.go @@ -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"}}, diff --git a/caddy/setup/basicauth.go b/caddyhttp/basicauth/setup.go similarity index 53% rename from caddy/setup/basicauth.go rename to caddyhttp/basicauth/setup.go index bc57d1c6e..d911f3bd5 100644 --- a/caddy/setup/basicauth.go +++ b/caddyhttp/basicauth/setup.go @@ -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) } diff --git a/caddy/setup/basicauth_test.go b/caddyhttp/basicauth/setup_test.go similarity index 74% rename from caddy/setup/basicauth_test.go rename to caddyhttp/basicauth/setup_test.go index 186a3e97e..c1245a9e1 100644 --- a/caddy/setup/basicauth_test.go +++ b/caddyhttp/basicauth/setup_test.go @@ -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) diff --git a/caddyhttp/bind/bind.go b/caddyhttp/bind/bind.go new file mode 100644 index 000000000..fd60f8d1e --- /dev/null +++ b/caddyhttp/bind/bind.go @@ -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 +} diff --git a/caddyhttp/bind/bind_test.go b/caddyhttp/bind/bind_test.go new file mode 100644 index 000000000..330d5427d --- /dev/null +++ b/caddyhttp/bind/bind_test.go @@ -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) + } +} diff --git a/middleware/browse/browse.go b/caddyhttp/browse/browse.go similarity index 97% rename from middleware/browse/browse.go rename to caddyhttp/browse/browse.go index 62e4b1684..4e804f05e 100644 --- a/middleware/browse/browse.go +++ b/caddyhttp/browse/browse.go @@ -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, diff --git a/middleware/browse/browse_test.go b/caddyhttp/browse/browse_test.go similarity index 97% rename from middleware/browse/browse_test.go rename to caddyhttp/browse/browse_test.go index 161498f43..eb200d3f6 100644 --- a/middleware/browse/browse_test.go +++ b/caddyhttp/browse/browse_test.go @@ -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 +} diff --git a/caddy/setup/browse.go b/caddyhttp/browse/setup.go similarity index 94% rename from caddy/setup/browse.go rename to caddyhttp/browse/setup.go index fdb667227..88f7d44d4 100644 --- a/caddy/setup/browse.go +++ b/caddyhttp/browse/setup.go @@ -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 diff --git a/caddy/setup/browse_test.go b/caddyhttp/browse/setup_test.go similarity index 79% rename from caddy/setup/browse_test.go rename to caddyhttp/browse/setup_test.go index 443e008bb..fadb7fa1e 100644 --- a/caddy/setup/browse_test.go +++ b/caddyhttp/browse/setup_test.go @@ -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) diff --git a/middleware/browse/testdata/header.html b/caddyhttp/browse/testdata/header.html similarity index 100% rename from middleware/browse/testdata/header.html rename to caddyhttp/browse/testdata/header.html diff --git a/middleware/browse/testdata/photos.tpl b/caddyhttp/browse/testdata/photos.tpl similarity index 100% rename from middleware/browse/testdata/photos.tpl rename to caddyhttp/browse/testdata/photos.tpl diff --git a/middleware/browse/testdata/photos/test.html b/caddyhttp/browse/testdata/photos/test.html similarity index 100% rename from middleware/browse/testdata/photos/test.html rename to caddyhttp/browse/testdata/photos/test.html diff --git a/middleware/browse/testdata/photos/test2.html b/caddyhttp/browse/testdata/photos/test2.html similarity index 100% rename from middleware/browse/testdata/photos/test2.html rename to caddyhttp/browse/testdata/photos/test2.html diff --git a/middleware/browse/testdata/photos/test3.html b/caddyhttp/browse/testdata/photos/test3.html similarity index 100% rename from middleware/browse/testdata/photos/test3.html rename to caddyhttp/browse/testdata/photos/test3.html diff --git a/caddyhttp/caddyhttp.go b/caddyhttp/caddyhttp.go new file mode 100644 index 000000000..86b572132 --- /dev/null +++ b/caddyhttp/caddyhttp.go @@ -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" +) diff --git a/middleware/errors/errors.go b/caddyhttp/errors/errors.go similarity index 85% rename from middleware/errors/errors.go rename to caddyhttp/errors/errors.go index 33a152692..2527b0069 100644 --- a/middleware/errors/errors.go +++ b/caddyhttp/errors/errors.go @@ -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... 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) diff --git a/middleware/errors/errors_test.go b/caddyhttp/errors/errors_test.go similarity index 91% rename from middleware/errors/errors_test.go rename to caddyhttp/errors/errors_test.go index 49af3e4f4..26456c7a9 100644 --- a/middleware/errors/errors_test.go +++ b/caddyhttp/errors/errors_test.go @@ -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) diff --git a/caddy/setup/errors.go b/caddyhttp/errors/setup.go similarity index 79% rename from caddy/setup/errors.go rename to caddyhttp/errors/setup.go index b4c0ab697..f43c95e97 100644 --- a/caddy/setup/errors.go +++ b/caddyhttp/errors/setup.go @@ -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) diff --git a/caddy/setup/errors_test.go b/caddyhttp/errors/setup_test.go similarity index 77% rename from caddy/setup/errors_test.go rename to caddyhttp/errors/setup_test.go index ace04624d..aed907ee8 100644 --- a/caddy/setup/errors_test.go +++ b/caddyhttp/errors/setup_test.go @@ -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) diff --git a/middleware/expvar/expvar.go b/caddyhttp/expvar/expvar.go similarity index 87% rename from middleware/expvar/expvar.go rename to caddyhttp/expvar/expvar.go index 178243486..d3107a048 100644 --- a/middleware/expvar/expvar.go +++ b/caddyhttp/expvar/expvar.go @@ -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) } diff --git a/middleware/expvar/expvar_test.go b/caddyhttp/expvar/expvar_test.go similarity index 89% rename from middleware/expvar/expvar_test.go rename to caddyhttp/expvar/expvar_test.go index e702f9418..dfc7cb311 100644 --- a/middleware/expvar/expvar_test.go +++ b/caddyhttp/expvar/expvar_test.go @@ -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", } diff --git a/caddyhttp/expvar/setup.go b/caddyhttp/expvar/setup.go new file mode 100644 index 000000000..4883d7ef3 --- /dev/null +++ b/caddyhttp/expvar/setup.go @@ -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" diff --git a/caddyhttp/expvar/setup_test.go b/caddyhttp/expvar/setup_test.go new file mode 100644 index 000000000..96dd4b038 --- /dev/null +++ b/caddyhttp/expvar/setup_test.go @@ -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") + } +} diff --git a/middleware/extensions/ext.go b/caddyhttp/extensions/ext.go similarity index 90% rename from middleware/extensions/ext.go rename to caddyhttp/extensions/ext.go index 6796325d8..46013b406 100644 --- a/middleware/extensions/ext.go +++ b/caddyhttp/extensions/ext.go @@ -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] != '/' { diff --git a/caddyhttp/extensions/setup.go b/caddyhttp/extensions/setup.go new file mode 100644 index 000000000..3d46e77e8 --- /dev/null +++ b/caddyhttp/extensions/setup.go @@ -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 +} diff --git a/caddy/setup/ext_test.go b/caddyhttp/extensions/setup_test.go similarity index 72% rename from caddy/setup/ext_test.go rename to caddyhttp/extensions/setup_test.go index 24e3cf947..f6248beb5 100644 --- a/caddy/setup/ext_test.go +++ b/caddyhttp/extensions/setup_test.go @@ -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) diff --git a/middleware/fastcgi/fastcgi.go b/caddyhttp/fastcgi/fastcgi.go old mode 100755 new mode 100644 similarity index 96% rename from middleware/fastcgi/fastcgi.go rename to caddyhttp/fastcgi/fastcgi.go index 9db71f594..0ac886b6d --- a/middleware/fastcgi/fastcgi.go +++ b/caddyhttp/fastcgi/fastcgi.go @@ -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 } } diff --git a/middleware/fastcgi/fastcgi_test.go b/caddyhttp/fastcgi/fastcgi_test.go similarity index 100% rename from middleware/fastcgi/fastcgi_test.go rename to caddyhttp/fastcgi/fastcgi_test.go diff --git a/middleware/fastcgi/fcgi_test.php b/caddyhttp/fastcgi/fcgi_test.php similarity index 100% rename from middleware/fastcgi/fcgi_test.php rename to caddyhttp/fastcgi/fcgi_test.php diff --git a/middleware/fastcgi/fcgiclient.go b/caddyhttp/fastcgi/fcgiclient.go similarity index 100% rename from middleware/fastcgi/fcgiclient.go rename to caddyhttp/fastcgi/fcgiclient.go diff --git a/middleware/fastcgi/fcgiclient_test.go b/caddyhttp/fastcgi/fcgiclient_test.go similarity index 100% rename from middleware/fastcgi/fcgiclient_test.go rename to caddyhttp/fastcgi/fcgiclient_test.go diff --git a/caddy/setup/fastcgi.go b/caddyhttp/fastcgi/setup.go similarity index 66% rename from caddy/setup/fastcgi.go rename to caddyhttp/fastcgi/setup.go index d1e53d151..1b417108e 100644 --- a/caddy/setup/fastcgi.go +++ b/caddyhttp/fastcgi/setup.go @@ -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" diff --git a/caddy/setup/fastcgi_test.go b/caddyhttp/fastcgi/setup_test.go similarity index 85% rename from caddy/setup/fastcgi_test.go rename to caddyhttp/fastcgi/setup_test.go index 366446dee..b28b147b8 100644 --- a/caddy/setup/fastcgi_test.go +++ b/caddyhttp/fastcgi/setup_test.go @@ -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) diff --git a/middleware/gzip/gzip.go b/caddyhttp/gzip/gzip.go similarity index 93% rename from middleware/gzip/gzip.go rename to caddyhttp/gzip/gzip.go index 4ef658556..dfee6c2e9 100644 --- a/middleware/gzip/gzip.go +++ b/caddyhttp/gzip/gzip.go @@ -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 diff --git a/middleware/gzip/gzip_test.go b/caddyhttp/gzip/gzip_test.go similarity index 94% rename from middleware/gzip/gzip_test.go rename to caddyhttp/gzip/gzip_test.go index b39dd1af8..738dff679 100644 --- a/middleware/gzip/gzip_test.go +++ b/caddyhttp/gzip/gzip_test.go @@ -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 { diff --git a/middleware/gzip/request_filter.go b/caddyhttp/gzip/requestfilter.go similarity index 91% rename from middleware/gzip/request_filter.go rename to caddyhttp/gzip/requestfilter.go index 10f25c59b..804232a9d 100644 --- a/middleware/gzip/request_filter.go +++ b/caddyhttp/gzip/requestfilter.go @@ -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) }) } diff --git a/middleware/gzip/request_filter_test.go b/caddyhttp/gzip/requestfilter_test.go similarity index 100% rename from middleware/gzip/request_filter_test.go rename to caddyhttp/gzip/requestfilter_test.go diff --git a/middleware/gzip/response_filter.go b/caddyhttp/gzip/responsefilter.go similarity index 100% rename from middleware/gzip/response_filter.go rename to caddyhttp/gzip/responsefilter.go diff --git a/middleware/gzip/response_filter_test.go b/caddyhttp/gzip/responsefilter_test.go similarity index 94% rename from middleware/gzip/response_filter_test.go rename to caddyhttp/gzip/responsefilter_test.go index 2878336c3..a34f58cd3 100644 --- a/middleware/gzip/response_filter_test.go +++ b/caddyhttp/gzip/responsefilter_test.go @@ -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 diff --git a/caddy/setup/gzip.go b/caddyhttp/gzip/setup.go similarity index 69% rename from caddy/setup/gzip.go rename to caddyhttp/gzip/setup.go index 7d09fe01e..824ac2141 100644 --- a/caddy/setup/gzip.go +++ b/caddyhttp/gzip/setup.go @@ -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 diff --git a/caddy/setup/gzip_test.go b/caddyhttp/gzip/setup_test.go similarity index 76% rename from caddy/setup/gzip_test.go rename to caddyhttp/gzip/setup_test.go index 4c24ab0ab..a71c9b19f 100644 --- a/caddy/setup/gzip_test.go +++ b/caddyhttp/gzip/setup_test.go @@ -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 { diff --git a/middleware/gzip/testdata/test.txt b/caddyhttp/gzip/testdata/test.txt similarity index 100% rename from middleware/gzip/testdata/test.txt rename to caddyhttp/gzip/testdata/test.txt diff --git a/middleware/headers/headers.go b/caddyhttp/header/header.go similarity index 76% rename from middleware/headers/headers.go rename to caddyhttp/header/header.go index 831a1afb4..c51199d11 100644 --- a/middleware/headers/headers.go +++ b/caddyhttp/header/header.go @@ -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, "-")) diff --git a/middleware/headers/headers_test.go b/caddyhttp/header/header_test.go similarity index 87% rename from middleware/headers/headers_test.go rename to caddyhttp/header/header_test.go index 0627902d1..dd86a09cf 100644 --- a/middleware/headers/headers_test.go +++ b/caddyhttp/header/header_test.go @@ -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{ diff --git a/caddy/setup/headers.go b/caddyhttp/header/setup.go similarity index 61% rename from caddy/setup/headers.go rename to caddyhttp/header/setup.go index 553f20b18..56034921c 100644 --- a/caddy/setup/headers.go +++ b/caddyhttp/header/setup.go @@ -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() diff --git a/caddy/setup/headers_test.go b/caddyhttp/header/setup_test.go similarity index 69% rename from caddy/setup/headers_test.go rename to caddyhttp/header/setup_test.go index 7b111cb42..e3b6cbf19 100644 --- a/caddy/setup/headers_test.go +++ b/caddyhttp/header/setup_test.go @@ -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) diff --git a/middleware/context.go b/caddyhttp/httpserver/context.go similarity index 99% rename from middleware/context.go rename to caddyhttp/httpserver/context.go index 7dbb8c877..32ae2a40f 100644 --- a/middleware/context.go +++ b/caddyhttp/httpserver/context.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "bytes" diff --git a/middleware/context_test.go b/caddyhttp/httpserver/context_test.go similarity index 99% rename from middleware/context_test.go rename to caddyhttp/httpserver/context_test.go index a61a3bf92..332a649de 100644 --- a/middleware/context_test.go +++ b/caddyhttp/httpserver/context_test.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "bytes" @@ -58,7 +58,7 @@ func TestInclude(t *testing.T) { fileContent: `str1 {{ .InvalidField }} str2`, expectedContent: "", shouldErr: true, - expectedErrorContent: `type middleware.Context`, + expectedErrorContent: `type httpserver.Context`, }, } diff --git a/server/graceful.go b/caddyhttp/httpserver/graceful.go similarity index 68% rename from server/graceful.go rename to caddyhttp/httpserver/graceful.go index 5057d039b..f11a6c9aa 100644 --- a/server/graceful.go +++ b/caddyhttp/httpserver/graceful.go @@ -1,4 +1,4 @@ -package server +package httpserver import ( "net" @@ -6,16 +6,20 @@ import ( "syscall" ) +// TODO: Should this be a generic graceful listener available in its own package or something? +// Also, passing in a WaitGroup is a little awkward. Why can't this listener just keep +// the waitgroup internal to itself? + // newGracefulListener returns a gracefulListener that wraps l and // uses wg (stored in the host server) to count connections. -func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener { - gl := &gracefulListener{ListenerFile: l, stop: make(chan error), httpWg: wg} +func newGracefulListener(l net.Listener, wg *sync.WaitGroup) *gracefulListener { + gl := &gracefulListener{Listener: l, stop: make(chan error), connWg: wg} go func() { <-gl.stop gl.Lock() gl.stopped = true gl.Unlock() - gl.stop <- gl.ListenerFile.Close() + gl.stop <- gl.Listener.Close() }() return gl } @@ -24,21 +28,21 @@ func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener { // count the number of connections on it. Its // methods mainly wrap net.Listener to be graceful. type gracefulListener struct { - ListenerFile + net.Listener stop chan error stopped bool sync.Mutex // protects the stopped flag - httpWg *sync.WaitGroup // pointer to the host's wg used for counting connections + connWg *sync.WaitGroup // pointer to the host's wg used for counting connections } // Accept accepts a connection. func (gl *gracefulListener) Accept() (c net.Conn, err error) { - c, err = gl.ListenerFile.Accept() + c, err = gl.Listener.Accept() if err != nil { return } - c = gracefulConn{Conn: c, httpWg: gl.httpWg} - gl.httpWg.Add(1) + c = gracefulConn{Conn: c, connWg: gl.connWg} + gl.connWg.Add(1) return } @@ -60,7 +64,7 @@ func (gl *gracefulListener) Close() error { // a graceful shutdown. type gracefulConn struct { net.Conn - httpWg *sync.WaitGroup // pointer to the host server's connection waitgroup + connWg *sync.WaitGroup // pointer to the host server's connection waitgroup } // Close closes c's underlying connection while updating the wg count. @@ -71,6 +75,6 @@ func (c gracefulConn) Close() error { } // close can fail on http2 connections (as of Oct. 2015, before http2 in std lib) // so don't decrement count unless close succeeds - c.httpWg.Done() + c.connWg.Done() return nil } diff --git a/caddyhttp/httpserver/https.go b/caddyhttp/httpserver/https.go new file mode 100644 index 000000000..b93a85439 --- /dev/null +++ b/caddyhttp/httpserver/https.go @@ -0,0 +1,154 @@ +package httpserver + +import ( + "net" + "net/http" + + "github.com/mholt/caddy/caddytls" +) + +func activateHTTPS() error { + // TODO: Is this loop a bug? Should we scope this method to just a single context? (restarts...?) + for _, ctx := range contexts { + // pre-screen each config and earmark the ones that qualify for managed TLS + markQualifiedForAutoHTTPS(ctx.siteConfigs) + + // place certificates and keys on disk + for _, c := range ctx.siteConfigs { + err := c.TLS.ObtainCert(true) + if err != nil { + return err + } + } + + // update TLS configurations + err := enableAutoHTTPS(ctx.siteConfigs, true) + if err != nil { + return err + } + + // set up redirects + ctx.siteConfigs = makePlaintextRedirects(ctx.siteConfigs) + } + + // renew all relevant certificates that need renewal. this is important + // to do right away so we guarantee that renewals aren't missed, and + // also the user can respond to any potential errors that occur. + err := caddytls.RenewManagedCertificates(true) + if err != nil { + return err + } + + return nil +} + +// markQualifiedForAutoHTTPS scans each config and, if it +// qualifies for managed TLS, it sets the Managed field of +// the TLS config to true. +func markQualifiedForAutoHTTPS(configs []*SiteConfig) { + for _, cfg := range configs { + if caddytls.QualifiesForManagedTLS(cfg) && cfg.Addr.Scheme != "http" { + cfg.TLS.Managed = true + } + } +} + +// enableAutoHTTPS 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 enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error { + for _, cfg := range configs { + if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed { + continue + } + cfg.TLS.Enabled = true + cfg.Addr.Scheme = "https" + if loadCertificates && caddytls.HostQualifies(cfg.Addr.Host) { + _, err := caddytls.CacheManagedCertificate(cfg.Addr.Host, cfg.TLS) + if err != nil { + return err + } + } + + // Make sure any config values not explicitly set are set to default + caddytls.SetDefaultTLSParams(cfg.TLS) + + // Set default port of 443 if not explicitly set + if cfg.Addr.Port == "" && + cfg.TLS.Enabled && + (!cfg.TLS.Manual || cfg.TLS.OnDemand) && + cfg.Addr.Host != "localhost" { + cfg.Addr.Port = "443" + } + } + return nil +} + +// 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 []*SiteConfig) []*SiteConfig { + for i, cfg := range allConfigs { + if cfg.TLS.Managed && + !hostHasOtherPort(allConfigs, i, "80") && + (cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) { + allConfigs = append(allConfigs, redirPlaintextHost(cfg)) + } + } + return allConfigs +} + +// 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 []*SiteConfig, 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.Addr.Host == allConfigs[thisConfigIdx].Addr.Host && + otherCfg.Addr.Port == otherPort { + return true + } + } + return false +} + +// 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. The TLS field of cfg must not be nil. +func redirPlaintextHost(cfg *SiteConfig) *SiteConfig { + redirPort := cfg.Addr.Port + if redirPort == "443" { + // default port is redundant + redirPort = "" + } + redirMiddleware := func(next Handler) Handler { + return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + toURL := "https://" + r.Host + if redirPort != "" { + toURL += ":" + redirPort + } + toURL += r.URL.RequestURI() + http.Redirect(w, r, toURL, http.StatusMovedPermanently) + return 0, nil + }) + } + host := cfg.Addr.Host + port := "80" + addr := net.JoinHostPort(host, port) + return &SiteConfig{ + Addr: Address{Original: addr, Host: host, Port: port}, + ListenHost: cfg.ListenHost, + middleware: []Middleware{redirMiddleware}, + TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort}, + } +} diff --git a/caddyhttp/httpserver/https_test.go b/caddyhttp/httpserver/https_test.go new file mode 100644 index 000000000..04a4db1e4 --- /dev/null +++ b/caddyhttp/httpserver/https_test.go @@ -0,0 +1,178 @@ +package httpserver + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/mholt/caddy/caddytls" +) + +func TestRedirPlaintextHost(t *testing.T) { + cfg := redirPlaintextHost(&SiteConfig{ + Addr: Address{ + Host: "example.com", + Port: "1234", + }, + ListenHost: "93.184.216.34", + TLS: new(caddytls.Config), + }) + + // Check host and port + if actual, expected := cfg.Addr.Host, "example.com"; actual != expected { + t.Errorf("Expected redir config to have host %s but got %s", expected, actual) + } + if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected { + t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual) + } + if actual, expected := cfg.Addr.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 := cfg.middleware[0](nil) + + // Check redirect for correctness + rec := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo/bar?q=1", nil) + if err != nil { + t.Fatal(err) + } + status, err := handler.ServeHTTP(rec, req) + if status != 0 { + t.Errorf("Expected status return to be 0, but was %d", status) + } + if err != nil { + t.Errorf("Expected returned error to be nil, but was %v", err) + } + if rec.Code != http.StatusMovedPermanently { + t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code) + } + if got, want := rec.Header().Get("Location"), "https://foo:1234/bar?q=1"; got != want { + t.Errorf("Expected Location: '%s' but got '%s'", want, got) + } + + // 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(&SiteConfig{Addr: Address{Host: "example.com", Port: "443"}, TLS: new(caddytls.Config)}) + handler = cfg.middleware[0](nil) + + rec = httptest.NewRecorder() + req, err = http.NewRequest("GET", "http://foo/bar?q=1", nil) + if err != nil { + t.Fatal(err) + } + status, err = handler.ServeHTTP(rec, req) + if status != 0 { + t.Errorf("Expected status return to be 0, but was %d", status) + } + if err != nil { + t.Errorf("Expected returned error to be nil, but was %v", err) + } + if rec.Code != http.StatusMovedPermanently { + t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code) + } + if got, want := rec.Header().Get("Location"), "https://foo/bar?q=1"; got != want { + t.Errorf("Expected Location: '%s' but got '%s'", want, got) + } +} + +func TestHostHasOtherPort(t *testing.T) { + configs := []*SiteConfig{ + {Addr: Address{Host: "example.com", Port: "80"}}, + {Addr: Address{Host: "sub1.example.com", Port: "80"}}, + {Addr: Address{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 := []*SiteConfig{ + // Happy path = standard redirect from 80 to 443 + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}}, + + // Host on port 80 already defined; don't change it (no redirect) + {Addr: Address{Host: "sub1.example.com", Port: "80", Scheme: "http"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Managed: true}}, + + // Redirect from port 80 to port 5000 in this case + {Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Managed: true}}, + + // Can redirect from 80 to either 443 or 5001, but choose 443 + {Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Managed: true}}, + {Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{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 TestEnableAutoHTTPS(t *testing.T) { + configs := []*SiteConfig{ + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}}, + {}, // not managed - no changes! + } + + enableAutoHTTPS(configs, false) + + if !configs[0].TLS.Enabled { + t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false") + } + if configs[0].Addr.Scheme != "https" { + t.Errorf("Expected config 0 to have Addr.Scheme == \"https\", but it was \"%s\"", + configs[0].Addr.Scheme) + } + if configs[1].TLS != nil && configs[1].TLS.Enabled { + t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true") + } +} + +func TestMarkQualifiedForAutoHTTPS(t *testing.T) { + // TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list... + configs := []*SiteConfig{ + {Addr: Address{Host: ""}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}}, + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}}, + {Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com"}}, + {Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)}, + {Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)}, + } + expectedManagedCount := 4 + + markQualifiedForAutoHTTPS(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) + } +} diff --git a/middleware/middleware.go b/caddyhttp/httpserver/middleware.go similarity index 68% rename from middleware/middleware.go rename to caddyhttp/httpserver/middleware.go index d91044ebe..e5e70de42 100644 --- a/middleware/middleware.go +++ b/caddyhttp/httpserver/middleware.go @@ -1,12 +1,18 @@ -// Package middleware provides some types and functions common among middleware. -package middleware +package httpserver import ( + "fmt" "net/http" + "os" "path" + "strings" "time" ) +func init() { + initCaseSettings() +} + type ( // Middleware is the middle layer which represents the traditional // idea of middleware: it chains one Handler to the next by being @@ -96,8 +102,55 @@ func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) { w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat)) } +// CaseSensitivePath determines if paths should be case sensitive. +// This is configurable via CASE_SENSITIVE_PATH environment variable. +var CaseSensitivePath = true + +const caseSensitivePathEnv = "CASE_SENSITIVE_PATH" + +// initCaseSettings loads case sensitivity config from environment variable. +// +// This could have been in init, but init cannot be called from tests. +func initCaseSettings() { + switch os.Getenv(caseSensitivePathEnv) { + case "0", "false": + CaseSensitivePath = false + default: + CaseSensitivePath = true + } +} + +// Path represents a URI path. +type Path string + +// Matches checks to see if other matches p. +// +// Path matching will probably not always be a direct +// comparison; this method assures that paths can be +// easily and consistently matched. +func (p Path) Matches(other string) bool { + if CaseSensitivePath { + return strings.HasPrefix(string(p), other) + } + return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other)) +} + // currentTime, as it is defined here, returns time.Now(). // It's defined as a variable for mocking time in tests. -var currentTime = func() time.Time { - return time.Now() +var currentTime = func() time.Time { return time.Now() } + +// EmptyNext is a no-op function that can be passed into +// 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 +// plugins can use this as a convenience. +var EmptyNext = 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 +// plugins can use this as a convenience. +func SameNext(next1, next2 Handler) bool { + return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2) } diff --git a/middleware/path_test.go b/caddyhttp/httpserver/middleware_test.go similarity index 98% rename from middleware/path_test.go rename to caddyhttp/httpserver/middleware_test.go index eb054b1e4..2f75c8bb9 100644 --- a/middleware/path_test.go +++ b/caddyhttp/httpserver/middleware_test.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "os" diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go new file mode 100644 index 000000000..881fd8127 --- /dev/null +++ b/caddyhttp/httpserver/plugin.go @@ -0,0 +1,386 @@ +package httpserver + +import ( + "flag" + "fmt" + "log" + "net" + "net/url" + "strings" + "time" + + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyfile" + "github.com/mholt/caddy/caddytls" +) + +const serverType = "http" + +func init() { + flag.StringVar(&Host, "host", DefaultHost, "Default host") + flag.StringVar(&Port, "port", DefaultPort, "Default port") + flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site") + flag.DurationVar(&GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") // TODO + flag.BoolVar(&HTTP2, "http2", true, "Use HTTP/2") + flag.BoolVar(&QUIC, "quic", false, "Use experimental QUIC") + + caddy.RegisterServerType(serverType, caddy.ServerType{ + Directives: directives, + DefaultInput: func() caddy.Input { + if Port == DefaultPort && Host != "" { + // by leaving the port blank in this case we give auto HTTPS + // a chance to set the port to 443 for us + return caddy.CaddyfileInput{ + Contents: []byte(fmt.Sprintf("%s\nroot %s", Host, Root)), + ServerTypeName: serverType, + } + } + return caddy.CaddyfileInput{ + Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)), + ServerTypeName: serverType, + } + }, + NewContext: newContext, + }) + caddy.RegisterCaddyfileLoader("short", caddy.LoaderFunc(shortCaddyfileLoader)) + caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS) + caddytls.RegisterConfigGetter(serverType, func(key string) *caddytls.Config { return GetConfig(key).TLS }) +} + +var contexts []*httpContext + +func newContext() caddy.Context { + context := &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)} + contexts = append(contexts, context) + return context +} + +type httpContext struct { + // keysToSiteConfigs maps an address at the top of a + // server block (a "key") to its SiteConfig. Not all + // SiteConfigs will be represented here, only ones + // that appeared in the Caddyfile. + keysToSiteConfigs map[string]*SiteConfig + + // siteConfigs is the master list of all site configs. + siteConfigs []*SiteConfig +} + +// InspectServerBlocks make sure that everything checks out before +// executing directives and otherwise prepares the directives to +// be parsed and executed. +func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) { + // For each address in each server block, make a new config + for _, sb := range serverBlocks { + for _, key := range sb.Keys { + key = strings.ToLower(key) + if _, dup := h.keysToSiteConfigs[key]; dup { + return serverBlocks, fmt.Errorf("duplicate site address: %s", key) + } + addr, err := standardizeAddress(key) + if err != nil { + return serverBlocks, err + } + // Save the config to our master list, and key it for lookups + cfg := &SiteConfig{ + Addr: addr, + Root: Root, + TLS: &caddytls.Config{Hostname: addr.Host}, + HiddenFiles: []string{sourceFile}, + } + h.siteConfigs = append(h.siteConfigs, cfg) + h.keysToSiteConfigs[key] = cfg + } + } + + // For sites that have gzip (which gets chained in + // before the error handler) we should ensure that the + // errors directive also appears so error pages aren't + // written after the gzip writer is closed. + for _, sb := range serverBlocks { + _, hasGzip := sb.Tokens["gzip"] + _, hasErrors := sb.Tokens["errors"] + if hasGzip && !hasErrors { + sb.Tokens["errors"] = []caddyfile.Token{{Text: "errors"}} + } + } + + return serverBlocks, nil +} + +// MakeServers uses the newly-created siteConfigs to +// create and return a list of server instances. +func (h *httpContext) MakeServers() ([]caddy.Server, error) { + // make sure TLS is disabled for explicitly-HTTP sites + // (necessary when HTTP address shares a block containing tls) + for _, cfg := range h.siteConfigs { + if cfg.TLS.Enabled && (cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http") { + cfg.TLS.Enabled = false + log.Printf("[WARNING] TLS disabled for %s", cfg.Addr) + } + } + + // we must map (group) each config to a bind address + groups, err := groupSiteConfigsByListenAddr(h.siteConfigs) + if err != nil { + return nil, err + } + + // then we create a server for each group + var servers []caddy.Server + for addr, group := range groups { + s, err := NewServer(addr, group) + if err != nil { + return nil, err + } + servers = append(servers, s) + } + + return servers, nil +} + +// GetConfig gets a SiteConfig that is keyed by addrKey. +// It creates an empty one in the latest context if +// the key does not exist in any context, so it +// will never return nil. If no contexts exist (which +// should never happen except in tests), it creates a +// new context in which to put it. +func GetConfig(addrKey string) *SiteConfig { + for _, context := range contexts { + if cfg, ok := context.keysToSiteConfigs[addrKey]; ok { + return cfg + } + } + if len(contexts) == 0 { + // this shouldn't happen except in tests + newContext() + } + cfg := &SiteConfig{Root: Root, TLS: new(caddytls.Config)} + defaultCtx := contexts[len(contexts)-1] + defaultCtx.siteConfigs = append(defaultCtx.siteConfigs, cfg) + defaultCtx.keysToSiteConfigs[addrKey] = cfg + return cfg +} + +// shortCaddyfileLoader loads a Caddyfile if positional arguments are +// detected, or, in other words, if un-named arguments are provided to +// the program. A "short Caddyfile" is one in which each argument +// is a line of the Caddyfile. The default host and port are prepended +// according to the Host and Port values. +func shortCaddyfileLoader(serverType string) (caddy.Input, error) { + if flag.NArg() > 0 && serverType == "http" { + confBody := fmt.Sprintf("%s:%s\n%s", Host, Port, strings.Join(flag.Args(), "\n")) + return caddy.CaddyfileInput{ + Contents: []byte(confBody), + Filepath: "args", + ServerTypeName: serverType, + }, nil + } + return nil, nil +} + +// groupSiteConfigsByListenAddr groups site configs by their listen +// (bind) address, so sites that use the same listener can be served +// on the same server instance. The return value maps the listen +// address (what you pass into net.Listen) to the list of site configs. +// This function does NOT vet the configs to ensure they are compatible. +func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConfig, error) { + groups := make(map[string][]*SiteConfig) + + for _, conf := range configs { + if caddy.IsLoopback(conf.Addr.Host) && conf.ListenHost == "" { + // special case: one would not expect a site served + // at loopback to be connected to from the outside. + conf.ListenHost = conf.Addr.Host + } + if conf.Addr.Port == "" { + conf.Addr.Port = Port + } + addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Addr.Port)) + if err != nil { + return nil, err + } + addrstr := addr.String() + groups[addrstr] = append(groups[addrstr], conf) + } + + return groups, nil +} + +// AddMiddleware adds a middleware to a site's middleware stack. +func (sc *SiteConfig) AddMiddleware(m Middleware) { + sc.middleware = append(sc.middleware, m) +} + +// Address represents a site address. It contains +// the original input value, and the component +// parts of an address. +type Address struct { + Original, Scheme, Host, Port, Path string +} + +// String returns a human-friendly print of the address. +func (a Address) String() string { + if a.Host == "" && a.Port == "" { + return "" + } + scheme := a.Scheme + if scheme == "" { + if a.Port == "443" { + scheme = "https" + } else { + scheme = "http" + } + } + s := scheme + if s != "" { + s += "://" + } + s += a.Host + if a.Port != "" && + ((scheme == "https" && a.Port != "443") || + (scheme == "http" && a.Port != "80")) { + s += ":" + a.Port + } + if a.Path != "" { + s += a.Path + } + return s +} + +// VHost returns a sensible concatenation of Host:Port/Path from a. +// It's basically the a.Original but without the scheme. +func (a Address) VHost() string { + if idx := strings.Index(a.Original, "://"); idx > -1 { + return a.Original[idx+3:] + } + return a.Original +} + +// standardizeAddress parses an address string into a structured format with separate +// scheme, host, and port portions, as well as the original input string. +func standardizeAddress(str string) (Address, error) { + input := str + + // Split input into components (prepend with // to assert host by default) + if !strings.Contains(str, "//") { + str = "//" + str + } + u, err := url.Parse(str) + if err != nil { + return Address{}, err + } + + // separate host and port + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + host, port, err = net.SplitHostPort(u.Host + ":") + if err != nil { + host = u.Host + } + } + + // see if we can set port based off scheme + if port == "" { + if u.Scheme == "http" { + port = "80" + } else if u.Scheme == "https" { + port = "443" + } + } + + // repeated or conflicting scheme is confusing, so error + if u.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 (u.Scheme == "http" && port == "443") || (u.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" { + u.Scheme = "http" + port = "80" + } else if port == "https" { + u.Scheme = "https" + port = "443" + } + + return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err +} + +// directives is the list of all directives known to exist for the +// http server type, including non-standard (3rd-party) directives. +// The ordering of this list is important. +var directives = []string{ + // primitive actions that set up the fundamental vitals of each config + "root", + "tls", + "bind", + + // services/utilities, or other directives that don't necessarily inject handlers + "startup", + "shutdown", + "realip", // github.com/captncraig/caddy-realip + "git", // github.com/abiosoft/caddy-git + + // directives that add middleware to the stack + "log", + "gzip", + "errors", + "ipfilter", // github.com/pyed/ipfilter + "search", // github.com/pedronasser/caddy-search + "header", + "cors", // github.com/captncraig/cors/caddy + "rewrite", + "redir", + "ext", + "mime", + "basicauth", + "jwt", // github.com/BTBurke/caddy-jwt + "jsonp", // github.com/pschlump/caddy-jsonp + "upload", // blitznote.com/src/caddy.upload + "internal", + "proxy", + "fastcgi", + "websocket", + "markdown", + "templates", + "browse", + "hugo", // github.com/hacdias/caddy-hugo + "mailout", // github.com/SchumacherFM/mailout + "prometheus", // github.com/miekg/caddy-prometheus +} + +const ( + // DefaultHost is the default host. + DefaultHost = "" + // DefaultPort is the default port. + DefaultPort = "2015" + // DefaultRoot is the default root folder. + DefaultRoot = "." +) + +// These "soft defaults" are configurable by +// command line flags, etc. +var ( + // Root is the site root + Root = DefaultRoot + + // Host is the site host + Host = DefaultHost + + // Port is the site port + Port = DefaultPort + + // GracefulTimeout is the maximum duration of a graceful shutdown. + GracefulTimeout time.Duration + + // HTTP2 indicates whether HTTP2 is enabled or not. + HTTP2 bool + + // QUIC indicates whether QUIC is enabled or not. + QUIC bool +) diff --git a/caddyhttp/httpserver/plugin_test.go b/caddyhttp/httpserver/plugin_test.go new file mode 100644 index 000000000..9959ce9bb --- /dev/null +++ b/caddyhttp/httpserver/plugin_test.go @@ -0,0 +1,114 @@ +package httpserver + +import "testing" + +func TestStandardizeAddress(t *testing.T) { + for i, test := range []struct { + input string + scheme, host, port, path string + shouldErr bool + }{ + {`localhost`, "", "localhost", "", "", 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}, + {`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}, + {`host/path`, "", "host", "", "/path", false}, + {`http://host/`, "http", "host", "80", "/", false}, + {`//asdf`, "", "asdf", "", "", false}, + {`:1234/asdf`, "", "", "1234", "/asdf", false}, + {`http://host/path`, "http", "host", "80", "/path", false}, + {`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false}, + {`host:80/path`, "", "host", "80", "/path", false}, + {`host:https/path`, "https", "host", "443", "/path", false}, + } { + actual, err := standardizeAddress(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 !test.shouldErr && actual.Original != test.input { + t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original) + } + 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) + } + if actual.Path != test.path { + t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path) + } + } +} + +func TestAddressVHost(t *testing.T) { + for i, test := range []struct { + addr Address + expected string + }{ + {Address{Original: "host:1234"}, "host:1234"}, + {Address{Original: "host:1234/foo"}, "host:1234/foo"}, + {Address{Original: "host/foo"}, "host/foo"}, + {Address{Original: "http://host/foo"}, "host/foo"}, + {Address{Original: "https://host/foo"}, "host/foo"}, + } { + actual := test.addr.VHost() + if actual != test.expected { + t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) + } + } +} + +func TestAddressString(t *testing.T) { + for i, test := range []struct { + addr Address + expected string + }{ + {Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"}, + {Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"}, + {Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"}, + {Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"}, + {Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"}, + {Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"}, + {Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"}, + {Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"}, + {Address{Scheme: "", Host: "", Port: "", Path: ""}, ""}, + } { + actual := test.addr.String() + if actual != test.expected { + t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) + } + } +} diff --git a/middleware/recorder.go b/caddyhttp/httpserver/recorder.go similarity index 99% rename from middleware/recorder.go rename to caddyhttp/httpserver/recorder.go index 50f4811cf..5788ab44b 100644 --- a/middleware/recorder.go +++ b/caddyhttp/httpserver/recorder.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "bufio" diff --git a/middleware/recorder_test.go b/caddyhttp/httpserver/recorder_test.go similarity index 98% rename from middleware/recorder_test.go rename to caddyhttp/httpserver/recorder_test.go index ed6c6abdd..0772d669f 100644 --- a/middleware/recorder_test.go +++ b/caddyhttp/httpserver/recorder_test.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "net/http" diff --git a/middleware/replacer.go b/caddyhttp/httpserver/replacer.go similarity index 99% rename from middleware/replacer.go rename to caddyhttp/httpserver/replacer.go index 6748f6060..e0299b8bf 100644 --- a/middleware/replacer.go +++ b/caddyhttp/httpserver/replacer.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "net" diff --git a/middleware/replacer_test.go b/caddyhttp/httpserver/replacer_test.go similarity index 99% rename from middleware/replacer_test.go rename to caddyhttp/httpserver/replacer_test.go index f5d50b047..466e06239 100644 --- a/middleware/replacer_test.go +++ b/caddyhttp/httpserver/replacer_test.go @@ -1,4 +1,4 @@ -package middleware +package httpserver import ( "net/http" diff --git a/caddy/setup/roller.go b/caddyhttp/httpserver/roller.go similarity index 51% rename from caddy/setup/roller.go rename to caddyhttp/httpserver/roller.go index fedc52c58..b82646361 100644 --- a/caddy/setup/roller.go +++ b/caddyhttp/httpserver/roller.go @@ -1,12 +1,36 @@ -package setup +package httpserver import ( + "io" "strconv" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy" + + "gopkg.in/natefinch/lumberjack.v2" ) -func parseRoller(c *Controller) (*middleware.LogRoller, error) { +// LogRoller implements a type that provides a rolling logger. +type LogRoller struct { + Filename string + MaxSize int + MaxAge int + MaxBackups int + LocalTime bool +} + +// GetLogWriter returns an io.Writer that writes to a rolling logger. +func (l LogRoller) GetLogWriter() io.Writer { + return &lumberjack.Logger{ + Filename: l.Filename, + MaxSize: l.MaxSize, + MaxAge: l.MaxAge, + MaxBackups: l.MaxBackups, + LocalTime: l.LocalTime, + } +} + +// ParseRoller parses roller contents out of c. +func ParseRoller(c *caddy.Controller) (*LogRoller, error) { var size, age, keep int // This is kind of a hack to support nested blocks: // As we are already in a block: either log or errors, @@ -31,7 +55,7 @@ func parseRoller(c *Controller) (*middleware.LogRoller, error) { return nil, err } } - return &middleware.LogRoller{ + return &LogRoller{ MaxSize: size, MaxAge: age, MaxBackups: keep, diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go new file mode 100644 index 000000000..2156bcf42 --- /dev/null +++ b/caddyhttp/httpserver/server.go @@ -0,0 +1,378 @@ +// Package httpserver implements an HTTP server on top of Caddy. +package httpserver + +import ( + "crypto/tls" + "fmt" + "log" + "net" + "net/http" + "os" + "path" + "runtime" + "strings" + "sync" + "time" + + "github.com/lucas-clemente/quic-go/h2quic" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/staticfiles" + "github.com/mholt/caddy/caddytls" +) + +// Server is the HTTP server implementation. +type Server struct { + Server *http.Server + quicServer *h2quic.Server + listener net.Listener + listenerMu sync.Mutex + sites []*SiteConfig + connTimeout time.Duration // max time to wait for a connection before force stop + connWg sync.WaitGroup // one increment per connection + tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine + vhosts *vhostTrie +} + +// ensure it satisfies the interface +var _ caddy.GracefulServer = new(Server) + +// NewServer creates a new Server instance that will listen on addr +// and will serve the sites configured in group. +func NewServer(addr string, group []*SiteConfig) (*Server, error) { + s := &Server{ + Server: &http.Server{ + Addr: addr, + // TODO: Make these values configurable? + // ReadTimeout: 2 * time.Minute, + // WriteTimeout: 2 * time.Minute, + // MaxHeaderBytes: 1 << 16, + }, + vhosts: newVHostTrie(), + sites: group, + connTimeout: GracefulTimeout, + } + s.Server.Handler = s // this is weird, but whatever + + // Disable HTTP/2 if desired + if !HTTP2 { + s.Server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + } + + // Enable QUIC if desired + if QUIC { + s.quicServer = &h2quic.Server{Server: s.Server} + } + + // We have to bound our wg with one increment + // to prevent a "race condition" that is hard-coded + // into sync.WaitGroup.Wait() - basically, an add + // with a positive delta must be guaranteed to + // occur before Wait() is called on the wg. + // In a way, this kind of acts as a safety barrier. + s.connWg.Add(1) + + // Set up TLS configuration + var tlsConfigs []*caddytls.Config + var err error + for _, site := range group { + tlsConfigs = append(tlsConfigs, site.TLS) + } + s.Server.TLSConfig, err = caddytls.MakeTLSConfig(tlsConfigs) + if err != nil { + return nil, err + } + + // Compile custom middleware for every site (enables virtual hosting) + for _, site := range group { + stack := Handler(staticfiles.FileServer{Root: http.Dir(site.Root), Hide: site.HiddenFiles}) + for i := len(site.middleware) - 1; i >= 0; i-- { + stack = site.middleware[i](stack) + } + site.middlewareChain = stack + s.vhosts.Insert(site.Addr.VHost(), site) + } + + return s, nil +} + +// Listen creates an active listener for s that can be +// used to serve requests. +func (s *Server) Listen() (net.Listener, error) { + if s.Server == nil { + return nil, fmt.Errorf("Server field is nil") + } + + ln, err := net.Listen("tcp", s.Server.Addr) + if err != nil { + var succeeded bool + if runtime.GOOS == "windows" { + // Windows has been known to keep sockets open even after closing the listeners. + // Tests reveal this error case easily because they call Start() then Stop() + // in succession. TODO: Better way to handle this? And why limit this to Windows? + for i := 0; i < 20; i++ { + time.Sleep(100 * time.Millisecond) + ln, err = net.Listen("tcp", s.Server.Addr) + if err == nil { + succeeded = true + break + } + } + } + if !succeeded { + return nil, err + } + } + + // Very important to return a concrete caddy.Listener + // implementation for graceful restarts. + return ln.(*net.TCPListener), nil +} + +// Serve serves requests on ln. It blocks until ln is closed. +func (s *Server) Serve(ln net.Listener) error { + if tcpLn, ok := ln.(*net.TCPListener); ok { + ln = tcpKeepAliveListener{TCPListener: tcpLn} + } + + ln = newGracefulListener(ln, &s.connWg) + + s.listenerMu.Lock() + s.listener = ln + s.listenerMu.Unlock() + + if s.Server.TLSConfig != nil { + // Create TLS listener - note that we do not replace s.listener + // with this TLS listener; tls.listener is unexported and does + // not implement the File() method we need for graceful restarts + // on POSIX systems. + // TODO: Is this ^ still relevant anymore? Maybe we can now that it's a net.Listener... + ln = tls.NewListener(ln, s.Server.TLSConfig) + + // Rotate TLS session ticket keys + s.tlsGovChan = caddytls.RotateSessionTicketKeys(s.Server.TLSConfig) + } + + if QUIC { + go func() { + err := s.quicServer.ListenAndServe() + if err != nil { + log.Printf("[ERROR] listening for QUIC connections: %v", err) + } + }() + } + + err := s.Server.Serve(ln) + if QUIC { + s.quicServer.Close() + } + return err +} + +// ServeHTTP is the entry point of all HTTP requests. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + // We absolutely need to be sure we stay alive up here, + // even though, in theory, the errors middleware does this. + if rec := recover(); rec != nil { + log.Printf("[PANIC] %v", rec) + DefaultErrorFunc(w, r, http.StatusInternalServerError) + } + }() + + w.Header().Set("Server", "Caddy") + + sanitizePath(r) + + status, _ := s.serveHTTP(w, r) + + // Fallback error response in case error handling wasn't chained in + if status >= 400 { + DefaultErrorFunc(w, r, status) + } +} + +func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + // strip out the port because it's not used in virtual + // hosting; the port is irrelevant because each listener + // is on a different port. + hostname, _, err := net.SplitHostPort(r.Host) + if err != nil { + hostname = r.Host + } + + // look up the virtualhost; if no match, serve error + vhost, pathPrefix := s.vhosts.Match(hostname + r.URL.Path) + + if vhost == nil { + // check for ACME challenge even if vhost is nil; + // could be a new host coming online soon + if caddytls.HTTPChallengeHandler(w, r, caddytls.DefaultHTTPAlternatePort) { + return 0, nil + } + // otherwise, log the error and write a message to the client + remoteHost, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + remoteHost = r.RemoteAddr + } + WriteTextResponse(w, http.StatusNotFound, "No such site at "+s.Server.Addr) + log.Printf("[INFO] %s - No such site at %s (Remote: %s, Referer: %s)", + hostname, s.Server.Addr, remoteHost, r.Header.Get("Referer")) + return 0, nil + } + + // we still check for ACME challenge if the vhost exists, + // because we must apply its HTTP challenge config settings + if s.proxyHTTPChallenge(vhost, w, r) { + return 0, nil + } + + // trim the path portion of the site address from the beginning of + // the URL path, so a request to example.com/foo/blog on the site + // defined as example.com/foo appears as /blog instead of /foo/blog. + if pathPrefix != "/" { + r.URL.Path = strings.TrimPrefix(r.URL.Path, pathPrefix) + if !strings.HasPrefix(r.URL.Path, "/") { + r.URL.Path = "/" + r.URL.Path + } + } + + return vhost.middlewareChain.ServeHTTP(w, r) +} + +// proxyHTTPChallenge solves the ACME HTTP challenge if r is the HTTP +// request for the challenge. If it is, and if the request has been +// fulfilled (response written), true is returned; false otherwise. +// If you don't have a vhost, just call the challenge handler directly. +func (s *Server) proxyHTTPChallenge(vhost *SiteConfig, w http.ResponseWriter, r *http.Request) bool { + if vhost.Addr.Port != caddytls.HTTPChallengePort { + return false + } + if vhost.TLS != nil && vhost.TLS.Manual { + return false + } + altPort := caddytls.DefaultHTTPAlternatePort + if vhost.TLS != nil && vhost.TLS.AltHTTPPort != "" { + altPort = vhost.TLS.AltHTTPPort + } + return caddytls.HTTPChallengeHandler(w, r, altPort) +} + +// Address returns the address s was assigned to listen on. +func (s *Server) Address() string { + return s.Server.Addr +} + +// Stop stops s gracefully (or forcefully after timeout) and +// closes its listener. +func (s *Server) Stop() (err error) { + s.Server.SetKeepAlivesEnabled(false) + + if runtime.GOOS != "windows" { + // force connections to close after timeout + done := make(chan struct{}) + go func() { + s.connWg.Done() // decrement our initial increment used as a barrier + s.connWg.Wait() + close(done) + }() + + // Wait for remaining connections to finish or + // force them all to close after timeout + select { + case <-time.After(s.connTimeout): + case <-done: + } + } + + // Close the listener now; this stops the server without delay + s.listenerMu.Lock() + if s.listener != nil { + err = s.listener.Close() + } + s.listenerMu.Unlock() + + // Closing this signals any TLS governor goroutines to exit + if s.tlsGovChan != nil { + close(s.tlsGovChan) + } + + return +} + +// sanitizePath collapses any ./ ../ /// madness +// which helps prevent path traversal attacks. +// Note to middleware: use URL.RawPath If you need +// the "original" URL.Path value. +func sanitizePath(r *http.Request) { + if r.URL.Path == "/" { + return + } + cleanedPath := path.Clean(r.URL.Path) + if cleanedPath == "." { + r.URL.Path = "/" + } else { + if !strings.HasPrefix(cleanedPath, "/") { + cleanedPath = "/" + cleanedPath + } + if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(cleanedPath, "/") { + cleanedPath = cleanedPath + "/" + } + r.URL.Path = cleanedPath + } +} + +// OnStartupComplete lists the sites served by this server +// and any relevant information, assuming caddy.Quiet == false. +func (s *Server) OnStartupComplete() { + if caddy.Quiet { + return + } + for _, site := range s.sites { + output := site.Addr.String() + if caddy.IsLoopback(s.Address()) && !caddy.IsLoopback(site.Addr.Host) { + output += " (only accessible on this machine)" + } + fmt.Println(output) + } +} + +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +// +// Borrowed from the Go standard library. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +// Accept accepts the connection with a keep-alive enabled. +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + +// File implements caddy.Listener; it returns the underlying file of the listener. +func (ln tcpKeepAliveListener) File() (*os.File, error) { + return ln.TCPListener.File() +} + +// DefaultErrorFunc responds to an HTTP request with a simple description +// of the specified HTTP status code. +func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) { + WriteTextResponse(w, status, fmt.Sprintf("%d %s\n", status, http.StatusText(status))) +} + +// WriteTextResponse writes body with code status to w. The body will +// be interpreted as plain text. +func WriteTextResponse(w http.ResponseWriter, status int, body string) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(status) + w.Write([]byte(body)) +} diff --git a/caddyhttp/httpserver/server_test.go b/caddyhttp/httpserver/server_test.go new file mode 100644 index 000000000..d8e53c100 --- /dev/null +++ b/caddyhttp/httpserver/server_test.go @@ -0,0 +1,15 @@ +package httpserver + +import ( + "net/http" + "testing" +) + +func TestAddress(t *testing.T) { + addr := "127.0.0.1:9005" + srv := &Server{Server: &http.Server{Addr: addr}} + + if got, want := srv.Address(), addr; got != want { + t.Errorf("Expected '%s' but got '%s'", want, got) + } +} diff --git a/caddyhttp/httpserver/siteconfig.go b/caddyhttp/httpserver/siteconfig.go new file mode 100644 index 000000000..dffe74901 --- /dev/null +++ b/caddyhttp/httpserver/siteconfig.go @@ -0,0 +1,53 @@ +package httpserver + +import "github.com/mholt/caddy/caddytls" + +// SiteConfig contains information about a site +// (also known as a virtual host). +type SiteConfig struct { + // The address of the site + Addr Address + + // The hostname to bind listener to; + // defaults to Addr.Host + ListenHost string + + // TLS configuration + TLS *caddytls.Config + + // Uncompiled middleware stack + middleware []Middleware + + // Compiled middleware stack + middlewareChain Handler + + // Directory from which to serve files + Root string + + // A list of files to hide (for example, the + // source Caddyfile). TODO: Enforcing this + // should be centralized, for example, a + // standardized way of loading files from disk + // for a request. + HiddenFiles []string +} + +// TLSConfig returns s.TLS. +func (s SiteConfig) TLSConfig() *caddytls.Config { + return s.TLS +} + +// Host returns s.Addr.Host. +func (s SiteConfig) Host() string { + return s.Addr.Host +} + +// Port returns s.Addr.Port. +func (s SiteConfig) Port() string { + return s.Addr.Port +} + +// Middleware returns s.middleware (useful for tests). +func (s SiteConfig) Middleware() []Middleware { + return s.middleware +} diff --git a/caddyhttp/httpserver/vhosttrie.go b/caddyhttp/httpserver/vhosttrie.go new file mode 100644 index 000000000..558255783 --- /dev/null +++ b/caddyhttp/httpserver/vhosttrie.go @@ -0,0 +1,139 @@ +package httpserver + +import ( + "net" + "strings" +) + +// vhostTrie facilitates virtual hosting. It matches +// requests first by hostname (with support for +// wildcards as TLS certificates support them), then +// by longest matching path. +type vhostTrie struct { + edges map[string]*vhostTrie + site *SiteConfig // also known as a virtual host + path string // the path portion of the key for this node +} + +// newVHostTrie returns a new vhostTrie. +func newVHostTrie() *vhostTrie { + return &vhostTrie{edges: make(map[string]*vhostTrie)} +} + +// Insert adds stack to t keyed by key. The key should be +// a valid "host/path" combination (or just host). +func (t *vhostTrie) Insert(key string, site *SiteConfig) { + host, path := t.splitHostPath(key) + if _, ok := t.edges[host]; !ok { + t.edges[host] = newVHostTrie() + } + t.edges[host].insertPath(path, path, site) +} + +// insertPath expects t to be a host node (not a root node), +// and inserts site into the t according to remainingPath. +func (t *vhostTrie) insertPath(remainingPath, originalPath string, site *SiteConfig) { + if remainingPath == "" { + t.site = site + t.path = originalPath + return + } + ch := string(remainingPath[0]) + if _, ok := t.edges[ch]; !ok { + t.edges[ch] = newVHostTrie() + } + t.edges[ch].insertPath(remainingPath[1:], originalPath, site) +} + +// Match returns the virtual host (site) in v with +// the closest match to key. If there was a match, +// it returns the SiteConfig and the path portion of +// the key used to make the match. The matched path +// would be a prefix of the path portion of the +// key, if not the whole path portion of the key. +// If there is no match, nil and empty string will +// be returned. +// +// A typical key will be in the form "host" or "host/path". +func (t *vhostTrie) Match(key string) (*SiteConfig, string) { + host, path := t.splitHostPath(key) + // try the given host, then, if no match, try wildcard hosts + branch := t.matchHost(host) + if branch == nil { + branch = t.matchHost("0.0.0.0") + } + if branch == nil { + branch = t.matchHost("") + } + if branch == nil { + return nil, "" + } + node := branch.matchPath(path) + if node == nil { + return nil, "" + } + return node.site, node.path +} + +// matchHost returns the vhostTrie matching host. The matching +// algorithm is the same as used to match certificates to host +// with SNI during TLS handshakes. In other words, it supports, +// to some degree, the use of wildcard (*) characters. +func (t *vhostTrie) matchHost(host string) *vhostTrie { + // try exact match + if subtree, ok := t.edges[host]; ok { + return subtree + } + + // then try replacing labels in the host + // with wildcards until we get a match + labels := strings.Split(host, ".") + for i := range labels { + labels[i] = "*" + candidate := strings.Join(labels, ".") + if subtree, ok := t.edges[candidate]; ok { + return subtree + } + } + + return nil +} + +// matchPath traverses t until it finds the longest key matching +// remainingPath, and returns its node. +func (t *vhostTrie) matchPath(remainingPath string) *vhostTrie { + var longestMatch *vhostTrie + for len(remainingPath) > 0 { + ch := string(remainingPath[0]) + next, ok := t.edges[ch] + if !ok { + break + } + if next.site != nil { + longestMatch = next + } + t = next + remainingPath = remainingPath[1:] + } + return longestMatch +} + +// splitHostPath separates host from path in key. +func (t *vhostTrie) splitHostPath(key string) (host, path string) { + parts := strings.SplitN(key, "/", 2) + host, path = strings.ToLower(parts[0]), "/" + if len(parts) > 1 { + path += parts[1] + } + // strip out the port (if present) from the host, since + // each port has its own socket, and each socket has its + // own listener, and each listener has its own server + // instance, and each server instance has its own vhosts. + // removing the port is a simple way to standardize so + // when requests come in, we can be sure to get a match. + hostname, _, err := net.SplitHostPort(host) + if err == nil { + host = hostname + } + return +} diff --git a/caddyhttp/httpserver/vhosttrie_test.go b/caddyhttp/httpserver/vhosttrie_test.go new file mode 100644 index 000000000..95ef1fba5 --- /dev/null +++ b/caddyhttp/httpserver/vhosttrie_test.go @@ -0,0 +1,141 @@ +package httpserver + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestVHostTrie(t *testing.T) { + trie := newVHostTrie() + populateTestTrie(trie, []string{ + "example", + "example.com", + "*.example.com", + "example.com/foo", + "example.com/foo/bar", + "*.example.com/test", + }) + assertTestTrie(t, trie, []vhostTrieTest{ + {"not-in-trie.com", false, "", "/"}, + {"example", true, "example", "/"}, + {"example.com", true, "example.com", "/"}, + {"example.com/test", true, "example.com", "/"}, + {"example.com/foo", true, "example.com/foo", "/foo"}, + {"example.com/foo/", true, "example.com/foo", "/foo"}, + {"EXAMPLE.COM/foo", true, "example.com/foo", "/foo"}, + {"EXAMPLE.COM/Foo", true, "example.com", "/"}, + {"example.com/foo/bar", true, "example.com/foo/bar", "/foo/bar"}, + {"example.com/foo/bar/baz", true, "example.com/foo/bar", "/foo/bar"}, + {"example.com/foo/other", true, "example.com/foo", "/foo"}, + {"foo.example.com", true, "*.example.com", "/"}, + {"foo.example.com/else", true, "*.example.com", "/"}, + }, false) +} + +func TestVHostTrieWildcard1(t *testing.T) { + trie := newVHostTrie() + populateTestTrie(trie, []string{ + "example.com", + "", + }) + assertTestTrie(t, trie, []vhostTrieTest{ + {"not-in-trie.com", true, "", "/"}, + {"example.com", true, "example.com", "/"}, + {"example.com/foo", true, "example.com", "/"}, + {"not-in-trie.com/asdf", true, "", "/"}, + }, true) +} + +func TestVHostTrieWildcard2(t *testing.T) { + trie := newVHostTrie() + populateTestTrie(trie, []string{ + "0.0.0.0/asdf", + }) + assertTestTrie(t, trie, []vhostTrieTest{ + {"example.com/asdf/foo", true, "0.0.0.0/asdf", "/asdf"}, + {"example.com/foo", false, "", "/"}, + {"host/asdf", true, "0.0.0.0/asdf", "/asdf"}, + }, true) +} + +func TestVHostTrieWildcard3(t *testing.T) { + trie := newVHostTrie() + populateTestTrie(trie, []string{ + "*/foo", + }) + assertTestTrie(t, trie, []vhostTrieTest{ + {"example.com/foo", true, "*/foo", "/foo"}, + {"example.com", false, "", "/"}, + }, true) +} + +func TestVHostTriePort(t *testing.T) { + // Make sure port is stripped out + trie := newVHostTrie() + populateTestTrie(trie, []string{ + "example.com:1234", + }) + assertTestTrie(t, trie, []vhostTrieTest{ + {"example.com/foo", true, "example.com:1234", "/"}, + }, true) +} + +func populateTestTrie(trie *vhostTrie, keys []string) { + for _, key := range keys { + // we wrap this in a func, passing in the key, otherwise the + // handler always writes the last key to the response, even + // if the handler is actually from one of the earlier keys. + func(key string) { + site := &SiteConfig{ + middlewareChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + w.Write([]byte(key)) + return 0, nil + }), + } + trie.Insert(key, site) + }(key) + } +} + +type vhostTrieTest struct { + query string + expectMatch bool + expectedKey string + matchedPrefix string // the path portion of a key that is expected to be matched +} + +func assertTestTrie(t *testing.T, trie *vhostTrie, tests []vhostTrieTest, hasWildcardHosts bool) { + for i, test := range tests { + site, pathPrefix := trie.Match(test.query) + + if !test.expectMatch { + if site != nil { + // If not expecting a value, then just make sure we didn't get one + t.Errorf("Test %d: Expected no matches, but got %v", i, site) + } + continue + } + + // Otherwise, we must assert we got a value + if site == nil { + t.Errorf("Test %d: Expected non-nil return value, but got: %v", i, site) + continue + } + + // And it must be the correct value + resp := httptest.NewRecorder() + site.middlewareChain.ServeHTTP(resp, nil) + actualHandlerKey := resp.Body.String() + if actualHandlerKey != test.expectedKey { + t.Errorf("Test %d: Expected match '%s' but matched '%s'", + i, test.expectedKey, actualHandlerKey) + } + + // The path prefix must also be correct + if test.matchedPrefix != pathPrefix { + t.Errorf("Test %d: Expected matched path prefix to be '%s', got '%s'", + i, test.matchedPrefix, pathPrefix) + } + } +} diff --git a/middleware/inner/internal.go b/caddyhttp/internalsrv/internal.go similarity index 83% rename from middleware/inner/internal.go rename to caddyhttp/internalsrv/internal.go index d7f044f70..aa7b69a8d 100644 --- a/middleware/inner/internal.go +++ b/caddyhttp/internalsrv/internal.go @@ -1,18 +1,21 @@ -// Package inner provides a simple middleware that (a) prevents access +// Package internalsrv provides a simple middleware that (a) prevents access // to internal locations and (b) allows to return files from internal location // by setting a special header, e.g. in a proxy response. -package inner +// +// The package is named internalsrv so as not to conflict with Go tooling +// convention which treats folders called "internal" differently. +package internalsrv import ( "net/http" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // Internal middleware protects internal locations from external requests - // but allows access from the inside by using a special HTTP header. type Internal struct { - Next middleware.Handler + Next httpserver.Handler Paths []string } @@ -25,12 +28,12 @@ func isInternalRedirect(w http.ResponseWriter) bool { return w.Header().Get(redirectHeader) != "" } -// ServeHTTP implements the middlware.Handler interface. +// ServeHTTP implements the httpserver.Handler interface. func (i Internal) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { // Internal location requested? -> Not found. for _, prefix := range i.Paths { - if middleware.Path(r.URL.Path).Matches(prefix) { + if httpserver.Path(r.URL.Path).Matches(prefix) { return http.StatusNotFound, nil } } diff --git a/middleware/inner/internal_test.go b/caddyhttp/internalsrv/internal_test.go similarity index 91% rename from middleware/inner/internal_test.go rename to caddyhttp/internalsrv/internal_test.go index 97078febc..fa9e05b43 100644 --- a/middleware/inner/internal_test.go +++ b/caddyhttp/internalsrv/internal_test.go @@ -1,4 +1,4 @@ -package inner +package internalsrv import ( "fmt" @@ -6,12 +6,12 @@ import ( "net/http/httptest" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestInternal(t *testing.T) { im := Internal{ - Next: middleware.HandlerFunc(internalTestHandlerFunc), + Next: httpserver.HandlerFunc(internalTestHandlerFunc), Paths: []string{"/internal"}, } diff --git a/caddyhttp/internalsrv/setup.go b/caddyhttp/internalsrv/setup.go new file mode 100644 index 000000000..a77edce90 --- /dev/null +++ b/caddyhttp/internalsrv/setup.go @@ -0,0 +1,41 @@ +package internalsrv + +import ( + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "internal", + ServerType: "http", + Action: setup, + }) +} + +// Internal configures a new Internal middleware instance. +func setup(c *caddy.Controller) error { + paths, err := internalParse(c) + if err != nil { + return err + } + + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Internal{Next: next, Paths: paths} + }) + + return nil +} + +func internalParse(c *caddy.Controller) ([]string, error) { + var paths []string + + for c.Next() { + if !c.NextArg() { + return paths, c.ArgErr() + } + paths = append(paths, c.Val()) + } + + return paths, nil +} diff --git a/caddy/setup/internal_test.go b/caddyhttp/internalsrv/setup_test.go similarity index 71% rename from caddy/setup/internal_test.go rename to caddyhttp/internalsrv/setup_test.go index f4d0ed8b9..e67982ce0 100644 --- a/caddy/setup/internal_test.go +++ b/caddyhttp/internalsrv/setup_test.go @@ -1,26 +1,24 @@ -package setup +package internalsrv import ( "testing" - "github.com/mholt/caddy/middleware/inner" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestInternal(t *testing.T) { - c := NewTestController(`internal /internal`) - - mid, err := Internal(c) - +func TestSetup(t *testing.T) { + err := setup(caddy.NewTestController(`internal /internal`)) 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.(inner.Internal) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Internal) if !ok { t.Fatalf("Expected handler to be type Internal, got: %#v", handler) @@ -30,7 +28,7 @@ func TestInternal(t *testing.T) { t.Errorf("Expected internal in the list of internal Paths") } - if !SameNext(myHandler.Next, EmptyNext) { + if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { t.Error("'Next' field of handler was not set properly") } @@ -48,8 +46,7 @@ func TestInternalParse(t *testing.T) { internal /internal2`, false, []string{"/internal1", "/internal2"}}, } for i, test := range tests { - c := NewTestController(test.inputInternalPaths) - actualInternalPaths, err := internalParse(c) + actualInternalPaths, err := internalParse(caddy.NewTestController(test.inputInternalPaths)) if err == nil && test.shouldErr { t.Errorf("Test %d didn't error, but it should have", i) diff --git a/middleware/log/log.go b/caddyhttp/log/log.go similarity index 81% rename from middleware/log/log.go rename to caddyhttp/log/log.go index 4e0c2e29b..1f0b5f0bc 100644 --- a/middleware/log/log.go +++ b/caddyhttp/log/log.go @@ -6,25 +6,34 @@ import ( "log" "net/http" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "log", + ServerType: "http", + Action: setup, + }) +} + // Logger is a basic request logging middleware. type Logger struct { - Next middleware.Handler + Next httpserver.Handler Rules []Rule ErrorFunc func(http.ResponseWriter, *http.Request, int) // failover error handler } func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range l.Rules { - if middleware.Path(r.URL.Path).Matches(rule.PathScope) { + if httpserver.Path(r.URL.Path).Matches(rule.PathScope) { // Record the response - responseRecorder := middleware.NewResponseRecorder(w) + responseRecorder := httpserver.NewResponseRecorder(w) // Attach the Replacer we'll use so that other middlewares can // set their own placeholders if they want to. - rep := middleware.NewReplacer(r, responseRecorder, CommonLogEmptyValue) + rep := httpserver.NewReplacer(r, responseRecorder, CommonLogEmptyValue) responseRecorder.Replacer = rep // Bon voyage, request! @@ -58,7 +67,7 @@ type Rule struct { OutputFile string Format string Log *log.Logger - Roller *middleware.LogRoller + Roller *httpserver.LogRoller } const ( diff --git a/middleware/log/log_test.go b/caddyhttp/log/log_test.go similarity index 92% rename from middleware/log/log_test.go rename to caddyhttp/log/log_test.go index 0ce12b0ca..af48f4424 100644 --- a/middleware/log/log_test.go +++ b/caddyhttp/log/log_test.go @@ -8,13 +8,13 @@ import ( "strings" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) type erroringMiddleware struct{} func (erroringMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - if rr, ok := w.(*middleware.ResponseRecorder); ok { + if rr, ok := w.(*httpserver.ResponseRecorder); ok { rr.Replacer.Set("testval", "foobar") } return http.StatusNotFound, nil diff --git a/caddy/setup/log.go b/caddyhttp/log/setup.go similarity index 66% rename from caddy/setup/log.go rename to caddyhttp/log/setup.go index 8bb4788a1..9aa3d9a49 100644 --- a/caddy/setup/log.go +++ b/caddyhttp/log/setup.go @@ -1,4 +1,4 @@ -package setup +package log import ( "io" @@ -6,20 +6,19 @@ import ( "os" "github.com/hashicorp/go-syslog" - "github.com/mholt/caddy/middleware" - caddylog "github.com/mholt/caddy/middleware/log" - "github.com/mholt/caddy/server" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// Log sets up the logging middleware. -func Log(c *Controller) (middleware.Middleware, error) { +// setup sets up the logging middleware. +func setup(c *caddy.Controller) error { rules, err := logParse(c) if err != nil { - return nil, err + return err } // Open the log files for writing when the server starts - c.Startup = append(c.Startup, func() error { + c.OnStartup(func() error { for i := 0; i < len(rules); i++ { var err error var writer io.Writer @@ -54,24 +53,26 @@ func Log(c *Controller) (middleware.Middleware, error) { return nil }) - return func(next middleware.Handler) middleware.Handler { - return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc} - }, nil + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Logger{Next: next, Rules: rules, ErrorFunc: httpserver.DefaultErrorFunc} + }) + + return nil } -func logParse(c *Controller) ([]caddylog.Rule, error) { - var rules []caddylog.Rule +func logParse(c *caddy.Controller) ([]Rule, error) { + var rules []Rule for c.Next() { args := c.RemainingArgs() - var logRoller *middleware.LogRoller + var logRoller *httpserver.LogRoller if c.NextBlock() { if c.Val() == "rotate" { if c.NextArg() { if c.Val() == "{" { var err error - logRoller, err = parseRoller(c) + logRoller, err = httpserver.ParseRoller(c) if err != nil { return nil, err } @@ -87,37 +88,37 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { } if len(args) == 0 { // Nothing specified; use defaults - rules = append(rules, caddylog.Rule{ + rules = append(rules, Rule{ PathScope: "/", - OutputFile: caddylog.DefaultLogFilename, - Format: caddylog.DefaultLogFormat, + OutputFile: DefaultLogFilename, + Format: DefaultLogFormat, Roller: logRoller, }) } else if len(args) == 1 { // Only an output file specified - rules = append(rules, caddylog.Rule{ + rules = append(rules, Rule{ PathScope: "/", OutputFile: args[0], - Format: caddylog.DefaultLogFormat, + Format: DefaultLogFormat, Roller: logRoller, }) } else { // Path scope, output file, and maybe a format specified - format := caddylog.DefaultLogFormat + format := DefaultLogFormat if len(args) > 2 { switch args[2] { case "{common}": - format = caddylog.CommonLogFormat + format = CommonLogFormat case "{combined}": - format = caddylog.CombinedLogFormat + format = CombinedLogFormat default: format = args[2] } } - rules = append(rules, caddylog.Rule{ + rules = append(rules, Rule{ PathScope: args[0], OutputFile: args[1], Format: format, diff --git a/caddy/setup/log_test.go b/caddyhttp/log/setup_test.go similarity index 73% rename from caddy/setup/log_test.go rename to caddyhttp/log/setup_test.go index ae7a96e31..436002ac3 100644 --- a/caddy/setup/log_test.go +++ b/caddyhttp/log/setup_test.go @@ -1,28 +1,27 @@ -package setup +package log import ( "testing" - "github.com/mholt/caddy/middleware" - caddylog "github.com/mholt/caddy/middleware/log" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestLog(t *testing.T) { - - c := NewTestController(`log`) - - mid, err := Log(c) +func TestSetup(t *testing.T) { + cfg := httpserver.GetConfig("") + err := setup(caddy.NewTestController(`log`)) if err != nil { t.Errorf("Expected no errors, got: %v", err) } - if mid == nil { + mids := cfg.Middleware() + if mids == nil { t.Fatal("Expected middleware, was nil instead") } - handler := mid(EmptyNext) - myHandler, ok := handler.(caddylog.Logger) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Logger) if !ok { t.Fatalf("Expected handler to be type Logger, got: %#v", handler) @@ -31,16 +30,16 @@ func TestLog(t *testing.T) { if myHandler.Rules[0].PathScope != "/" { t.Errorf("Expected / as the default PathScope") } - if myHandler.Rules[0].OutputFile != caddylog.DefaultLogFilename { - t.Errorf("Expected %s as the default OutputFile", caddylog.DefaultLogFilename) + if myHandler.Rules[0].OutputFile != DefaultLogFilename { + t.Errorf("Expected %s as the default OutputFile", DefaultLogFilename) } - if myHandler.Rules[0].Format != caddylog.DefaultLogFormat { - t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat) + if myHandler.Rules[0].Format != DefaultLogFormat { + t.Errorf("Expected %s as the default Log Format", DefaultLogFormat) } if myHandler.Rules[0].Roller != nil { t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller) } - if !SameNext(myHandler.Next, EmptyNext) { + if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { t.Error("'Next' field of handler was not set properly") } @@ -50,50 +49,50 @@ func TestLogParse(t *testing.T) { tests := []struct { inputLogRules string shouldErr bool - expectedLogRules []caddylog.Rule + expectedLogRules []Rule }{ - {`log`, false, []caddylog.Rule{{ + {`log`, false, []Rule{{ PathScope: "/", - OutputFile: caddylog.DefaultLogFilename, - Format: caddylog.DefaultLogFormat, + OutputFile: DefaultLogFilename, + Format: DefaultLogFormat, }}}, - {`log log.txt`, false, []caddylog.Rule{{ + {`log log.txt`, false, []Rule{{ PathScope: "/", OutputFile: "log.txt", - Format: caddylog.DefaultLogFormat, + Format: DefaultLogFormat, }}}, - {`log /api log.txt`, false, []caddylog.Rule{{ + {`log /api log.txt`, false, []Rule{{ PathScope: "/api", OutputFile: "log.txt", - Format: caddylog.DefaultLogFormat, + Format: DefaultLogFormat, }}}, - {`log /serve stdout`, false, []caddylog.Rule{{ + {`log /serve stdout`, false, []Rule{{ PathScope: "/serve", OutputFile: "stdout", - Format: caddylog.DefaultLogFormat, + Format: DefaultLogFormat, }}}, - {`log /myapi log.txt {common}`, false, []caddylog.Rule{{ + {`log /myapi log.txt {common}`, false, []Rule{{ PathScope: "/myapi", OutputFile: "log.txt", - Format: caddylog.CommonLogFormat, + Format: CommonLogFormat, }}}, - {`log /test accesslog.txt {combined}`, false, []caddylog.Rule{{ + {`log /test accesslog.txt {combined}`, false, []Rule{{ PathScope: "/test", OutputFile: "accesslog.txt", - Format: caddylog.CombinedLogFormat, + Format: CombinedLogFormat, }}}, {`log /api1 log.txt - log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{ + log /api2 accesslog.txt {combined}`, false, []Rule{{ PathScope: "/api1", OutputFile: "log.txt", - Format: caddylog.DefaultLogFormat, + Format: DefaultLogFormat, }, { PathScope: "/api2", OutputFile: "accesslog.txt", - Format: caddylog.CombinedLogFormat, + Format: CombinedLogFormat, }}}, {`log /api3 stdout {host} - log /api4 log.txt {when}`, false, []caddylog.Rule{{ + log /api4 log.txt {when}`, false, []Rule{{ PathScope: "/api3", OutputFile: "stdout", Format: "{host}", @@ -102,11 +101,11 @@ func TestLogParse(t *testing.T) { OutputFile: "log.txt", Format: "{when}", }}}, - {`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{ + {`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []Rule{{ PathScope: "/", OutputFile: "access.log", - Format: caddylog.DefaultLogFormat, - Roller: &middleware.LogRoller{ + Format: DefaultLogFormat, + Roller: &httpserver.LogRoller{ MaxSize: 2, MaxAge: 10, MaxBackups: 3, @@ -115,7 +114,7 @@ func TestLogParse(t *testing.T) { }}}, } for i, test := range tests { - c := NewTestController(test.inputLogRules) + c := caddy.NewTestController(test.inputLogRules) actualLogRules, err := logParse(c) if err == nil && test.shouldErr { diff --git a/middleware/markdown/markdown.go b/caddyhttp/markdown/markdown.go similarity index 93% rename from middleware/markdown/markdown.go rename to caddyhttp/markdown/markdown.go index ab53710e0..bef1dce01 100644 --- a/middleware/markdown/markdown.go +++ b/caddyhttp/markdown/markdown.go @@ -11,7 +11,7 @@ import ( "text/template" "time" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" "github.com/russross/blackfriday" ) @@ -25,7 +25,7 @@ type Markdown struct { FileSys http.FileSystem // Next HTTP handler in the chain - Next middleware.Handler + Next httpserver.Handler // The list of markdown configurations Configs []*Config @@ -59,7 +59,7 @@ type Config struct { func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var cfg *Config for _, c := range md.Configs { - if middleware.Path(r.URL.Path).Matches(c.PathScope) { // not negated + if httpserver.Path(r.URL.Path).Matches(c.PathScope) { // not negated cfg = c break // or goto } @@ -78,7 +78,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error var dirents []os.FileInfo var lastModTime time.Time fpath := r.URL.Path - if idx, ok := middleware.IndexFile(md.FileSys, fpath, md.IndexFiles); ok { + if idx, ok := httpserver.IndexFile(md.FileSys, fpath, md.IndexFiles); ok { // We're serving a directory index file, which may be a markdown // file with a template. Let's grab a list of files this directory // URL points to, and pass that in to any possible template invocations, @@ -133,7 +133,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error lastModTime = latest(lastModTime, fs.ModTime()) } - ctx := middleware.Context{ + ctx := httpserver.Context{ Root: md.FileSys, Req: r, URL: r.URL, @@ -145,7 +145,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Length", strconv.FormatInt(int64(len(html)), 10)) - middleware.SetLastModifiedHeader(w, lastModTime) + httpserver.SetLastModifiedHeader(w, lastModTime) if r.Method == http.MethodGet { w.Write(html) } diff --git a/middleware/markdown/markdown_test.go b/caddyhttp/markdown/markdown_test.go similarity index 97% rename from middleware/markdown/markdown_test.go rename to caddyhttp/markdown/markdown_test.go index 382c8e120..b4db6ead6 100644 --- a/middleware/markdown/markdown_test.go +++ b/caddyhttp/markdown/markdown_test.go @@ -12,7 +12,7 @@ import ( "text/template" "time" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" "github.com/russross/blackfriday" ) @@ -69,7 +69,7 @@ func TestMarkdown(t *testing.T) { }, }, IndexFiles: []string{"index.html"}, - 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 }), diff --git a/middleware/markdown/metadata/metadata.go b/caddyhttp/markdown/metadata/metadata.go similarity index 100% rename from middleware/markdown/metadata/metadata.go rename to caddyhttp/markdown/metadata/metadata.go diff --git a/middleware/markdown/metadata/metadata_json.go b/caddyhttp/markdown/metadata/metadata_json.go similarity index 100% rename from middleware/markdown/metadata/metadata_json.go rename to caddyhttp/markdown/metadata/metadata_json.go diff --git a/middleware/markdown/metadata/metadata_none.go b/caddyhttp/markdown/metadata/metadata_none.go similarity index 100% rename from middleware/markdown/metadata/metadata_none.go rename to caddyhttp/markdown/metadata/metadata_none.go diff --git a/middleware/markdown/metadata/metadata_test.go b/caddyhttp/markdown/metadata/metadata_test.go similarity index 100% rename from middleware/markdown/metadata/metadata_test.go rename to caddyhttp/markdown/metadata/metadata_test.go diff --git a/middleware/markdown/metadata/metadata_toml.go b/caddyhttp/markdown/metadata/metadata_toml.go similarity index 100% rename from middleware/markdown/metadata/metadata_toml.go rename to caddyhttp/markdown/metadata/metadata_toml.go diff --git a/middleware/markdown/metadata/metadata_yaml.go b/caddyhttp/markdown/metadata/metadata_yaml.go similarity index 100% rename from middleware/markdown/metadata/metadata_yaml.go rename to caddyhttp/markdown/metadata/metadata_yaml.go diff --git a/middleware/markdown/process.go b/caddyhttp/markdown/process.go similarity index 86% rename from middleware/markdown/process.go rename to caddyhttp/markdown/process.go index dc1dc6d0b..32c887c72 100644 --- a/middleware/markdown/process.go +++ b/caddyhttp/markdown/process.go @@ -5,15 +5,15 @@ import ( "io/ioutil" "os" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/markdown/metadata" - "github.com/mholt/caddy/middleware/markdown/summary" + "github.com/mholt/caddy/caddyhttp/httpserver" + "github.com/mholt/caddy/caddyhttp/markdown/metadata" + "github.com/mholt/caddy/caddyhttp/markdown/summary" "github.com/russross/blackfriday" ) type FileInfo struct { os.FileInfo - ctx middleware.Context + ctx httpserver.Context } func (f FileInfo) Summarize(wordcount int) (string, error) { @@ -33,7 +33,7 @@ func (f FileInfo) Summarize(wordcount int) (string, error) { // Markdown processes the contents of a page in b. It parses the metadata // (if any) and uses the template (if found). -func (c *Config) Markdown(title string, r io.Reader, dirents []os.FileInfo, ctx middleware.Context) ([]byte, error) { +func (c *Config) Markdown(title string, r io.Reader, dirents []os.FileInfo, ctx httpserver.Context) ([]byte, error) { body, err := ioutil.ReadAll(r) if err != nil { return nil, err diff --git a/caddy/setup/markdown.go b/caddyhttp/markdown/setup.go similarity index 63% rename from caddy/setup/markdown.go rename to caddyhttp/markdown/setup.go index fdc91991a..4bf9426aa 100644 --- a/caddy/setup/markdown.go +++ b/caddyhttp/markdown/setup.go @@ -1,42 +1,54 @@ -package setup +package markdown import ( "net/http" "path/filepath" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/markdown" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" "github.com/russross/blackfriday" ) -// Markdown configures a new Markdown middleware instance. -func Markdown(c *Controller) (middleware.Middleware, error) { +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "markdown", + ServerType: "http", + Action: setup, + }) +} + +// setup configures a new Markdown middleware instance. +func setup(c *caddy.Controller) error { mdconfigs, err := markdownParse(c) if err != nil { - return nil, err + return err } - md := markdown.Markdown{ - Root: c.Root, - FileSys: http.Dir(c.Root), + cfg := httpserver.GetConfig(c.Key) + + md := Markdown{ + Root: cfg.Root, + FileSys: http.Dir(cfg.Root), Configs: mdconfigs, IndexFiles: []string{"index.md"}, } - return func(next middleware.Handler) middleware.Handler { + cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { md.Next = next return md - }, nil + }) + + return nil } -func markdownParse(c *Controller) ([]*markdown.Config, error) { - var mdconfigs []*markdown.Config +func markdownParse(c *caddy.Controller) ([]*Config, error) { + var mdconfigs []*Config for c.Next() { - md := &markdown.Config{ + md := &Config{ Renderer: blackfriday.HtmlRenderer(0, "", ""), Extensions: make(map[string]struct{}), - Template: markdown.GetDefaultTemplate(), + Template: GetDefaultTemplate(), } // Get the path scope @@ -70,7 +82,9 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) { return mdconfigs, nil } -func loadParams(c *Controller, mdc *markdown.Config) error { +func loadParams(c *caddy.Controller, mdc *Config) error { + cfg := httpserver.GetConfig(c.Key) + switch c.Val() { case "ext": for _, ext := range c.RemainingArgs() { @@ -95,16 +109,16 @@ func loadParams(c *Controller, mdc *markdown.Config) error { default: return c.ArgErr() case 1: - fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0])) + fpath := filepath.ToSlash(filepath.Clean(cfg.Root + string(filepath.Separator) + tArgs[0])) - if err := markdown.SetTemplate(mdc.Template, "", fpath); err != nil { + if err := SetTemplate(mdc.Template, "", fpath); err != nil { c.Errf("default template parse error: %v", err) } return nil case 2: - fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1])) + fpath := filepath.ToSlash(filepath.Clean(cfg.Root + string(filepath.Separator) + tArgs[1])) - if err := markdown.SetTemplate(mdc.Template, tArgs[0], fpath); err != nil { + if err := SetTemplate(mdc.Template, tArgs[0], fpath); err != nil { c.Errf("template parse error: %v", err) } return nil diff --git a/caddy/setup/markdown_test.go b/caddyhttp/markdown/setup_test.go similarity index 84% rename from caddy/setup/markdown_test.go rename to caddyhttp/markdown/setup_test.go index fee9a3326..2880a230f 100644 --- a/caddy/setup/markdown_test.go +++ b/caddyhttp/markdown/setup_test.go @@ -1,4 +1,4 @@ -package setup +package markdown import ( "bytes" @@ -7,26 +7,22 @@ import ( "testing" "text/template" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/markdown" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestMarkdown(t *testing.T) { - - c := NewTestController(`markdown /blog`) - - mid, err := Markdown(c) - +func TestSetup(t *testing.T) { + err := setup(caddy.NewTestController(`markdown /blog`)) 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.(markdown.Markdown) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Markdown) if !ok { t.Fatalf("Expected handler to be type Markdown, got: %#v", handler) @@ -49,14 +45,14 @@ func TestMarkdownParse(t *testing.T) { tests := []struct { inputMarkdownConfig string shouldErr bool - expectedMarkdownConfig []markdown.Config + expectedMarkdownConfig []Config }{ {`markdown /blog { ext .md .txt css /resources/css/blog.css js /resources/js/blog.js -}`, false, []markdown.Config{{ +}`, false, []Config{{ PathScope: "/blog", Extensions: map[string]struct{}{ ".md": {}, @@ -64,26 +60,26 @@ func TestMarkdownParse(t *testing.T) { }, Styles: []string{"/resources/css/blog.css"}, Scripts: []string{"/resources/js/blog.js"}, - Template: markdown.GetDefaultTemplate(), + Template: GetDefaultTemplate(), }}}, {`markdown /blog { ext .md template tpl_with_include.html -}`, false, []markdown.Config{{ +}`, false, []Config{{ PathScope: "/blog", Extensions: map[string]struct{}{ ".md": {}, }, - Template: markdown.GetDefaultTemplate(), + Template: GetDefaultTemplate(), }}}, } // Setup the extra template tmpl := tests[1].expectedMarkdownConfig[0].Template - markdown.SetTemplate(tmpl, "", "./testdata/tpl_with_include.html") + SetTemplate(tmpl, "", "./testdata/tpl_with_include.html") for i, test := range tests { - c := NewTestController(test.inputMarkdownConfig) - c.Root = "./testdata" + c := caddy.NewTestController(test.inputMarkdownConfig) + httpserver.GetConfig("").Root = "./testdata" actualMarkdownConfigs, err := markdownParse(c) if err == nil && test.shouldErr { @@ -128,11 +124,11 @@ func equalTemplates(i, j *template.Template) (bool, string, string) { // sure that they're the same. // This is exceedingly ugly. - ctx := middleware.Context{ + ctx := httpserver.Context{ Root: http.Dir("./testdata"), } - md := markdown.Data{ + md := Data{ Context: ctx, Doc: make(map[string]string), DocFlags: make(map[string]bool), diff --git a/middleware/markdown/summary/render.go b/caddyhttp/markdown/summary/render.go similarity index 79% rename from middleware/markdown/summary/render.go rename to caddyhttp/markdown/summary/render.go index 0de9800e2..b23affbd1 100644 --- a/middleware/markdown/summary/render.go +++ b/caddyhttp/markdown/summary/render.go @@ -17,19 +17,19 @@ type renderer struct{} // Blocklevel callbacks -// Stub BlockCode is the code tag callback. +// BlockCode is the code tag callback. func (r renderer) BlockCode(out *bytes.Buffer, text []byte, land string) {} -// Stub BlockQuote is teh quote tag callback. +// BlockQuote is the quote tag callback. func (r renderer) BlockQuote(out *bytes.Buffer, text []byte) {} -// Stub BlockHtml is the HTML tag callback. +// BlockHtml is the HTML tag callback. func (r renderer) BlockHtml(out *bytes.Buffer, text []byte) {} -// Stub Header is the header tag callback. +// Header is the header tag callback. func (r renderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {} -// Stub HRule is the horizontal rule tag callback. +// HRule is the horizontal rule tag callback. func (r renderer) HRule(out *bytes.Buffer) {} // List is the list tag callback. @@ -43,7 +43,7 @@ func (r renderer) List(out *bytes.Buffer, text func() bool, flags int) { out.Write([]byte{' '}) } -// Stub ListItem is the list item tag callback. +// ListItem is the list item tag callback. func (r renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {} // Paragraph is the paragraph tag callback. This renders simple paragraph text @@ -56,30 +56,30 @@ func (r renderer) Paragraph(out *bytes.Buffer, text func() bool) { out.Write([]byte{' '}) } -// Stub Table is the table tag callback. +// Table is the table tag callback. func (r renderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {} -// Stub TableRow is the table row tag callback. +// TableRow is the table row tag callback. func (r renderer) TableRow(out *bytes.Buffer, text []byte) {} -// Stub TableHeaderCell is the table header cell tag callback. +// TableHeaderCell is the table header cell tag callback. func (r renderer) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {} -// Stub TableCell is the table cell tag callback. +// TableCell is the table cell tag callback. func (r renderer) TableCell(out *bytes.Buffer, text []byte, flags int) {} -// Stub Footnotes is the foot notes tag callback. +// Footnotes is the foot notes tag callback. func (r renderer) Footnotes(out *bytes.Buffer, text func() bool) {} -// Stub FootnoteItem is the footnote item tag callback. +// FootnoteItem is the footnote item tag callback. func (r renderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {} -// Stub TitleBlock is the title tag callback. +// TitleBlock is the title tag callback. func (r renderer) TitleBlock(out *bytes.Buffer, text []byte) {} // Spanlevel callbacks -// Stub AutoLink is the autolink tag callback. +// AutoLink is the autolink tag callback. func (r renderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {} // CodeSpan is the code span tag callback. Outputs a simple Markdown version @@ -102,10 +102,10 @@ func (r renderer) Emphasis(out *bytes.Buffer, text []byte) { out.Write(text) } -// Stub Image is the image tag callback. +// Image is the image tag callback. func (r renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {} -// Stub LineBreak is the line break tag callback. +// LineBreak is the line break tag callback. func (r renderer) LineBreak(out *bytes.Buffer) {} // Link is the link tag callback. Outputs a sipmle plain-text version @@ -114,7 +114,7 @@ func (r renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []b out.Write(content) } -// Stub RawHtmlTag is the raw HTML tag callback. +// RawHtmlTag is the raw HTML tag callback. func (r renderer) RawHtmlTag(out *bytes.Buffer, tag []byte) {} // TripleEmphasis is the triple emphasis tag callback. Outputs a simple plain-text @@ -123,10 +123,10 @@ func (r renderer) TripleEmphasis(out *bytes.Buffer, text []byte) { out.Write(text) } -// Stub StrikeThrough is the strikethrough tag callback. +// StrikeThrough is the strikethrough tag callback. func (r renderer) StrikeThrough(out *bytes.Buffer, text []byte) {} -// Stub FootnoteRef is the footnote ref tag callback. +// FootnoteRef is the footnote ref tag callback. func (r renderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {} // Lowlevel callbacks @@ -143,11 +143,11 @@ func (r renderer) NormalText(out *bytes.Buffer, text []byte) { // Header and footer -// Stub DocumentHeader callback. +// DocumentHeader callback. func (r renderer) DocumentHeader(out *bytes.Buffer) {} -// Stub DocumentFooter callback. +// DocumentFooter callback. func (r renderer) DocumentFooter(out *bytes.Buffer) {} -// Stub GetFlags returns zero. +// GetFlags returns zero. func (r renderer) GetFlags() int { return 0 } diff --git a/middleware/markdown/summary/summary.go b/caddyhttp/markdown/summary/summary.go similarity index 99% rename from middleware/markdown/summary/summary.go rename to caddyhttp/markdown/summary/summary.go index e43a17187..e55bba2c9 100644 --- a/middleware/markdown/summary/summary.go +++ b/caddyhttp/markdown/summary/summary.go @@ -13,6 +13,5 @@ func Markdown(input []byte, wordcount int) []byte { if wordcount > len(words) { wordcount = len(words) } - return bytes.Join(words[0:wordcount], []byte{' '}) } diff --git a/middleware/markdown/template.go b/caddyhttp/markdown/template.go similarity index 85% rename from middleware/markdown/template.go rename to caddyhttp/markdown/template.go index 10ea31c58..fcd8f31a1 100644 --- a/middleware/markdown/template.go +++ b/caddyhttp/markdown/template.go @@ -5,13 +5,13 @@ import ( "io/ioutil" "text/template" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/markdown/metadata" + "github.com/mholt/caddy/caddyhttp/httpserver" + "github.com/mholt/caddy/caddyhttp/markdown/metadata" ) // Data represents a markdown document. type Data struct { - middleware.Context + httpserver.Context Doc map[string]string DocFlags map[string]bool Styles []string @@ -19,15 +19,15 @@ type Data struct { Files []FileInfo } -// Include "overrides" the embedded middleware.Context's Include() +// Include "overrides" the embedded httpserver.Context's Include() // method so that included files have access to d's fields. // Note: using {{template 'template-name' .}} instead might be better. func (d Data) Include(filename string) (string, error) { - return middleware.ContextInclude(filename, d, d.Root) + return httpserver.ContextInclude(filename, d, d.Root) } // execTemplate executes a template given a requestPath, template, and metadata -func execTemplate(c *Config, mdata metadata.Metadata, files []FileInfo, ctx middleware.Context) ([]byte, error) { +func execTemplate(c *Config, mdata metadata.Metadata, files []FileInfo, ctx httpserver.Context) ([]byte, error) { mdData := Data{ Context: ctx, Doc: mdata.Variables, diff --git a/middleware/mime/mime.go b/caddyhttp/mime/mime.go similarity index 74% rename from middleware/mime/mime.go rename to caddyhttp/mime/mime.go index 6990c596d..b215fc8a0 100644 --- a/middleware/mime/mime.go +++ b/caddyhttp/mime/mime.go @@ -4,23 +4,22 @@ import ( "net/http" "path" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// Config represent a mime config. Map from extension to mime-type. +// Config represent a mime config. Map from extension to mime-type. // Note, this should be safe with concurrent read access, as this is // not modified concurrently. type Config map[string]string // Mime sets Content-Type header of requests based on configurations. type Mime struct { - Next middleware.Handler + Next httpserver.Handler Configs Config } -// ServeHTTP implements the middleware.Handler interface. +// ServeHTTP implements the httpserver.Handler interface. func (e Mime) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - // Get a clean /-path, grab the extension ext := path.Ext(path.Clean(r.URL.Path)) diff --git a/middleware/mime/mime_test.go b/caddyhttp/mime/mime_test.go similarity index 87% rename from middleware/mime/mime_test.go rename to caddyhttp/mime/mime_test.go index 4010b0aef..f97fffadc 100644 --- a/middleware/mime/mime_test.go +++ b/caddyhttp/mime/mime_test.go @@ -6,11 +6,10 @@ import ( "net/http/httptest" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestMimeHandler(t *testing.T) { - mimes := Config{ ".html": "text/html", ".txt": "text/plain", @@ -54,8 +53,8 @@ func TestMimeHandler(t *testing.T) { } } -func nextFunc(shouldMime bool, contentType string) middleware.Handler { - return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { +func nextFunc(shouldMime bool, contentType string) httpserver.Handler { + return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { if shouldMime { if w.Header().Get("Content-Type") != contentType { return 0, fmt.Errorf("expected Content-Type: %v, found %v", contentType, r.Header.Get("Content-Type")) diff --git a/caddy/setup/mime.go b/caddyhttp/mime/setup.go similarity index 60% rename from caddy/setup/mime.go rename to caddyhttp/mime/setup.go index 59667dc36..bb5c40e0f 100644 --- a/caddy/setup/mime.go +++ b/caddyhttp/mime/setup.go @@ -1,27 +1,37 @@ -package setup +package mime import ( "fmt" "strings" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/mime" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// Mime configures a new mime middleware instance. -func Mime(c *Controller) (middleware.Middleware, error) { - configs, err := mimeParse(c) - if err != nil { - return nil, err - } - - return func(next middleware.Handler) middleware.Handler { - return mime.Mime{Next: next, Configs: configs} - }, nil +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "mime", + ServerType: "http", + Action: setup, + }) } -func mimeParse(c *Controller) (mime.Config, error) { - configs := mime.Config{} +// setup configures a new mime middleware instance. +func setup(c *caddy.Controller) error { + configs, err := mimeParse(c) + if err != nil { + return err + } + + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Mime{Next: next, Configs: configs} + }) + + return nil +} + +func mimeParse(c *caddy.Controller) (Config, error) { + configs := Config{} for c.Next() { // At least one extension is required @@ -54,7 +64,7 @@ func mimeParse(c *Controller) (mime.Config, error) { } // validateExt checks for valid file name extension. -func validateExt(configs mime.Config, ext string) error { +func validateExt(configs Config, ext string) error { if !strings.HasPrefix(ext, ".") { return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext) } diff --git a/caddy/setup/mime_test.go b/caddyhttp/mime/setup_test.go similarity index 65% rename from caddy/setup/mime_test.go rename to caddyhttp/mime/setup_test.go index 7b11f3d57..3d1fce605 100644 --- a/caddy/setup/mime_test.go +++ b/caddyhttp/mime/setup_test.go @@ -1,30 +1,29 @@ -package setup +package mime import ( "testing" - "github.com/mholt/caddy/middleware/mime" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestMime(t *testing.T) { - - c := NewTestController(`mime .txt text/plain`) - - mid, err := Mime(c) +func TestSetup(t *testing.T) { + err := setup(caddy.NewTestController(`mime .txt text/plain`)) 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, but had 0 instead") } - handler := mid(EmptyNext) - myHandler, ok := handler.(mime.Mime) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Mime) if !ok { t.Fatalf("Expected handler to be type Mime, 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") } @@ -53,8 +52,7 @@ func TestMime(t *testing.T) { {`mime .txt text/plain`, false}, } for i, test := range tests { - c := NewTestController(test.input) - m, err := mimeParse(c) + m, err := mimeParse(caddy.NewTestController(test.input)) if test.shouldErr && err == nil { t.Errorf("Test %v: Expected error but found nil %v", i, m) } else if !test.shouldErr && err != nil { diff --git a/middleware/pprof/pprof.go b/caddyhttp/pprof/pprof.go similarity index 89% rename from middleware/pprof/pprof.go rename to caddyhttp/pprof/pprof.go index 8d8e9c788..9ac089423 100644 --- a/middleware/pprof/pprof.go +++ b/caddyhttp/pprof/pprof.go @@ -4,7 +4,7 @@ import ( "net/http" pp "net/http/pprof" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // BasePath is the base path to match for all pprof requests. @@ -13,14 +13,14 @@ const BasePath = "/debug/pprof" // Handler is a simple struct whose ServeHTTP will delegate pprof // endpoints to their equivalent net/http/pprof handlers. type Handler struct { - Next middleware.Handler + Next httpserver.Handler Mux *http.ServeMux } // ServeHTTP handles requests to BasePath with pprof, or passes // all other requests up the chain. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - if middleware.Path(r.URL.Path).Matches(BasePath) { + if httpserver.Path(r.URL.Path).Matches(BasePath) { h.Mux.ServeHTTP(w, r) return 0, nil } diff --git a/middleware/pprof/pprof_test.go b/caddyhttp/pprof/pprof_test.go similarity index 92% rename from middleware/pprof/pprof_test.go rename to caddyhttp/pprof/pprof_test.go index a9aee20c9..816658694 100644 --- a/middleware/pprof/pprof_test.go +++ b/caddyhttp/pprof/pprof_test.go @@ -6,12 +6,12 @@ import ( "net/http/httptest" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestServeHTTP(t *testing.T) { h := Handler{ - Next: middleware.HandlerFunc(nextHandler), + Next: httpserver.HandlerFunc(nextHandler), Mux: NewMux(), } diff --git a/caddyhttp/pprof/setup.go b/caddyhttp/pprof/setup.go new file mode 100644 index 000000000..1c82b856b --- /dev/null +++ b/caddyhttp/pprof/setup.go @@ -0,0 +1,38 @@ +package pprof + +import ( + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "pprof", + ServerType: "http", + Action: setup, + }) +} + +// setup returns a new instance of a pprof handler. It accepts no arguments or options. +func setup(c *caddy.Controller) error { + found := false + + for c.Next() { + if found { + return c.Err("pprof can only be specified once") + } + if len(c.RemainingArgs()) != 0 { + return c.ArgErr() + } + if c.NextBlock() { + return c.ArgErr() + } + found = true + } + + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return &Handler{Next: next, Mux: NewMux()} + }) + + return nil +} diff --git a/caddy/setup/pprof_test.go b/caddyhttp/pprof/setup_test.go similarity index 75% rename from caddy/setup/pprof_test.go rename to caddyhttp/pprof/setup_test.go index ac9375af7..d53257d2b 100644 --- a/caddy/setup/pprof_test.go +++ b/caddyhttp/pprof/setup_test.go @@ -1,8 +1,12 @@ -package setup +package pprof -import "testing" +import ( + "testing" -func TestPProf(t *testing.T) { + "github.com/mholt/caddy" +) + +func TestSetup(t *testing.T) { tests := []struct { input string shouldErr bool @@ -17,8 +21,7 @@ func TestPProf(t *testing.T) { pprof`, true}, } for i, test := range tests { - c := NewTestController(test.input) - _, err := PProf(c) + err := setup(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 { diff --git a/middleware/proxy/policy.go b/caddyhttp/proxy/policy.go similarity index 100% rename from middleware/proxy/policy.go rename to caddyhttp/proxy/policy.go diff --git a/middleware/proxy/policy_test.go b/caddyhttp/proxy/policy_test.go similarity index 100% rename from middleware/proxy/policy_test.go rename to caddyhttp/proxy/policy_test.go diff --git a/middleware/proxy/proxy.go b/caddyhttp/proxy/proxy.go similarity index 91% rename from middleware/proxy/proxy.go rename to caddyhttp/proxy/proxy.go index 264444d9a..4cdce972f 100644 --- a/middleware/proxy/proxy.go +++ b/caddyhttp/proxy/proxy.go @@ -1,4 +1,4 @@ -// Package proxy is middleware that proxies requests. +// Package proxy is middleware that proxies HTTP requests. package proxy import ( @@ -10,14 +10,14 @@ import ( "sync/atomic" "time" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) var errUnreachable = errors.New("unreachable backend") // Proxy represents a middleware instance that can proxy requests. type Proxy struct { - Next middleware.Handler + Next httpserver.Handler Upstreams []Upstream } @@ -75,15 +75,15 @@ func (uh *UpstreamHost) Available() bool { // immediate retries until this duration ends or we get a nil host. var tryDuration = 60 * time.Second -// ServeHTTP satisfies the middleware.Handler interface. +// ServeHTTP satisfies the httpserver.Handler interface. func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, upstream := range p.Upstreams { - if !middleware.Path(r.URL.Path).Matches(upstream.From()) || + if !httpserver.Path(r.URL.Path).Matches(upstream.From()) || !upstream.AllowedPath(r.URL.Path) { continue } - var replacer middleware.Replacer + var replacer httpserver.Replacer start := time.Now() outreq := createUpstreamRequest(r) @@ -95,7 +95,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { if host == nil { return http.StatusBadGateway, errUnreachable } - if rr, ok := w.(*middleware.ResponseRecorder); ok && rr.Replacer != nil { + if rr, ok := w.(*httpserver.ResponseRecorder); ok && rr.Replacer != nil { rr.Replacer.Set("upstream", host.Name) } @@ -103,7 +103,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { if host.UpstreamHeaders != nil { if replacer == nil { rHost := r.Host - replacer = middleware.NewReplacer(r, nil, "") + replacer = httpserver.NewReplacer(r, nil, "") outreq.Host = rHost } if v, ok := host.UpstreamHeaders["Host"]; ok { @@ -120,7 +120,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { if host.DownstreamHeaders != nil { if replacer == nil { rHost := r.Host - replacer = middleware.NewReplacer(r, nil, "") + replacer = httpserver.NewReplacer(r, nil, "") outreq.Host = rHost } //Creates a function that is used to update headers the response received by the reverse proxy @@ -196,7 +196,7 @@ func createUpstreamRequest(r *http.Request) *http.Request { return outreq } -func createRespHeaderUpdateFn(rules http.Header, replacer middleware.Replacer) respUpdateFn { +func createRespHeaderUpdateFn(rules http.Header, replacer httpserver.Replacer) respUpdateFn { return func(resp *http.Response) { newHeaders := createHeadersByRules(rules, resp.Header, replacer) for h, v := range newHeaders { @@ -205,7 +205,7 @@ func createRespHeaderUpdateFn(rules http.Header, replacer middleware.Replacer) r } } -func createHeadersByRules(rules http.Header, base http.Header, repl middleware.Replacer) http.Header { +func createHeadersByRules(rules http.Header, base http.Header, repl httpserver.Replacer) http.Header { newHeaders := make(http.Header) for header, values := range rules { if strings.HasPrefix(header, "+") { diff --git a/middleware/proxy/proxy_test.go b/caddyhttp/proxy/proxy_test.go similarity index 98% rename from middleware/proxy/proxy_test.go rename to caddyhttp/proxy/proxy_test.go index 592b782ea..4c04fd2c2 100644 --- a/middleware/proxy/proxy_test.go +++ b/caddyhttp/proxy/proxy_test.go @@ -18,7 +18,7 @@ import ( "testing" "time" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" "golang.org/x/net/websocket" ) @@ -57,8 +57,8 @@ func TestReverseProxy(t *testing.T) { } // Make sure {upstream} placeholder is set - rr := middleware.NewResponseRecorder(httptest.NewRecorder()) - rr.Replacer = middleware.NewReplacer(r, rr, "-") + rr := httpserver.NewResponseRecorder(httptest.NewRecorder()) + rr.Replacer = httpserver.NewReplacer(r, rr, "-") p.ServeHTTP(rr, r) @@ -389,7 +389,7 @@ func TestUpstreamHeadersUpdate(t *testing.T) { p.ServeHTTP(w, r) - replacer := middleware.NewReplacer(r, nil, "") + replacer := httpserver.NewReplacer(r, nil, "") headerKey := "Merge-Me" values, ok := actualHeaders[headerKey] @@ -453,7 +453,7 @@ func TestDownstreamHeadersUpdate(t *testing.T) { p.ServeHTTP(w, r) - replacer := middleware.NewReplacer(r, nil, "") + replacer := httpserver.NewReplacer(r, nil, "") actualHeaders := w.Header() headerKey := "Merge-Me" diff --git a/middleware/proxy/reverseproxy.go b/caddyhttp/proxy/reverseproxy.go similarity index 100% rename from middleware/proxy/reverseproxy.go rename to caddyhttp/proxy/reverseproxy.go diff --git a/caddyhttp/proxy/setup.go b/caddyhttp/proxy/setup.go new file mode 100644 index 000000000..07d9ac953 --- /dev/null +++ b/caddyhttp/proxy/setup.go @@ -0,0 +1,26 @@ +package proxy + +import ( + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "proxy", + ServerType: "http", + Action: setup, + }) +} + +// setup configures a new Proxy middleware instance. +func setup(c *caddy.Controller) error { + upstreams, err := NewStaticUpstreams(c.Dispenser) + if err != nil { + return err + } + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Proxy{Next: next, Upstreams: upstreams} + }) + return nil +} diff --git a/caddy/setup/proxy_test.go b/caddyhttp/proxy/setup_test.go similarity index 89% rename from caddy/setup/proxy_test.go rename to caddyhttp/proxy/setup_test.go index 3d6d04a09..c48d3479a 100644 --- a/caddy/setup/proxy_test.go +++ b/caddyhttp/proxy/setup_test.go @@ -1,13 +1,14 @@ -package setup +package proxy import ( "reflect" "testing" - "github.com/mholt/caddy/middleware/proxy" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestUpstream(t *testing.T) { +func TestSetup(t *testing.T) { for i, test := range []struct { input string shouldErr bool @@ -111,17 +112,20 @@ func TestUpstream(t *testing.T) { }, }, } { - receivedFunc, err := Proxy(NewTestController(test.input)) + err := setup(caddy.NewTestController(test.input)) if err != nil && !test.shouldErr { t.Errorf("Test case #%d received an error of %v", i, err) } else if test.shouldErr { continue } - upstreams := receivedFunc(nil).(proxy.Proxy).Upstreams + mids := httpserver.GetConfig("").Middleware() + mid := mids[len(mids)-1] + + upstreams := mid(nil).(Proxy).Upstreams for _, upstream := range upstreams { val := reflect.ValueOf(upstream).Elem() - hosts := val.FieldByName("Hosts").Interface().(proxy.HostPool) + hosts := val.FieldByName("Hosts").Interface().(HostPool) if len(hosts) != len(test.expectedHosts) { t.Errorf("Test case #%d expected %d hosts but received %d", i, len(test.expectedHosts), len(hosts)) } else { diff --git a/middleware/proxy/upstream.go b/caddyhttp/proxy/upstream.go similarity index 96% rename from middleware/proxy/upstream.go rename to caddyhttp/proxy/upstream.go index a1d9fcfce..4dc78e820 100644 --- a/middleware/proxy/upstream.go +++ b/caddyhttp/proxy/upstream.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "github.com/mholt/caddy/caddy/parse" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyfile" + "github.com/mholt/caddy/caddyhttp/httpserver" ) var ( @@ -40,7 +40,7 @@ type staticUpstream struct { // NewStaticUpstreams parses the configuration input and sets up // static upstreams for the proxy middleware. -func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) { +func NewStaticUpstreams(c caddyfile.Dispenser) ([]Upstream, error) { var upstreams []Upstream for c.Next() { upstream := &staticUpstream{ @@ -195,7 +195,7 @@ func parseUpstream(u string) ([]string, error) { } -func parseBlock(c *parse.Dispenser, u *staticUpstream) error { +func parseBlock(c *caddyfile.Dispenser, u *staticUpstream) error { switch c.Val() { case "policy": if !c.NextArg() { @@ -337,7 +337,7 @@ func (u *staticUpstream) Select() *UpstreamHost { func (u *staticUpstream) AllowedPath(requestPath string) bool { for _, ignoredSubPath := range u.IgnoredSubPaths { - if middleware.Path(path.Clean(requestPath)).Matches(path.Join(u.From(), ignoredSubPath)) { + if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(u.From(), ignoredSubPath)) { return false } } diff --git a/middleware/proxy/upstream_test.go b/caddyhttp/proxy/upstream_test.go similarity index 100% rename from middleware/proxy/upstream_test.go rename to caddyhttp/proxy/upstream_test.go diff --git a/middleware/redirect/redirect.go b/caddyhttp/redirect/redirect.go similarity index 86% rename from middleware/redirect/redirect.go rename to caddyhttp/redirect/redirect.go index 04fb1c63a..edb7caea5 100644 --- a/middleware/redirect/redirect.go +++ b/caddyhttp/redirect/redirect.go @@ -7,20 +7,20 @@ import ( "html" "net/http" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // Redirect is middleware to respond with HTTP redirects type Redirect struct { - Next middleware.Handler + Next httpserver.Handler Rules []Rule } -// ServeHTTP implements the middleware.Handler interface. +// ServeHTTP implements the httpserver.Handler interface. func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range rd.Rules { if (rule.FromPath == "/" || r.URL.Path == rule.FromPath) && schemeMatches(rule, r) { - to := middleware.NewReplacer(r, nil, "").Replace(rule.To) + to := httpserver.NewReplacer(r, nil, "").Replace(rule.To) if rule.Meta { safeTo := html.EscapeString(to) fmt.Fprintf(w, metaRedir, safeTo, safeTo) @@ -54,4 +54,5 @@ const metaRedir = ` Redirecting... -` + +` diff --git a/middleware/redirect/redirect_test.go b/caddyhttp/redirect/redirect_test.go similarity index 97% rename from middleware/redirect/redirect_test.go rename to caddyhttp/redirect/redirect_test.go index 3107921af..b6f8f74d0 100644 --- a/middleware/redirect/redirect_test.go +++ b/caddyhttp/redirect/redirect_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestRedirect(t *testing.T) { @@ -42,7 +42,7 @@ func TestRedirect(t *testing.T) { var nextCalled bool re := Redirect{ - Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { nextCalled = true return 0, nil }), diff --git a/caddy/setup/redir.go b/caddyhttp/redirect/setup.go similarity index 83% rename from caddy/setup/redir.go rename to caddyhttp/redirect/setup.go index 63488f4ab..31fbd7afd 100644 --- a/caddy/setup/redir.go +++ b/caddyhttp/redirect/setup.go @@ -1,29 +1,41 @@ -package setup +package redirect import ( "net/http" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/redirect" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// Redir configures a new Redirect middleware instance. -func Redir(c *Controller) (middleware.Middleware, error) { - rules, err := redirParse(c) - if err != nil { - return nil, err - } - - return func(next middleware.Handler) middleware.Handler { - return redirect.Redirect{Next: next, Rules: rules} - }, nil +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "redir", + ServerType: "http", + Action: setup, + }) } -func redirParse(c *Controller) ([]redirect.Rule, error) { - var redirects []redirect.Rule +// setup configures a new Redirect middleware instance. +func setup(c *caddy.Controller) error { + rules, err := redirParse(c) + if err != nil { + return err + } + + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Redirect{Next: next, Rules: rules} + }) + + return nil +} + +func redirParse(c *caddy.Controller) ([]Rule, error) { + var redirects []Rule + + cfg := httpserver.GetConfig(c.Key) // setRedirCode sets the redirect code for rule if it can, or returns an error - setRedirCode := func(code string, rule *redirect.Rule) error { + setRedirCode := func(code string, rule *Rule) error { if code == "meta" { rule.Meta = true } else if codeNumber, ok := httpRedirs[code]; ok { @@ -36,7 +48,7 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { // checkAndSaveRule checks the rule for validity (except the redir code) // and saves it if it's valid, or returns an error. - checkAndSaveRule := func(rule redirect.Rule) error { + checkAndSaveRule := func(rule Rule) error { if rule.FromPath == rule.To { return c.Err("'from' and 'to' values of redirect rule cannot be the same") } @@ -58,9 +70,9 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { for c.NextBlock() { hadOptionalBlock = true - var rule redirect.Rule + var rule Rule - if c.Config.TLS.Enabled { + if cfg.TLS.Enabled { rule.FromScheme = "https" } else { rule.FromScheme = "http" @@ -115,9 +127,9 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { } if !hadOptionalBlock { - var rule redirect.Rule + var rule Rule - if c.Config.TLS.Enabled { + if cfg.TLS.Enabled { rule.FromScheme = "https" } else { rule.FromScheme = "http" diff --git a/caddy/setup/redir_test.go b/caddyhttp/redirect/setup_test.go similarity index 68% rename from caddy/setup/redir_test.go rename to caddyhttp/redirect/setup_test.go index 0285784fa..c4774cfaf 100644 --- a/caddy/setup/redir_test.go +++ b/caddyhttp/redirect/setup_test.go @@ -1,55 +1,57 @@ -package setup +package redirect import ( "testing" - "github.com/mholt/caddy/middleware/redirect" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestRedir(t *testing.T) { +func TestSetup(t *testing.T) { for j, test := range []struct { input string shouldErr bool - expectedRules []redirect.Rule + expectedRules []Rule }{ // test case #0 tests the recognition of a valid HTTP status code defined outside of block statement - {"redir 300 {\n/ /foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 300}}}, + {"redir 300 {\n/ /foo\n}", false, []Rule{{FromPath: "/", To: "/foo", Code: 300}}}, // test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement - {"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{{}}}, + {"redir 9000 {\n/ /foo\n}", true, []Rule{{}}}, // test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement - {"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{{}}}, + {"redir 300 {\n/ /foo 9000\n}", true, []Rule{{}}}, // test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement - {"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{{}}}, + {"redir 9000 {\n/ /foo 300\n}", true, []Rule{{}}}, // test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently - {"redir 302 {\n/foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 302}}}, + {"redir 302 {\n/foo\n}", false, []Rule{{FromPath: "/", To: "/foo", Code: 302}}}, // test case #5 tests the recognition of a TO and From redirection in a block statement - {"redir {\n/bar /foo 303\n}", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}}, + {"redir {\n/bar /foo 303\n}", false, []Rule{{FromPath: "/bar", To: "/foo", Code: 303}}}, // test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently - {"redir /foo", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 301}}}, + {"redir /foo", false, []Rule{{FromPath: "/", To: "/foo", Code: 301}}}, // test case #7 tests the recognition of a TO and From redirection in a non-block statement - {"redir /bar /foo 303", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}}, + {"redir /bar /foo 303", false, []Rule{{FromPath: "/bar", To: "/foo", Code: 303}}}, // test case #8 tests the recognition of multiple redirections - {"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}}, + {"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}}, // test case #9 tests the detection of duplicate redirections - {"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{{}}}, + {"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []Rule{{}}}, } { - recievedFunc, err := Redir(NewTestController(test.input)) + err := setup(caddy.NewTestController(test.input)) if err != nil && !test.shouldErr { t.Errorf("Test case #%d recieved an error of %v", j, err) } else if test.shouldErr { continue } - recievedRules := recievedFunc(nil).(redirect.Redirect).Rules + mids := httpserver.GetConfig("").Middleware() + recievedRules := mids[len(mids)-1](nil).(Redirect).Rules for i, recievedRule := range recievedRules { if recievedRule.FromPath != test.expectedRules[i].FromPath { diff --git a/middleware/rewrite/condition.go b/caddyhttp/rewrite/condition.go similarity index 95% rename from middleware/rewrite/condition.go rename to caddyhttp/rewrite/condition.go index 1431afc9c..97b0e96aa 100644 --- a/middleware/rewrite/condition.go +++ b/caddyhttp/rewrite/condition.go @@ -6,7 +6,7 @@ import ( "regexp" "strings" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // Operators @@ -25,8 +25,8 @@ func operatorError(operator string) error { return fmt.Errorf("Invalid operator %v", operator) } -func newReplacer(r *http.Request) middleware.Replacer { - return middleware.NewReplacer(r, nil, "") +func newReplacer(r *http.Request) httpserver.Replacer { + return httpserver.NewReplacer(r, nil, "") } // condition is a rewrite condition. diff --git a/middleware/rewrite/condition_test.go b/caddyhttp/rewrite/condition_test.go similarity index 100% rename from middleware/rewrite/condition_test.go rename to caddyhttp/rewrite/condition_test.go diff --git a/middleware/rewrite/rewrite.go b/caddyhttp/rewrite/rewrite.go similarity index 96% rename from middleware/rewrite/rewrite.go rename to caddyhttp/rewrite/rewrite.go index 1c2e26006..7567f5d85 100644 --- a/middleware/rewrite/rewrite.go +++ b/caddyhttp/rewrite/rewrite.go @@ -11,7 +11,7 @@ import ( "regexp" "strings" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // Result is the result of a rewrite @@ -29,12 +29,12 @@ const ( // Rewrite is middleware to rewrite request locations internally before being handled. type Rewrite struct { - Next middleware.Handler + Next httpserver.Handler FileSys http.FileSystem Rules []Rule } -// ServeHTTP implements the middleware.Handler interface. +// ServeHTTP implements the httpserver.Handler interface. func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { outer: for _, rule := range rw.Rules { @@ -142,7 +142,7 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) replacer := newReplacer(req) // validate base - if !middleware.Path(rPath).Matches(r.Base) { + if !httpserver.Path(rPath).Matches(r.Base) { return } diff --git a/middleware/rewrite/rewrite_test.go b/caddyhttp/rewrite/rewrite_test.go similarity index 97% rename from middleware/rewrite/rewrite_test.go rename to caddyhttp/rewrite/rewrite_test.go index 2baf91219..c2c59afa1 100644 --- a/middleware/rewrite/rewrite_test.go +++ b/caddyhttp/rewrite/rewrite_test.go @@ -7,12 +7,12 @@ import ( "strings" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestRewrite(t *testing.T) { rw := Rewrite{ - Next: middleware.HandlerFunc(urlPrinter), + Next: httpserver.HandlerFunc(urlPrinter), Rules: []Rule{ NewSimpleRule("/from", "/to"), NewSimpleRule("/a", "/b"), diff --git a/caddy/setup/rewrite.go b/caddyhttp/rewrite/setup.go similarity index 66% rename from caddy/setup/rewrite.go rename to caddyhttp/rewrite/setup.go index b270c93dd..317b21d4d 100644 --- a/caddy/setup/rewrite.go +++ b/caddyhttp/rewrite/setup.go @@ -1,36 +1,48 @@ -package setup +package rewrite import ( "net/http" "strconv" "strings" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/rewrite" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// Rewrite configures a new Rewrite middleware instance. -func Rewrite(c *Controller) (middleware.Middleware, error) { - rewrites, err := rewriteParse(c) - if err != nil { - return nil, err - } - - return func(next middleware.Handler) middleware.Handler { - return rewrite.Rewrite{ - Next: next, - FileSys: http.Dir(c.Root), - Rules: rewrites, - } - }, nil +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "rewrite", + ServerType: "http", + Action: setup, + }) } -func rewriteParse(c *Controller) ([]rewrite.Rule, error) { - var simpleRules []rewrite.Rule - var regexpRules []rewrite.Rule +// setup configures a new Rewrite middleware instance. +func setup(c *caddy.Controller) error { + rewrites, err := rewriteParse(c) + if err != nil { + return err + } + + cfg := httpserver.GetConfig(c.Key) + + cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Rewrite{ + Next: next, + FileSys: http.Dir(cfg.Root), + Rules: rewrites, + } + }) + + return nil +} + +func rewriteParse(c *caddy.Controller) ([]Rule, error) { + var simpleRules []Rule + var regexpRules []Rule for c.Next() { - var rule rewrite.Rule + var rule Rule var err error var base = "/" var pattern, to string @@ -39,7 +51,7 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) { args := c.RemainingArgs() - var ifs []rewrite.If + var ifs []If switch len(args) { case 1: @@ -70,7 +82,7 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) { if len(args1) != 3 { return nil, c.ArgErr() } - ifCond, err := rewrite.NewIf(args1[0], args1[1], args1[2]) + ifCond, err := NewIf(args1[0], args1[1], args1[2]) if err != nil { return nil, err } @@ -91,14 +103,14 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) { if to == "" && status == 0 { return nil, c.ArgErr() } - if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil { + if rule, err = NewComplexRule(base, pattern, to, status, ext, ifs); err != nil { return nil, err } regexpRules = append(regexpRules, rule) // the only unhandled case is 2 and above default: - rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " ")) + rule = NewSimpleRule(args[0], strings.Join(args[1:], " ")) simpleRules = append(simpleRules, rule) } diff --git a/caddy/setup/rewrite_test.go b/caddyhttp/rewrite/setup_test.go similarity index 57% rename from caddy/setup/rewrite_test.go rename to caddyhttp/rewrite/setup_test.go index d252ed904..ec22aa3c3 100644 --- a/caddy/setup/rewrite_test.go +++ b/caddyhttp/rewrite/setup_test.go @@ -1,31 +1,31 @@ -package setup +package rewrite import ( "fmt" "regexp" "testing" - "github.com/mholt/caddy/middleware/rewrite" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestRewrite(t *testing.T) { - c := NewTestController(`rewrite /from /to`) - - mid, err := Rewrite(c) +func TestSetup(t *testing.T) { + err := setup(caddy.NewTestController(`rewrite /from /to`)) 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.(rewrite.Rewrite) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Rewrite) if !ok { t.Fatalf("Expected handler to be type Rewrite, 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") } @@ -38,26 +38,25 @@ func TestRewriteParse(t *testing.T) { simpleTests := []struct { input string shouldErr bool - expected []rewrite.Rule + expected []Rule }{ - {`rewrite /from /to`, false, []rewrite.Rule{ - rewrite.SimpleRule{From: "/from", To: "/to"}, + {`rewrite /from /to`, false, []Rule{ + SimpleRule{From: "/from", To: "/to"}, }}, {`rewrite /from /to - rewrite a b`, false, []rewrite.Rule{ - rewrite.SimpleRule{From: "/from", To: "/to"}, - rewrite.SimpleRule{From: "a", To: "b"}, + rewrite a b`, false, []Rule{ + SimpleRule{From: "/from", To: "/to"}, + SimpleRule{From: "a", To: "b"}, }}, - {`rewrite a`, true, []rewrite.Rule{}}, - {`rewrite`, true, []rewrite.Rule{}}, - {`rewrite a b c`, false, []rewrite.Rule{ - rewrite.SimpleRule{From: "a", To: "b c"}, + {`rewrite a`, true, []Rule{}}, + {`rewrite`, true, []Rule{}}, + {`rewrite a b c`, false, []Rule{ + SimpleRule{From: "a", To: "b c"}, }}, } for i, test := range simpleTests { - c := NewTestController(test.input) - actual, err := rewriteParse(c) + actual, err := rewriteParse(caddy.NewTestController(test.input)) if err == nil && test.shouldErr { t.Errorf("Test %d didn't error, but it should have", i) @@ -73,8 +72,8 @@ func TestRewriteParse(t *testing.T) { } for j, e := range test.expected { - actualRule := actual[j].(rewrite.SimpleRule) - expectedRule := e.(rewrite.SimpleRule) + actualRule := actual[j].(SimpleRule) + expectedRule := e.(SimpleRule) if actualRule.From != expectedRule.From { t.Errorf("Test %d, rule %d: Expected From=%s, got %s", @@ -91,20 +90,20 @@ func TestRewriteParse(t *testing.T) { regexpTests := []struct { input string shouldErr bool - expected []rewrite.Rule + expected []Rule }{ {`rewrite { r .* to /to /index.php? - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")}, + }`, false, []Rule{ + &ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")}, }}, {`rewrite { regexp .* to /to ext / html txt - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")}, + }`, false, []Rule{ + &ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")}, }}, {`rewrite /path { r rr @@ -114,82 +113,81 @@ func TestRewriteParse(t *testing.T) { regexp [a-z]+ to /to /to2 } - `, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")}, - &rewrite.ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")}, + `, false, []Rule{ + &ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")}, + &ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")}, }}, {`rewrite { r .* - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, {`rewrite { - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, - {`rewrite /`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + {`rewrite /`, true, []Rule{ + &ComplexRule{}, }}, {`rewrite { to /to if {path} is a - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{{A: "{path}", Operator: "is", B: "a"}}}, + }`, false, []Rule{ + &ComplexRule{Base: "/", To: "/to", Ifs: []If{{A: "{path}", Operator: "is", B: "a"}}}, }}, {`rewrite { status 500 - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, {`rewrite { status 400 - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", Status: 400}, + }`, false, []Rule{ + &ComplexRule{Base: "/", Status: 400}, }}, {`rewrite { to /to status 400 - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", To: "/to", Status: 400}, + }`, false, []Rule{ + &ComplexRule{Base: "/", To: "/to", Status: 400}, }}, {`rewrite { status 399 - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, {`rewrite { status 200 - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", Status: 200}, + }`, false, []Rule{ + &ComplexRule{Base: "/", Status: 200}, }}, {`rewrite { to /to status 200 - }`, false, []rewrite.Rule{ - &rewrite.ComplexRule{Base: "/", To: "/to", Status: 200}, + }`, false, []Rule{ + &ComplexRule{Base: "/", To: "/to", Status: 200}, }}, {`rewrite { status 199 - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, {`rewrite { status 0 - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, {`rewrite { to /to status 0 - }`, true, []rewrite.Rule{ - &rewrite.ComplexRule{}, + }`, true, []Rule{ + &ComplexRule{}, }}, } for i, test := range regexpTests { - c := NewTestController(test.input) - actual, err := rewriteParse(c) + actual, err := rewriteParse(caddy.NewTestController(test.input)) if err == nil && test.shouldErr { t.Errorf("Test %d didn't error, but it should have", i) @@ -205,8 +203,8 @@ func TestRewriteParse(t *testing.T) { } for j, e := range test.expected { - actualRule := actual[j].(*rewrite.ComplexRule) - expectedRule := e.(*rewrite.ComplexRule) + actualRule := actual[j].(*ComplexRule) + expectedRule := e.(*ComplexRule) if actualRule.Base != expectedRule.Base { t.Errorf("Test %d, rule %d: Expected Base=%s, got %s", diff --git a/middleware/rewrite/testdata/testdir/empty b/caddyhttp/rewrite/testdata/testdir/empty similarity index 100% rename from middleware/rewrite/testdata/testdir/empty rename to caddyhttp/rewrite/testdata/testdir/empty diff --git a/middleware/rewrite/testdata/testfile b/caddyhttp/rewrite/testdata/testfile similarity index 100% rename from middleware/rewrite/testdata/testfile rename to caddyhttp/rewrite/testdata/testfile diff --git a/middleware/rewrite/to.go b/caddyhttp/rewrite/to.go similarity index 95% rename from middleware/rewrite/to.go rename to caddyhttp/rewrite/to.go index 7a38349ff..2cfe1de46 100644 --- a/middleware/rewrite/to.go +++ b/caddyhttp/rewrite/to.go @@ -7,13 +7,13 @@ import ( "path" "strings" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // To attempts rewrite. It attempts to rewrite to first valid path // or the last path if none of the paths are valid. // Returns true if rewrite is successful and false otherwise. -func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Replacer) Result { +func To(fs http.FileSystem, r *http.Request, to string, replacer httpserver.Replacer) Result { tos := strings.Fields(to) // try each rewrite paths diff --git a/middleware/rewrite/to_test.go b/caddyhttp/rewrite/to_test.go similarity index 100% rename from middleware/rewrite/to_test.go rename to caddyhttp/rewrite/to_test.go diff --git a/caddyhttp/root/root.go b/caddyhttp/root/root.go new file mode 100644 index 000000000..b4e485d1f --- /dev/null +++ b/caddyhttp/root/root.go @@ -0,0 +1,42 @@ +package root + +import ( + "log" + "os" + + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "root", + ServerType: "http", + Action: setupRoot, + }) +} + +func setupRoot(c *caddy.Controller) error { + config := httpserver.GetConfig(c.Key) + + for c.Next() { + if !c.NextArg() { + return c.ArgErr() + } + config.Root = c.Val() + } + + // Check if root path exists + _, err := os.Stat(config.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", config.Root) + } else { + return c.Errf("Unable to access root path '%s': %v", config.Root, err) + } + } + + return nil +} diff --git a/caddy/setup/root_test.go b/caddyhttp/root/root_test.go similarity index 83% rename from caddy/setup/root_test.go rename to caddyhttp/root/root_test.go index 8b38e6d04..20b2c7a9b 100644 --- a/caddy/setup/root_test.go +++ b/caddyhttp/root/root_test.go @@ -1,4 +1,4 @@ -package setup +package root import ( "fmt" @@ -7,9 +7,13 @@ import ( "path/filepath" "strings" "testing" + + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestRoot(t *testing.T) { + cfg := httpserver.GetConfig("") // Predefined error substrings parseErrContent := "Parse error:" @@ -61,8 +65,8 @@ func TestRoot(t *testing.T) { } for i, test := range tests { - c := NewTestController(test.input) - mid, err := Root(c) + c := caddy.NewTestController(test.input) + err := setupRoot(c) if test.shouldErr && err == nil { t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) @@ -78,14 +82,9 @@ func TestRoot(t *testing.T) { } } - // the Root method always returns a nil middleware - if mid != nil { - t.Errorf("Middware, returned from Root() was not nil: %v", mid) - } - - // check c.Root only if we are in a positive test. - if !test.shouldErr && test.expectedRoot != c.Root { - t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root) + // check root only if we are in a positive test. + if !test.shouldErr && test.expectedRoot != cfg.Root { + t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, cfg.Root) } } } @@ -93,16 +92,13 @@ func TestRoot(t *testing.T) { // getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned. func getTempDirPath() (string, error) { tempDir := os.TempDir() - _, err := os.Stat(tempDir) if err != nil { return "", err } - return tempDir, nil } func getInaccessiblePath(file string) string { - // null byte in filename is not allowed on Windows AND unix - return filepath.Join("C:", "file\x00name") + return filepath.Join("C:", "file\x00name") // null byte in filename is not allowed on Windows AND unix } diff --git a/middleware/fileserver.go b/caddyhttp/staticfiles/fileserver.go similarity index 68% rename from middleware/fileserver.go rename to caddyhttp/staticfiles/fileserver.go index b1c3d66d5..a2b874f8b 100644 --- a/middleware/fileserver.go +++ b/caddyhttp/staticfiles/fileserver.go @@ -1,4 +1,4 @@ -package middleware +package staticfiles import ( "fmt" @@ -7,59 +7,51 @@ import ( "os" "path" "path/filepath" + "runtime" "strconv" "strings" ) -// This file contains a standard way for Caddy middleware -// to load files from the file system given a request -// URI and path to site root. Other middleware that load -// files should use these facilities. - // FileServer implements a production-ready file server // and is the 'default' handler for all requests to Caddy. -// It simply loads and serves the URI requested. If Caddy is -// run without any extra configuration/directives, this is the -// only middleware handler that runs. It is not in its own -// folder like most other middleware handlers because it does -// not require a directive. It is a special case. -// -// FileServer is adapted from the one in net/http by -// the Go authors. Significant modifications have been made. +// It simply loads and serves the URI requested. FileServer +// is adapted from the one in net/http by the Go authors. +// Significant modifications have been made. // // Original license: // // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -func FileServer(root http.FileSystem, hide []string) Handler { - return &fileHandler{root: root, hide: hide} +type FileServer struct { + // Jailed disk access + Root http.FileSystem + + // List of files to treat as "Not Found" + Hide []string } -type fileHandler struct { - root http.FileSystem - hide []string // list of files to treat as "Not Found" -} - -func (fh *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - // r.URL.Path has already been cleaned in caddy/server by path.Clean(). +// ServeHTTP serves static files for r according to fs's configuration. +func (fs FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + // r.URL.Path has already been cleaned by Caddy. if r.URL.Path == "" { r.URL.Path = "/" } - return fh.serveFile(w, r, r.URL.Path) + return fs.serveFile(w, r, r.URL.Path) } // serveFile writes the specified file to the HTTP response. // name is '/'-separated, not filepath.Separator. -func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name string) (int, error) { +func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request, name string) (int, error) { // Prevent absolute path access on Windows. // TODO remove when stdlib http.Dir fixes this. - if runtimeGoos == "windows" { + if runtime.GOOS == "windows" { if filepath.IsAbs(name[1:]) { return http.StatusNotFound, nil } } - f, err := fh.root.Open(name) + + f, err := fs.Root.Open(name) if err != nil { if os.IsNotExist(err) { return http.StatusNotFound, nil @@ -104,7 +96,7 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st if d.IsDir() { for _, indexPage := range IndexPages { index := strings.TrimSuffix(name, "/") + "/" + indexPage - ff, err := fh.root.Open(index) + ff, err := fs.Root.Open(index) if err == nil { // this defer does not leak fds because previous iterations // of the loop must have had an err, so nothing to close @@ -126,12 +118,11 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st return http.StatusNotFound, nil } - // If file is on hide list. - if fh.isHidden(d) { + if fs.isHidden(d) { return http.StatusNotFound, nil } - // Add ETag header + // Experimental ETag header e := fmt.Sprintf(`W/"%x-%x"`, d.ModTime().Unix(), d.Size()) w.Header().Set("ETag", e) @@ -143,12 +134,11 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st } // isHidden checks if file with FileInfo d is on hide list. -func (fh fileHandler) isHidden(d os.FileInfo) bool { +func (fs FileServer) isHidden(d os.FileInfo) bool { // If the file is supposed to be hidden, return a 404 - // (TODO: If the slice gets large, a set may be faster) - for _, hiddenPath := range fh.hide { + for _, hiddenPath := range fs.Hide { // Check if the served file is exactly the hidden file. - if hFile, err := fh.root.Open(hiddenPath); err == nil { + if hFile, err := fs.Root.Open(hiddenPath); err == nil { fs, _ := hFile.Stat() hFile.Close() if os.SameFile(d, fs) { @@ -160,8 +150,8 @@ func (fh fileHandler) isHidden(d os.FileInfo) bool { } // redirect is taken from http.localRedirect of the std lib. It -// sends an HTTP redirect to the client but will preserve the -// query string for the new path. +// sends an HTTP permanent redirect to the client but will +// preserve the query string for the new path. func redirect(w http.ResponseWriter, r *http.Request, newPath string) { if q := r.URL.RawQuery; q != "" { newPath += "?" + q diff --git a/middleware/fileserver_test.go b/caddyhttp/staticfiles/fileserver_test.go similarity index 90% rename from middleware/fileserver_test.go rename to caddyhttp/staticfiles/fileserver_test.go index 40d369a8b..6c77cec1a 100644 --- a/middleware/fileserver_test.go +++ b/caddyhttp/staticfiles/fileserver_test.go @@ -1,4 +1,4 @@ -package middleware +package staticfiles import ( "errors" @@ -44,7 +44,10 @@ func TestServeHTTP(t *testing.T) { beforeServeHTTPTest(t) defer afterServeHTTPTest(t) - fileserver := FileServer(http.Dir(testWebRoot), []string{"dir/hidden.html"}) + fileserver := FileServer{ + Root: http.Dir(testWebRoot), + Hide: []string{"dir/hidden.html"}, + } movedPermanently := "Moved Permanently" @@ -169,22 +172,22 @@ func TestServeHTTP(t *testing.T) { // check if error matches expectations if err != nil { - t.Errorf(getTestPrefix(i)+"Serving file at %s failed. Error was: %v", test.url, err) + t.Errorf("Test %d: Serving file at %s failed. Error was: %v", i, test.url, err) } // check status code if test.expectedStatus != status { - t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) + t.Errorf("Test %d: Expected status %d, found %d", i, test.expectedStatus, status) } // check etag if test.expectedEtag != etag { - t.Errorf(getTestPrefix(i)+"Expected Etag header %d, found %d", test.expectedEtag, etag) + t.Errorf("Test %d: Expected Etag header %s, found %s", i, test.expectedEtag, etag) } // check body content if !strings.Contains(responseRecorder.Body.String(), test.expectedBodyContent) { - t.Errorf(getTestPrefix(i)+"Expected body to contain %q, found %q", test.expectedBodyContent, responseRecorder.Body.String()) + t.Errorf("Test %d: Expected body to contain %q, found %q", i, test.expectedBodyContent, responseRecorder.Body.String()) } } @@ -302,7 +305,7 @@ func TestServeHTTPFailingFS(t *testing.T) { for i, test := range tests { // initialize a file server with the failing FileSystem - fileserver := FileServer(failingFS{err: test.fsErr}, nil) + fileserver := FileServer{Root: failingFS{err: test.fsErr}} // prepare the request and response request, err := http.NewRequest("GET", "https://foo/", nil) @@ -315,12 +318,12 @@ func TestServeHTTPFailingFS(t *testing.T) { // check the status if status != test.expectedStatus { - t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) + t.Errorf("Test %d: Expected status %d, found %d", i, test.expectedStatus, status) } // check the error if actualErr != test.expectedErr { - t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr) + t.Errorf("Test %d: Expected err %v, found %v", i, test.expectedErr, actualErr) } // check the headers - a special case for server under load @@ -328,7 +331,7 @@ func TestServeHTTPFailingFS(t *testing.T) { for expectedKey, expectedVal := range test.expectedHeaders { actualVal := responseRecorder.Header().Get(expectedKey) if expectedVal != actualVal { - t.Errorf(getTestPrefix(i)+"Expected header %s: %s, found %s", expectedKey, expectedVal, actualVal) + t.Errorf("Test %d: Expected header %s: %s, found %s", i, expectedKey, expectedVal, actualVal) } } } @@ -362,7 +365,7 @@ func TestServeHTTPFailingStat(t *testing.T) { for i, test := range tests { // initialize a file server. The FileSystem will not fail, but calls to the Stat method of the returned File object will - fileserver := FileServer(failingFS{err: nil, fileImpl: failingFile{err: test.statErr}}, nil) + fileserver := FileServer{Root: failingFS{err: nil, fileImpl: failingFile{err: test.statErr}}} // prepare the request and response request, err := http.NewRequest("GET", "https://foo/", nil) @@ -375,12 +378,12 @@ func TestServeHTTPFailingStat(t *testing.T) { // check the status if status != test.expectedStatus { - t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) + t.Errorf("Test %d: Expected status %d, found %d", i, test.expectedStatus, status) } // check the error if actualErr != test.expectedErr { - t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr) + t.Errorf("Test %d: Expected err %v, found %v", i, test.expectedErr, actualErr) } } } diff --git a/caddy/setup/templates.go b/caddyhttp/templates/setup.go similarity index 68% rename from caddy/setup/templates.go rename to caddyhttp/templates/setup.go index f8d7e98bd..e291b532d 100644 --- a/caddy/setup/templates.go +++ b/caddyhttp/templates/setup.go @@ -1,36 +1,48 @@ -package setup +package templates import ( "net/http" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/templates" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// Templates configures a new Templates middleware instance. -func Templates(c *Controller) (middleware.Middleware, error) { - rules, err := templatesParse(c) - if err != nil { - return nil, err - } - - tmpls := templates.Templates{ - Rules: rules, - Root: c.Root, - FileSys: http.Dir(c.Root), - } - - return func(next middleware.Handler) middleware.Handler { - tmpls.Next = next - return tmpls - }, nil +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "templates", + ServerType: "http", + Action: setup, + }) } -func templatesParse(c *Controller) ([]templates.Rule, error) { - var rules []templates.Rule +// setup configures a new Templates middleware instance. +func setup(c *caddy.Controller) error { + rules, err := templatesParse(c) + if err != nil { + return err + } + + cfg := httpserver.GetConfig(c.Key) + + tmpls := Templates{ + Rules: rules, + Root: cfg.Root, + FileSys: http.Dir(cfg.Root), + } + + cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + tmpls.Next = next + return tmpls + }) + + return nil +} + +func templatesParse(c *caddy.Controller) ([]Rule, error) { + var rules []Rule for c.Next() { - var rule templates.Rule + var rule Rule rule.Path = defaultTemplatePath rule.Extensions = defaultTemplateExtensions diff --git a/caddy/setup/templates_test.go b/caddyhttp/templates/setup_test.go similarity index 81% rename from caddy/setup/templates_test.go rename to caddyhttp/templates/setup_test.go index b1cfb29ce..2427afd45 100644 --- a/caddy/setup/templates_test.go +++ b/caddyhttp/templates/setup_test.go @@ -1,28 +1,25 @@ -package setup +package templates import ( "fmt" "testing" - "github.com/mholt/caddy/middleware/templates" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func TestTemplates(t *testing.T) { - - c := NewTestController(`templates`) - - mid, err := Templates(c) - +func TestSetup(t *testing.T) { + err := setup(caddy.NewTestController(`templates`)) 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.(templates.Templates) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Templates) if !ok { t.Fatalf("Expected handler to be type Templates, got: %#v", handler) @@ -50,21 +47,21 @@ func TestTemplatesParse(t *testing.T) { tests := []struct { inputTemplateConfig string shouldErr bool - expectedTemplateConfig []templates.Rule + expectedTemplateConfig []Rule }{ - {`templates /api1`, false, []templates.Rule{{ + {`templates /api1`, false, []Rule{{ Path: "/api1", Extensions: defaultTemplateExtensions, Delims: [2]string{}, }}}, - {`templates /api2 .txt .htm`, false, []templates.Rule{{ + {`templates /api2 .txt .htm`, false, []Rule{{ Path: "/api2", Extensions: []string{".txt", ".htm"}, Delims: [2]string{}, }}}, {`templates /api3 .htm .html - templates /api4 .txt .tpl `, false, []templates.Rule{{ + templates /api4 .txt .tpl `, false, []Rule{{ Path: "/api3", Extensions: []string{".htm", ".html"}, Delims: [2]string{}, @@ -77,14 +74,14 @@ func TestTemplatesParse(t *testing.T) { path /api5 ext .html between {% %} - }`, false, []templates.Rule{{ + }`, false, []Rule{{ Path: "/api5", Extensions: []string{".html"}, Delims: [2]string{"{%", "%}"}, }}}, } for i, test := range tests { - c := NewTestController(test.inputTemplateConfig) + c := caddy.NewTestController(test.inputTemplateConfig) actualTemplateConfigs, err := templatesParse(c) if err == nil && test.shouldErr { @@ -97,12 +94,10 @@ func TestTemplatesParse(t *testing.T) { i, len(test.expectedTemplateConfig), len(actualTemplateConfigs)) } for j, actualTemplateConfig := range actualTemplateConfigs { - if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path { t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s", i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path) } - if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) { t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions) } diff --git a/middleware/templates/templates.go b/caddyhttp/templates/templates.go similarity index 82% rename from middleware/templates/templates.go rename to caddyhttp/templates/templates.go index c8c08922b..91491f115 100644 --- a/middleware/templates/templates.go +++ b/caddyhttp/templates/templates.go @@ -1,4 +1,5 @@ -// Package templates implements template execution for files to be dynamically rendered for the client. +// Package templates implements template execution for files to be +// dynamically rendered for the client. package templates import ( @@ -9,19 +10,19 @@ import ( "path/filepath" "text/template" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// ServeHTTP implements the middleware.Handler interface. +// ServeHTTP implements the httpserver.Handler interface. func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range t.Rules { - if !middleware.Path(r.URL.Path).Matches(rule.Path) { + if !httpserver.Path(r.URL.Path).Matches(rule.Path) { continue } // Check for index files fpath := r.URL.Path - if idx, ok := middleware.IndexFile(t.FileSys, fpath, rule.IndexFiles); ok { + if idx, ok := httpserver.IndexFile(t.FileSys, fpath, rule.IndexFiles); ok { fpath = idx } @@ -31,7 +32,7 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error for _, ext := range rule.Extensions { if reqExt == ext { // Create execution context - ctx := middleware.Context{Root: t.FileSys, Req: r, URL: r.URL} + ctx := httpserver.Context{Root: t.FileSys, Req: r, URL: r.URL} // New template templateName := filepath.Base(fpath) @@ -64,7 +65,7 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error templateInfo, err := os.Stat(templatePath) if err == nil { // add the Last-Modified header if we were able to read the stamp - middleware.SetLastModifiedHeader(w, templateInfo.ModTime()) + httpserver.SetLastModifiedHeader(w, templateInfo.ModTime()) } buf.WriteTo(w) @@ -78,7 +79,7 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // Templates is middleware to render templated files as the HTTP response. type Templates struct { - Next middleware.Handler + Next httpserver.Handler Rules []Rule Root string FileSys http.FileSystem diff --git a/middleware/templates/templates_test.go b/caddyhttp/templates/templates_test.go similarity index 89% rename from middleware/templates/templates_test.go rename to caddyhttp/templates/templates_test.go index c5a5d24a8..841cf2027 100644 --- a/middleware/templates/templates_test.go +++ b/caddyhttp/templates/templates_test.go @@ -5,12 +5,12 @@ import ( "net/http/httptest" "testing" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -func Test(t *testing.T) { +func TestTemplates(t *testing.T) { tmpl := Templates{ - 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{ @@ -31,7 +31,7 @@ func Test(t *testing.T) { } tmplroot := Templates{ - 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{ @@ -45,9 +45,7 @@ func Test(t *testing.T) { FileSys: http.Dir("./testdata"), } - /* - * Test tmpl on /photos/test.html - */ + // Test tmpl on /photos/test.html req, err := http.NewRequest("GET", "/photos/test.html", nil) if err != nil { t.Fatalf("Test: Could not create HTTP request: %v", err) @@ -70,9 +68,7 @@ func Test(t *testing.T) { t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody) } - /* - * Test tmpl on /images/img.htm - */ + // Test tmpl on /images/img.htm req, err = http.NewRequest("GET", "/images/img.htm", nil) if err != nil { t.Fatalf("Could not create HTTP request: %v", err) @@ -95,9 +91,7 @@ func Test(t *testing.T) { t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody) } - /* - * Test tmpl on /images/img2.htm - */ + // Test tmpl on /images/img2.htm req, err = http.NewRequest("GET", "/images/img2.htm", nil) if err != nil { t.Fatalf("Could not create HTTP request: %v", err) @@ -119,9 +113,7 @@ func Test(t *testing.T) { t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody) } - /* - * Test tmplroot on /root.html - */ + // Test tmplroot on /root.html req, err = http.NewRequest("GET", "/root.html", nil) if err != nil { t.Fatalf("Could not create HTTP request: %v", err) diff --git a/caddy/setup/testdata/header.html b/caddyhttp/templates/testdata/header.html similarity index 100% rename from caddy/setup/testdata/header.html rename to caddyhttp/templates/testdata/header.html diff --git a/middleware/templates/testdata/header.html b/caddyhttp/templates/testdata/images/header.html similarity index 100% rename from middleware/templates/testdata/header.html rename to caddyhttp/templates/testdata/images/header.html diff --git a/middleware/templates/testdata/images/img.htm b/caddyhttp/templates/testdata/images/img.htm similarity index 100% rename from middleware/templates/testdata/images/img.htm rename to caddyhttp/templates/testdata/images/img.htm diff --git a/middleware/templates/testdata/images/img2.htm b/caddyhttp/templates/testdata/images/img2.htm similarity index 100% rename from middleware/templates/testdata/images/img2.htm rename to caddyhttp/templates/testdata/images/img2.htm diff --git a/middleware/templates/testdata/photos/test.html b/caddyhttp/templates/testdata/photos/test.html similarity index 100% rename from middleware/templates/testdata/photos/test.html rename to caddyhttp/templates/testdata/photos/test.html diff --git a/middleware/templates/testdata/root.html b/caddyhttp/templates/testdata/root.html similarity index 100% rename from middleware/templates/testdata/root.html rename to caddyhttp/templates/testdata/root.html diff --git a/caddy/setup/websocket.go b/caddyhttp/websocket/setup.go similarity index 61% rename from caddy/setup/websocket.go rename to caddyhttp/websocket/setup.go index 17617c406..fe930de1e 100644 --- a/caddy/setup/websocket.go +++ b/caddyhttp/websocket/setup.go @@ -1,27 +1,37 @@ -package setup +package websocket import ( - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/websocket" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) -// WebSocket configures a new WebSocket middleware instance. -func WebSocket(c *Controller) (middleware.Middleware, error) { - - websocks, err := webSocketParse(c) - if err != nil { - return nil, err - } - websocket.GatewayInterface = c.AppName + "-CGI/1.1" - websocket.ServerSoftware = c.AppName + "/" + c.AppVersion - - return func(next middleware.Handler) middleware.Handler { - return websocket.WebSocket{Next: next, Sockets: websocks} - }, nil +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "websocket", + ServerType: "http", + Action: setup, + }) } -func webSocketParse(c *Controller) ([]websocket.Config, error) { - var websocks []websocket.Config +// setup configures a new WebSocket middleware instance. +func setup(c *caddy.Controller) error { + websocks, err := webSocketParse(c) + if err != nil { + return err + } + + GatewayInterface = caddy.AppName + "-CGI/1.1" + ServerSoftware = caddy.AppName + "/" + caddy.AppVersion + + httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return WebSocket{Next: next, Sockets: websocks} + }) + + return nil +} + +func webSocketParse(c *caddy.Controller) ([]Config, error) { + var websocks []Config var respawn bool optionalBlock := func() (hadBlock bool, err error) { @@ -69,12 +79,12 @@ func webSocketParse(c *Controller) ([]websocket.Config, error) { } // Split command into the actual command and its arguments - cmd, args, err := middleware.SplitCommandAndArgs(command) + cmd, args, err := caddy.SplitCommandAndArgs(command) if err != nil { return nil, err } - websocks = append(websocks, websocket.Config{ + websocks = append(websocks, Config{ Path: path, Command: cmd, Arguments: args, diff --git a/caddy/setup/websocket_test.go b/caddyhttp/websocket/setup_test.go similarity index 76% rename from caddy/setup/websocket_test.go rename to caddyhttp/websocket/setup_test.go index ae3513602..f8b283304 100644 --- a/caddy/setup/websocket_test.go +++ b/caddyhttp/websocket/setup_test.go @@ -1,27 +1,24 @@ -package setup +package websocket import ( "testing" - "github.com/mholt/caddy/middleware/websocket" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" ) func TestWebSocket(t *testing.T) { - - c := NewTestController(`websocket cat`) - - mid, err := WebSocket(c) - + err := setup(caddy.NewTestController(`websocket cat`)) 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.(websocket.WebSocket) + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(WebSocket) if !ok { t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler) @@ -39,15 +36,15 @@ func TestWebSocketParse(t *testing.T) { tests := []struct { inputWebSocketConfig string shouldErr bool - expectedWebSocketConfig []websocket.Config + expectedWebSocketConfig []Config }{ - {`websocket /api1 cat`, false, []websocket.Config{{ + {`websocket /api1 cat`, false, []Config{{ Path: "/api1", Command: "cat", }}}, {`websocket /api3 cat - websocket /api4 cat `, false, []websocket.Config{{ + websocket /api4 cat `, false, []Config{{ Path: "/api3", Command: "cat", }, { @@ -55,7 +52,7 @@ func TestWebSocketParse(t *testing.T) { Command: "cat", }}}, - {`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{ + {`websocket /api5 "cmd arg1 arg2 arg3"`, false, []Config{{ Path: "/api5", Command: "cmd", Arguments: []string{"arg1", "arg2", "arg3"}, @@ -64,7 +61,7 @@ func TestWebSocketParse(t *testing.T) { // accept respawn {`websocket /api6 cat { respawn - }`, false, []websocket.Config{{ + }`, false, []Config{{ Path: "/api6", Command: "cat", }}}, @@ -72,10 +69,10 @@ func TestWebSocketParse(t *testing.T) { // invalid configuration {`websocket /api7 cat { invalid - }`, true, []websocket.Config{}}, + }`, true, []Config{}}, } for i, test := range tests { - c := NewTestController(test.inputWebSocketConfig) + c := caddy.NewTestController(test.inputWebSocketConfig) actualWebSocketConfigs, err := webSocketParse(c) if err == nil && test.shouldErr { diff --git a/middleware/websocket/websocket.go b/caddyhttp/websocket/websocket.go similarity index 98% rename from middleware/websocket/websocket.go rename to caddyhttp/websocket/websocket.go index 76781ba10..ca135f33f 100644 --- a/middleware/websocket/websocket.go +++ b/caddyhttp/websocket/websocket.go @@ -15,7 +15,7 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/caddyhttp/httpserver" ) const ( @@ -48,7 +48,7 @@ type ( // websocket endpoints. WebSocket struct { // Next is the next HTTP handler in the chain for when the path doesn't match - Next middleware.Handler + Next httpserver.Handler // Sockets holds all the web socket endpoint configurations Sockets []Config @@ -67,7 +67,7 @@ type ( // ServeHTTP converts the HTTP request to a WebSocket connection and serves it up. func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, sockconfig := range ws.Sockets { - if middleware.Path(r.URL.Path).Matches(sockconfig.Path) { + if httpserver.Path(r.URL.Path).Matches(sockconfig.Path) { return serveWS(w, r, &sockconfig) } } diff --git a/middleware/websocket/websocket_test.go b/caddyhttp/websocket/websocket_test.go similarity index 100% rename from middleware/websocket/websocket_test.go rename to caddyhttp/websocket/websocket_test.go diff --git a/caddy/https/certificates.go b/caddytls/certificates.go similarity index 87% rename from caddy/https/certificates.go rename to caddytls/certificates.go index 0dc3db523..b91180ba5 100644 --- a/caddy/https/certificates.go +++ b/caddytls/certificates.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "crypto/tls" @@ -33,21 +33,12 @@ type Certificate struct { // NotAfter is when the certificate expires. NotAfter time.Time - // Managed certificates are certificates that Caddy is managing, - // as opposed to the user specifying a certificate and key file - // or directory and managing the certificate resources themselves. - Managed bool - - // OnDemand certificates are obtained or loaded on-demand during TLS - // handshakes (as opposed to preloaded certificates, which are loaded - // at startup). If OnDemand is true, Managed must necessarily be true. - // OnDemand certificates are maintained in the background just like - // preloaded ones, however, if an OnDemand certificate fails to renew, - // it is removed from the in-memory cache. - OnDemand bool - // OCSP contains the certificate's parsed OCSP response. OCSP *ocsp.Response + + // Config is the configuration with which the certificate was + // loaded or obtained and with which it should be maintained. + Config *Config } // getCertificate gets a certificate that matches name (a server name) @@ -95,18 +86,21 @@ func getCertificate(name string) (cert Certificate, matched, defaulted bool) { return } -// cacheManagedCertificate loads the certificate for domain into the -// cache, flagging it as Managed and, if onDemand is true, as OnDemand +// CacheManagedCertificate loads the certificate for domain into the +// cache, flagging it as Managed and, if onDemand is true, as "OnDemand" // (meaning that it was obtained or loaded during a TLS handshake). // // This function is safe for concurrent use. -func cacheManagedCertificate(domain string, onDemand bool) (Certificate, error) { +func CacheManagedCertificate(domain string, cfg *Config) (Certificate, error) { + storage, err := StorageFor(cfg.CAUrl) + if err != nil { + return Certificate{}, err + } cert, err := makeCertificateFromDisk(storage.SiteCertFile(domain), storage.SiteKeyFile(domain)) if err != nil { return cert, err } - cert.Managed = true - cert.OnDemand = onDemand + cert.Config = cfg cacheCertificate(cert) return cert, nil } @@ -213,7 +207,7 @@ func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { func cacheCertificate(cert Certificate) { certCacheMu.Lock() if _, ok := certCache[""]; !ok { - // use as default + // use as default - must be *appended* to list, or bad things happen! cert.Names = append(cert.Names, "") certCache[""] = cert } @@ -232,3 +226,12 @@ func cacheCertificate(cert Certificate) { } certCacheMu.Unlock() } + +// uncacheCertificate deletes name's certificate from the +// cache. If name is not a key in the certificate cache, +// this function does nothing. +func uncacheCertificate(name string) { + certCacheMu.Lock() + delete(certCache, name) + certCacheMu.Unlock() +} diff --git a/caddy/https/certificates_test.go b/caddytls/certificates_test.go similarity index 99% rename from caddy/https/certificates_test.go rename to caddytls/certificates_test.go index dbfb4efc1..02f46cf1e 100644 --- a/caddy/https/certificates_test.go +++ b/caddytls/certificates_test.go @@ -1,4 +1,4 @@ -package https +package caddytls import "testing" diff --git a/caddy/https/client.go b/caddytls/client.go similarity index 56% rename from caddy/https/client.go rename to caddytls/client.go index 762e58aa1..8324b8382 100644 --- a/caddy/https/client.go +++ b/caddytls/client.go @@ -1,15 +1,19 @@ -package https +package caddytls import ( "encoding/json" "errors" "fmt" "io/ioutil" + "log" "net" + "net/url" + "os" + "strings" "sync" "time" - "github.com/mholt/caddy/server" + "github.com/mholt/caddy" "github.com/xenolf/lego/acme" ) @@ -19,22 +23,47 @@ var acmeMu sync.Mutex // ACMEClient is an acme.Client with custom state attached. type ACMEClient struct { *acme.Client - AllowPrompts bool // if false, we assume AlternatePort must be used + AllowPrompts bool + config *Config } -// NewACMEClient creates a new ACMEClient given an email and whether -// prompting the user is allowed. Clients should not be kept and -// re-used over long periods of time, but immediate re-use is more -// efficient than re-creating on every iteration. -var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) { - // Look up or create the LE user account - leUser, err := getUser(email) +// newACMEClient creates a new ACMEClient given an email and whether +// prompting the user is allowed. It's a variable so we can mock in tests. +var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) { + storage, err := StorageFor(config.CAUrl) if err != nil { return nil, err } + // Look up or create the LE user account + leUser, err := getUser(storage, config.ACMEEmail) + if err != nil { + return nil, err + } + + // ensure key type is set + keyType := DefaultKeyType + if config.KeyType != "" { + keyType = config.KeyType + } + + // ensure CA URL (directory endpoint) is set + caURL := DefaultCAUrl + if config.CAUrl != "" { + caURL = config.CAUrl + } + + // ensure endpoint is secure (assume HTTPS if scheme is missing) + if !strings.Contains(caURL, "://") { + caURL = "https://" + caURL + } + u, err := url.Parse(caURL) + if u.Scheme != "https" && !caddy.IsLoopback(u.Host) && !strings.HasPrefix(u.Host, "10.") { + return nil, fmt.Errorf("%s: insecure CA URL (HTTPS required)", caURL) + } + // The client facilitates our communication with the CA server. - client, err := acme.NewClient(CAUrl, &leUser, KeyType) + client, err := acme.NewClient(caURL, &leUser, keyType) if err != nil { return nil, err } @@ -59,52 +88,57 @@ var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) { err = client.AgreeToTOS() if err != nil { - saveUser(leUser) // Might as well try, right? + saveUser(storage, leUser) // Might as well try, right? return nil, errors.New("error agreeing to terms: " + err.Error()) } // save user to the file system - err = saveUser(leUser) + err = saveUser(storage, leUser) if err != nil { return nil, errors.New("could not save user: " + err.Error()) } } - return &ACMEClient{ - Client: client, - AllowPrompts: allowPrompts, - }, nil -} + c := &ACMEClient{Client: client, AllowPrompts: allowPrompts, config: config} -// NewACMEClientGetEmail creates a new ACMEClient and gets an email -// address at the same time (a server config is required, since it -// may contain an email address in it). -func NewACMEClientGetEmail(config server.Config, allowPrompts bool) (*ACMEClient, error) { - return NewACMEClient(getEmail(config, allowPrompts), allowPrompts) -} + if config.DNSProvider == "" { + // Use HTTP and TLS-SNI challenges by default -// Configure configures c according to bindHost, which is the host (not -// whole address) to bind the listener to in solving the http and tls-sni -// challenges. -func (c *ACMEClient) Configure(bindHost string) { - // If we allow prompts, operator must be present. In our case, - // that is synonymous with saying the server is not already - // started. So if the user is still there, we don't use - // AlternatePort because we don't need to proxy the challenges. - // Conversely, if the operator is not there, the server has - // already started and we need to proxy the challenge. - if c.AllowPrompts { - // Operator is present; server is not already listening - c.SetHTTPAddress(net.JoinHostPort(bindHost, "")) - c.SetTLSAddress(net.JoinHostPort(bindHost, "")) - //c.ExcludeChallenges([]acme.Challenge{acme.DNS01}) + // See if HTTP challenge needs to be proxied + if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, HTTPChallengePort)) { + altPort := config.AltHTTPPort + if altPort == "" { + altPort = DefaultHTTPAlternatePort + } + c.SetHTTPAddress(net.JoinHostPort(config.ListenHost, altPort)) + } + + // See if TLS challenge needs to be handled by our own facilities + if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, TLSSNIChallengePort)) { + c.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{}) + } } else { - // Operator is not present; server is started, so proxy challenges - c.SetHTTPAddress(net.JoinHostPort(bindHost, AlternatePort)) - c.SetTLSAddress(net.JoinHostPort(bindHost, AlternatePort)) - //c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) + // Otherwise, DNS challenge it is + + // Load provider constructor function + provFn, ok := dnsProviders[config.DNSProvider] + if !ok { + return nil, errors.New("unknown DNS provider by name '" + config.DNSProvider + "'") + } + + // we could pass credentials to create the provider, but for now + // we just let the solver package get them from the environment + prov, err := provFn() + if err != nil { + return nil, err + } + + // Use the DNS challenge exclusively + c.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) + c.SetChallengeProvider(acme.DNS01, prov) } - c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) // TODO: can we proxy TLS challenges? and we should support DNS... + + return c, nil } // Obtain obtains a single certificate for names. It stores the certificate @@ -121,7 +155,9 @@ Attempts: var promptedForAgreement bool // only prompt user for agreement at most once for errDomain, obtainErr := range failures { - // TODO: Double-check, will obtainErr ever be nil? + if obtainErr == nil { + continue + } if tosErr, ok := obtainErr.(acme.TOSError); ok { // Terms of Service agreement error; we can probably deal with this if !Agreed && !promptedForAgreement && c.AllowPrompts { @@ -144,7 +180,11 @@ Attempts: } // Success - immediately save the certificate resource - err := saveCertResource(certificate) + storage, err := StorageFor(c.config.CAUrl) + if err != nil { + return err + } + err = saveCertResource(storage, certificate) if err != nil { return fmt.Errorf("error saving assets for %v: %v", names, err) } @@ -163,6 +203,12 @@ Attempts: // // Anyway, this function is safe for concurrent use. func (c *ACMEClient) Renew(name string) error { + // Get access to ACME storage + storage, err := StorageFor(c.config.CAUrl) + if err != nil { + return err + } + // Prepare for renewal (load PEM cert, key, and meta) certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name)) if err != nil { @@ -204,12 +250,45 @@ func (c *ACMEClient) Renew(name string) error { } // For any other kind of error, wait 10s and try again. - time.Sleep(10 * time.Second) + wait := 10 * time.Second + log.Printf("[ERROR] Renewing: %v; trying again in %s", err, wait) + time.Sleep(wait) } if !success { return errors.New("too many renewal attempts; last error: " + err.Error()) } - return saveCertResource(newCertMeta) + return saveCertResource(storage, newCertMeta) +} + +// Revoke revokes the certificate for name and deltes +// it from storage. +func (c *ACMEClient) Revoke(name string) error { + storage, err := StorageFor(c.config.CAUrl) + if err != nil { + return err + } + + if !existingCertAndKey(storage, name) { + return errors.New("no certificate and key for " + name) + } + + certFile := storage.SiteCertFile(name) + certBytes, err := ioutil.ReadFile(certFile) + if err != nil { + return err + } + + err = c.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 } diff --git a/caddytls/client_test.go b/caddytls/client_test.go new file mode 100644 index 000000000..bd9cbbc81 --- /dev/null +++ b/caddytls/client_test.go @@ -0,0 +1,3 @@ +package caddytls + +// TODO diff --git a/caddytls/config.go b/caddytls/config.go new file mode 100644 index 000000000..550da2101 --- /dev/null +++ b/caddytls/config.go @@ -0,0 +1,437 @@ +package caddytls + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "time" + + "github.com/xenolf/lego/acme" +) + +// Config describes how TLS should be configured and used. +type Config struct { + // The hostname or class of hostnames this config is + // designated for; can contain wildcard characters + // according to RFC 6125 §6.4.3 - this field MUST + // NOT be empty in order for things to work smoothly + Hostname string + + // Whether TLS is enabled + Enabled bool + + // Minimum and maximum protocol versions to allow + ProtocolMinVersion uint16 + ProtocolMaxVersion uint16 + + // The list of cipher suites; first should be + // TLS_FALLBACK_SCSV to prevent degrade attacks + Ciphers []uint16 + + // Whether to prefer server cipher suites + PreferServerCipherSuites bool + + // Client authentication policy + ClientAuth tls.ClientAuthType + + // List of client CA certificates to allow, if + // client authentication is enabled + ClientCerts []string + + // Manual means user provides own certs and keys + Manual bool + + // Managed means config qualifies for implicit, + // automatic, managed TLS; as opposed to the user + // providing and managing the certificate manually + Managed bool + + // OnDemand means the class of hostnames this + // config applies to may obtain and manage + // certificates at handshake-time (as opposed + // to pre-loaded at startup); OnDemand certs + // will be managed the same way as preloaded + // ones, however, if an OnDemand cert fails to + // renew, it is removed from the in-memory + // cache; if this is true, Managed must + // necessarily be true + OnDemand bool + + // SelfSigned means that this hostname is + // served with a self-signed certificate + // that we generated in memory for convenience + SelfSigned bool + + // The endpoint of the directory for the ACME + // CA we are to use + CAUrl string + + // The host (ONLY the host, not port) to listen + //on if necessary to start a a listener to solve + // an ACME challenge + ListenHost string + + // The alternate port (ONLY port, not host) + // to use for the ACME HTTP challenge; this + // port will be used if we proxy challenges + // coming in on port 80 to this alternate port + AltHTTPPort string + + // The string identifier of the DNS provider + // to use when solving the ACME DNS challenge + DNSProvider string + + // The email address to use when creating or + // using an ACME account (fun fact: if this + // is set to "off" then this config will not + // qualify for managed TLS) + ACMEEmail string + + // The type of key to use when generating + // certificates + KeyType acme.KeyType +} + +// ObtainCert obtains a certificate for c.Hostname, as long as a certificate +// does not already exist in storage on disk. It only obtains and stores +// certificates (and their keys) to disk, it does not load them into memory. +// If allowPrompts is true, the user may be shown a prompt. If proxyACME is +// true, the relevant ACME challenges will be proxied to the alternate port. +func (c *Config) ObtainCert(allowPrompts bool) error { + return c.obtainCertName(c.Hostname, allowPrompts) +} + +func (c *Config) obtainCertName(name string, allowPrompts bool) error { + storage, err := StorageFor(c.CAUrl) + if err != nil { + return err + } + + if !c.Managed || !HostQualifies(name) || existingCertAndKey(storage, name) { + return nil + } + + if c.ACMEEmail == "" { + c.ACMEEmail = getEmail(storage, allowPrompts) + } + + client, err := newACMEClient(c, allowPrompts) + if err != nil { + return err + } + + return client.Obtain([]string{name}) +} + +// RenewCert renews the certificate for c.Hostname. +func (c *Config) RenewCert(allowPrompts bool) error { + return c.renewCertName(c.Hostname, allowPrompts) +} + +func (c *Config) renewCertName(name string, allowPrompts bool) error { + storage, err := StorageFor(c.CAUrl) + if err != nil { + return err + } + + // Prepare for renewal (load PEM cert, key, and meta) + certBytes, err := ioutil.ReadFile(storage.SiteCertFile(c.Hostname)) + if err != nil { + return err + } + keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(c.Hostname)) + if err != nil { + return err + } + metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(c.Hostname)) + if err != nil { + return err + } + var certMeta acme.CertificateResource + err = json.Unmarshal(metaBytes, &certMeta) + certMeta.Certificate = certBytes + certMeta.PrivateKey = keyBytes + + client, err := newACMEClient(c, allowPrompts) + if err != nil { + return err + } + + // Perform renewal and retry if necessary, but not too many times. + var newCertMeta acme.CertificateResource + var success bool + for attempts := 0; attempts < 2; attempts++ { + acmeMu.Lock() + newCertMeta, err = client.RenewCertificate(certMeta, true) + acmeMu.Unlock() + if err == nil { + success = true + break + } + + // If the legal terms were updated and need to be + // agreed to again, we can handle that. + if _, ok := err.(acme.TOSError); ok { + err := client.AgreeToTOS() + if err != nil { + return err + } + continue + } + + // For any other kind of error, wait 10s and try again. + time.Sleep(10 * time.Second) + } + + if !success { + return errors.New("too many renewal attempts; last error: " + err.Error()) + } + + return saveCertResource(storage, newCertMeta) +} + +// MakeTLSConfig reduces configs into a single tls.Config. +// If TLS is to be disabled, a nil tls.Config will be returned. +func MakeTLSConfig(configs []*Config) (*tls.Config, error) { + if configs == nil || len(configs) == 0 { + return nil, nil + } + + config := new(tls.Config) + ciphersAdded := make(map[uint16]struct{}) + configMap := make(configGroup) + + for i, cfg := range configs { + if cfg == nil { + // avoid nil pointer dereference below + configs[i] = new(Config) + continue + } + + // Key this config by its hostname; this + // overwrites configs with the same hostname + configMap[cfg.Hostname] = cfg + + // Can't serve TLS and not-TLS on same port + if i > 0 && cfg.Enabled != configs[i-1].Enabled { + thisConfProto, lastConfProto := "not TLS", "not TLS" + if cfg.Enabled { + thisConfProto = "TLS" + } + if configs[i-1].Enabled { + lastConfProto = "TLS" + } + return nil, fmt.Errorf("cannot multiplex %s (%s) and %s (%s) on same listener", + configs[i-1].Hostname, lastConfProto, cfg.Hostname, thisConfProto) + } + + // Union cipher suites + for _, ciph := range cfg.Ciphers { + if _, ok := ciphersAdded[ciph]; !ok { + ciphersAdded[ciph] = struct{}{} + config.CipherSuites = append(config.CipherSuites, ciph) + } + } + + // Can't resolve conflicting PreferServerCipherSuites settings + if i > 0 && cfg.PreferServerCipherSuites != configs[i-1].PreferServerCipherSuites { + return nil, fmt.Errorf("cannot both use PreferServerCipherSuites and not use it") + } + + // Go with the widest range of protocol versions + if cfg.ProtocolMinVersion < config.MinVersion { + config.MinVersion = cfg.ProtocolMinVersion + } + if cfg.ProtocolMaxVersion < config.MaxVersion { + config.MaxVersion = cfg.ProtocolMaxVersion + } + + // Go with the strictest ClientAuth type + if cfg.ClientAuth > config.ClientAuth { + config.ClientAuth = cfg.ClientAuth + } + } + + // Is TLS disabled? If so, we're done here. + // By now, we know that all configs agree + // whether it is or not, so we can just look + // at the first one. + if len(configs) == 0 || !configs[0].Enabled { + return nil, nil + } + + // Default cipher suites + if len(config.CipherSuites) == 0 { + config.CipherSuites = defaultCiphers + } + + // For security, ensure TLS_FALLBACK_SCSV is always included + if config.CipherSuites[0] != tls.TLS_FALLBACK_SCSV { + config.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.CipherSuites...) + } + + // Set up client authentication if enabled + if config.ClientAuth != tls.NoClientCert { + pool := x509.NewCertPool() + clientCertsAdded := make(map[string]struct{}) + for _, cfg := range configs { + for _, caFile := range cfg.ClientCerts { + // don't add cert to pool more than once + if _, ok := clientCertsAdded[caFile]; ok { + continue + } + clientCertsAdded[caFile] = struct{}{} + + // Any client with a certificate from this CA will be allowed to connect + caCrt, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + + if !pool.AppendCertsFromPEM(caCrt) { + return nil, fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile) + } + } + } + config.ClientCAs = pool + } + + // Associate the GetCertificate callback, or almost nothing we just did will work + config.GetCertificate = configMap.GetCertificate + + return config, nil +} + +// ConfigGetter gets a Config keyed by key. +type ConfigGetter func(key string) *Config + +var configGetters = make(map[string]ConfigGetter) + +// RegisterConfigGetter registers fn as the way to get a +// Config for server type serverType. +func RegisterConfigGetter(serverType string, fn ConfigGetter) { + configGetters[serverType] = fn +} + +// 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). +func SetDefaultTLSParams(config *Config) { + // If no ciphers provided, use default list + if len(config.Ciphers) == 0 { + config.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) + config.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.Ciphers...) + + // Set default protocol min and max versions - must balance compatibility and security + if config.ProtocolMinVersion == 0 { + config.ProtocolMinVersion = tls.VersionTLS11 + } + if config.ProtocolMaxVersion == 0 { + config.ProtocolMaxVersion = tls.VersionTLS12 + } + + // Prefer server cipher suites + config.PreferServerCipherSuites = true +} + +// 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. +// HTTP/2 only supports TLS 1.2 and higher. +var supportedProtocols = map[string]uint16{ + "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 four of the suites below (the four 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, +} + +const ( + // HTTPChallengePort is the officially designated port for + // the HTTP challenge. + HTTPChallengePort = "80" + + // TLSSNIChallengePort is the officially designated port for + // the TLS-SNI challenge. + TLSSNIChallengePort = "443" + + // DefaultHTTPAlternatePort is the port on which the ACME + // client will open a listener and solve the HTTP challenge. + // If this alternate port is used instead of the default + // port, then whatever is listening on the default port must + // be capable of proxying or forwarding the request to this + // alternate port. + DefaultHTTPAlternatePort = "5033" +) diff --git a/caddytls/crypto.go b/caddytls/crypto.go new file mode 100644 index 000000000..243b37f5d --- /dev/null +++ b/caddytls/crypto.go @@ -0,0 +1,258 @@ +package caddytls + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "math/big" + "net" + "os" + "time" + + "github.com/xenolf/lego/acme" +) + +// 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) +} + +// stapleOCSP staples OCSP information to cert for hostname name. +// If you have it handy, you should pass in the PEM-encoded certificate +// bundle; otherwise the DER-encoded cert will have to be PEM-encoded. +// If you don't have the PEM blocks handy, just pass in nil. +// +// Errors here are not necessarily fatal, it could just be that the +// certificate doesn't have an issuer URL. +func stapleOCSP(cert *Certificate, pemBundle []byte) error { + if pemBundle == nil { + // The function in the acme package that gets OCSP requires a PEM-encoded cert + bundle := new(bytes.Buffer) + for _, derBytes := range cert.Certificate.Certificate { + pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + } + pemBundle = bundle.Bytes() + } + + ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle) + if err != nil { + return err + } + + cert.Certificate.OCSPStaple = ocspBytes + cert.OCSP = ocspResp + + return nil +} + +// makeSelfSignedCert makes a self-signed certificate according +// to the parameters in config. It then caches the certificate +// in our cache. +func makeSelfSignedCert(config *Config) error { + // start by generating private key + var privKey interface{} + var err error + switch config.KeyType { + case "", acme.EC256: + privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case acme.EC384: + privKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case acme.RSA2048: + privKey, err = rsa.GenerateKey(rand.Reader, 2048) + case acme.RSA4096: + privKey, err = rsa.GenerateKey(rand.Reader, 4096) + case acme.RSA8192: + privKey, err = rsa.GenerateKey(rand.Reader, 8192) + default: + return fmt.Errorf("cannot generate private key; unknown key type %v", config.KeyType) + } + if err != nil { + return fmt.Errorf("failed to generate private key: %v", err) + } + + // create certificate structure with proper values + notBefore := time.Now() + notAfter := notBefore.Add(24 * time.Hour * 7) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return fmt.Errorf("failed to generate serial number: %v", err) + } + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{Organization: []string{"Caddy Self-Signed"}}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + if ip := net.ParseIP(config.Hostname); ip != nil { + cert.IPAddresses = append(cert.IPAddresses, ip) + } else { + cert.DNSNames = append(cert.DNSNames, config.Hostname) + } + + publicKey := func(privKey interface{}) interface{} { + switch k := privKey.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return errors.New("unknown key type") + } + } + derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey(privKey), privKey) + if err != nil { + return fmt.Errorf("could not create certificate: %v", err) + } + + cacheCertificate(Certificate{ + Certificate: tls.Certificate{ + Certificate: [][]byte{derBytes}, + PrivateKey: privKey, + Leaf: cert, + }, + Names: cert.DNSNames, + NotAfter: cert.NotAfter, + Config: config, + }) + + return nil +} + +// RotateSessionTicketKeys rotates the TLS session ticket keys +// on cfg every TicketRotateInterval. It spawns a new goroutine so +// this function does NOT block. It returns a channel you should +// close when you are ready to stop the key rotation, like when the +// server using cfg is no longer running. +func RotateSessionTicketKeys(cfg *tls.Config) chan struct{} { + ch := make(chan struct{}) + ticker := time.NewTicker(TicketRotateInterval) + go runTLSTicketKeyRotation(cfg, ticker, ch) + return ch +} + +// Functions that may be swapped out for testing +var ( + runTLSTicketKeyRotation = standaloneTLSTicketKeyRotation + setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { return keys } +) + +// standaloneTLSTicketKeyRotation governs over the array of TLS ticket keys used to de/crypt TLS tickets. +// It periodically sets a new ticket key as the first one, used to encrypt (and decrypt), +// pushing any old ticket keys to the back, where they are considered for decryption only. +// +// Lack of entropy for the very first ticket key results in the feature being disabled (as does Go), +// later lack of entropy temporarily disables ticket key rotation. +// Old ticket keys are still phased out, though. +// +// Stops the ticker when returning. +func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan chan struct{}) { + defer ticker.Stop() + + // The entire page should be marked as sticky, but Go cannot do that + // without resorting to syscall#Mlock. And, we don't have madvise (for NODUMP), too. ☹ + keys := make([][32]byte, 1, NumTickets) + + rng := c.Rand + if rng == nil { + rng = rand.Reader + } + if _, err := io.ReadFull(rng, keys[0][:]); err != nil { + c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one + return + } + c.SessionTicketKey = keys[0] // SetSessionTicketKeys doesn't set a 'tls.keysAlreadySet' + c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys)) + + for { + select { + case _, isOpen := <-exitChan: + if !isOpen { + return + } + case <-ticker.C: + rng = c.Rand // could've changed since the start + if rng == nil { + rng = rand.Reader + } + var newTicketKey [32]byte + _, err := io.ReadFull(rng, newTicketKey[:]) + + if len(keys) < NumTickets { + keys = append(keys, keys[0]) // manipulates the internal length + } + for idx := len(keys) - 1; idx >= 1; idx-- { + keys[idx] = keys[idx-1] // yes, this makes copies + } + + if err == nil { + keys[0] = newTicketKey + } + // pushes the last key out, doesn't matter that we don't have a new one + c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys)) + } + } +} + +const ( + // NumTickets is how many tickets to hold and consider + // to decrypt TLS sessions. + NumTickets = 4 + + // TicketRotateInterval is how often to generate + // new ticket for TLS PFS encryption + TicketRotateInterval = 10 * time.Hour +) diff --git a/caddy/https/crypto_test.go b/caddytls/crypto_test.go similarity index 60% rename from caddy/https/crypto_test.go rename to caddytls/crypto_test.go index efa45c65a..3eca43ae2 100644 --- a/caddy/https/crypto_test.go +++ b/caddytls/crypto_test.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "bytes" @@ -7,11 +7,12 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/tls" "crypto/x509" - "errors" "os" "runtime" "testing" + "time" ) func TestSaveAndLoadRSAPrivateKey(t *testing.T) { @@ -96,25 +97,70 @@ func TestSaveAndLoadECCPrivateKey(t *testing.T) { // PrivateKeysSame compares the bytes of a and b and returns true if they are the same. func PrivateKeysSame(a, b crypto.PrivateKey) bool { - var abytes, bbytes []byte - var err error - - if abytes, err = PrivateKeyBytes(a); err != nil { - return false - } - if bbytes, err = PrivateKeyBytes(b); err != nil { - return false - } - return bytes.Equal(abytes, bbytes) + return bytes.Equal(PrivateKeyBytes(a), PrivateKeyBytes(b)) } // PrivateKeyBytes returns the bytes of DER-encoded key. -func PrivateKeyBytes(key crypto.PrivateKey) ([]byte, error) { +func PrivateKeyBytes(key crypto.PrivateKey) []byte { + var keyBytes []byte switch key := key.(type) { case *rsa.PrivateKey: - return x509.MarshalPKCS1PrivateKey(key), nil + keyBytes = x509.MarshalPKCS1PrivateKey(key) case *ecdsa.PrivateKey: - return x509.MarshalECPrivateKey(key) + keyBytes, _ = x509.MarshalECPrivateKey(key) + } + return keyBytes +} + +func TestStandaloneTLSTicketKeyRotation(t *testing.T) { + tlsGovChan := make(chan struct{}) + defer close(tlsGovChan) + callSync := make(chan bool, 1) + defer close(callSync) + + oldHook := setSessionTicketKeysTestHook + defer func() { + setSessionTicketKeysTestHook = oldHook + }() + var keysInUse [][32]byte + setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { + keysInUse = keys + callSync <- true + return keys + } + + c := new(tls.Config) + timer := time.NewTicker(time.Millisecond * 1) + + go standaloneTLSTicketKeyRotation(c, timer, tlsGovChan) + + rounds := 0 + var lastTicketKey [32]byte + for { + select { + case <-callSync: + if lastTicketKey == keysInUse[0] { + close(tlsGovChan) + t.Errorf("The same TLS ticket key has been used again (not rotated): %x.", lastTicketKey) + return + } + lastTicketKey = keysInUse[0] + rounds++ + if rounds <= NumTickets && len(keysInUse) != rounds { + close(tlsGovChan) + t.Errorf("Expected TLS ticket keys in use: %d; Got instead: %d.", rounds, len(keysInUse)) + return + } + if c.SessionTicketsDisabled == true { + t.Error("Session tickets have been disabled unexpectedly.") + return + } + if rounds >= NumTickets+1 { + return + } + case <-time.After(time.Second * 1): + t.Errorf("Timeout after %d rounds.", rounds) + return + } } - return nil, errors.New("Unknown private key type") } diff --git a/caddy/https/handshake.go b/caddytls/handshake.go similarity index 64% rename from caddy/https/handshake.go rename to caddytls/handshake.go index fc6ef809e..f389dd7ae 100644 --- a/caddy/https/handshake.go +++ b/caddytls/handshake.go @@ -1,9 +1,7 @@ -package https +package caddytls import ( - "bytes" "crypto/tls" - "encoding/pem" "errors" "fmt" "log" @@ -11,67 +9,98 @@ import ( "sync" "sync/atomic" "time" - - "github.com/mholt/caddy/server" - "github.com/xenolf/lego/acme" ) -// GetCertificate gets a certificate to satisfy clientHello as long as -// the certificate is already cached in memory. It will not be loaded -// from disk or obtained from the CA during the handshake. +// configGroup is a type that keys configs by their hostname +// (hostnames can have wildcard characters; use the getConfig +// method to get a config by matching its hostname). Its +// GetCertificate function can be used with tls.Config. +type configGroup map[string]*Config + +// getConfig gets the config by the first key match for name. +// In other words, "sub.foo.bar" will get the config for "*.foo.bar" +// if that is the closest match. This function MAY return nil +// if no match is found. // -// This function is safe for use as a tls.Config.GetCertificate callback. -func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - cert, err := getCertDuringHandshake(clientHello.ServerName, false, false) - return &cert.Certificate, err +// This function follows nearly the same logic to lookup +// a hostname as the getCertificate function uses. +func (cg configGroup) getConfig(name string) *Config { + name = strings.ToLower(name) + + // exact match? great, let's use it + if config, ok := cg[name]; ok { + return config + } + + // try replacing labels in the name with wildcards until we get a match + labels := strings.Split(name, ".") + for i := range labels { + labels[i] = "*" + candidate := strings.Join(labels, ".") + if config, ok := cg[candidate]; ok { + return config + } + } + + // as last resort, try a config that serves all names + if config, ok := cg[""]; ok { + return config + } + + return nil } -// GetOrObtainCertificate will get a certificate to satisfy clientHello, even -// if that means obtaining a new certificate from a CA during the handshake. -// It first checks the in-memory cache, then accesses disk, then accesses the -// network if it must. An obtained certificate will be stored on disk and -// cached in memory. +// GetCertificate gets a certificate to satisfy clientHello. In getting +// the certificate, it abides the rules and settings defined in the +// Config that matches clientHello.ServerName. It first checks the in- +// memory cache, then, if the config enables "OnDemand", it accessses +// disk, then accesses the network if it must obtain a new certificate +// via ACME. // -// This function is safe for use as a tls.Config.GetCertificate callback. -func GetOrObtainCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - cert, err := getCertDuringHandshake(clientHello.ServerName, true, true) +// This method is safe for use as a tls.Config.GetCertificate callback. +func (cg configGroup) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := cg.getCertDuringHandshake(clientHello.ServerName, true, true) return &cert.Certificate, err } // getCertDuringHandshake will get a certificate for name. It first tries -// the in-memory cache. If no certificate for name is in the cache and if -// loadIfNecessary == true, it goes to disk to load it into the cache and -// serve it. If it's not on disk and if obtainIfNecessary == true, the -// certificate will be obtained from the CA, cached, and served. If -// obtainIfNecessary is true, then loadIfNecessary must also be set to true. -// An error will be returned if and only if no certificate is available. +// the in-memory cache. If no certificate for name is in the cache, the +// config most closely corresponding to name will be loaded. If that config +// allows it (OnDemand==true) and if loadIfNecessary == true, it goes to disk +// to load it into the cache and serve it. If it's not on disk and if +// obtainIfNecessary == true, the certificate will be obtained from the CA, +// cached, and served. If obtainIfNecessary is true, then loadIfNecessary +// must also be set to true. An error will be returned if and only if no +// certificate is available. // // This function is safe for concurrent use. -func getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { +func (cg configGroup) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { // First check our in-memory cache to see if we've already loaded it cert, matched, defaulted := getCertificate(name) if matched { return cert, nil } - if loadIfNecessary { + // Get the relevant TLS config for this name. If OnDemand is enabled, + // then we might be able to load or obtain a needed certificate. + cfg := cg.getConfig(name) + if cfg != nil && cfg.OnDemand && loadIfNecessary { // Then check to see if we have one on disk - loadedCert, err := cacheManagedCertificate(name, true) + loadedCert, err := CacheManagedCertificate(name, cfg) if err == nil { - loadedCert, err = handshakeMaintenance(name, loadedCert) + loadedCert, err = cg.handshakeMaintenance(name, loadedCert) if err != nil { log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err) } return loadedCert, nil } - if obtainIfNecessary { // By this point, we need to ask the CA for a certificate name = strings.ToLower(name) // Make sure aren't over any applicable limits - err := checkLimitsForObtainingNewCerts(name) + err := cg.checkLimitsForObtainingNewCerts(name) if err != nil { return Certificate{}, err } @@ -82,22 +111,23 @@ func getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool } // Obtain certificate from the CA - return obtainOnDemandCertificate(name) + return cg.obtainOnDemandCertificate(name, cfg) } } + // Fall back to the default certificate if there is one if defaulted { return cert, nil } - return Certificate{}, errors.New("no certificate for " + name) + return Certificate{}, fmt.Errorf("no certificate available for %s", name) } // checkLimitsForObtainingNewCerts checks to see if name can be issued right // now according to mitigating factors we keep track of and preferences the // user has set. If a non-nil error is returned, do not issue a new certificate // for name. -func checkLimitsForObtainingNewCerts(name string) error { +func (cg configGroup) checkLimitsForObtainingNewCerts(name string) error { // User can set hard limit for number of certs for the process to issue if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue { return fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue) @@ -129,7 +159,7 @@ func checkLimitsForObtainingNewCerts(name string) error { // name, it will wait and use what the other goroutine obtained. // // This function is safe for use by multiple concurrent goroutines. -func obtainOnDemandCertificate(name string) (Certificate, error) { +func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certificate, error) { // We must protect this process from happening concurrently, so synchronize. obtainCertWaitChansMu.Lock() wait, ok := obtainCertWaitChans[name] @@ -138,7 +168,7 @@ func obtainOnDemandCertificate(name string) (Certificate, error) { // wait for it to finish obtaining the cert and then we'll use it. obtainCertWaitChansMu.Unlock() <-wait - return getCertDuringHandshake(name, true, false) + return cg.getCertDuringHandshake(name, true, false) } // looks like it's up to us to do all the work and obtain the cert @@ -156,14 +186,7 @@ func obtainOnDemandCertificate(name string) (Certificate, error) { log.Printf("[INFO] Obtaining new certificate for %s", name) - // obtain cert - client, err := NewACMEClientGetEmail(server.Config{}, false) - if err != nil { - return Certificate{}, errors.New("error creating client: " + err.Error()) - } - client.Configure("") // TODO: which BindHost? - err = client.Obtain([]string{name}) - if err != nil { + if err := cfg.obtainCertName(name, false); err != nil { // Failed to solve challenge, so don't allow another on-demand // issue for this name to be attempted for a little while. failedIssuanceMu.Lock() @@ -185,19 +208,19 @@ func obtainOnDemandCertificate(name string) (Certificate, error) { lastIssueTimeMu.Unlock() // The certificate is already on disk; now just start over to load it and serve it - return getCertDuringHandshake(name, true, false) + return cg.getCertDuringHandshake(name, true, false) } // handshakeMaintenance performs a check on cert for expiration and OCSP // validity. // // This function is safe for use by multiple concurrent goroutines. -func handshakeMaintenance(name string, cert Certificate) (Certificate, error) { +func (cg configGroup) handshakeMaintenance(name string, cert Certificate) (Certificate, error) { // Check cert expiration timeLeft := cert.NotAfter.Sub(time.Now().UTC()) - if timeLeft < renewDurationBefore { + if timeLeft < RenewDurationBefore { log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft) - return renewDynamicCertificate(name) + return cg.renewDynamicCertificate(name, cert.Config) } // Check OCSP staple validity @@ -219,13 +242,13 @@ func handshakeMaintenance(name string, cert Certificate) (Certificate, error) { return cert, nil } -// renewDynamicCertificate renews currentCert using the clientHello. It returns the +// renewDynamicCertificate renews the certificate for name using cfg. It returns the // certificate to use and an error, if any. currentCert may be returned even if an // error occurs, since we perform renewals before they expire and it may still be // usable. name should already be lower-cased before calling this function. // // This function is safe for use by multiple concurrent goroutines. -func renewDynamicCertificate(name string) (Certificate, error) { +func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certificate, error) { obtainCertWaitChansMu.Lock() wait, ok := obtainCertWaitChans[name] if ok { @@ -233,7 +256,7 @@ func renewDynamicCertificate(name string) (Certificate, error) { // wait for it to finish, then we'll use the new one. obtainCertWaitChansMu.Unlock() <-wait - return getCertDuringHandshake(name, true, false) + return cg.getCertDuringHandshake(name, true, false) } // looks like it's up to us to do all the work and renew the cert @@ -251,45 +274,12 @@ func renewDynamicCertificate(name string) (Certificate, error) { log.Printf("[INFO] Renewing certificate for %s", name) - client, err := NewACMEClientGetEmail(server.Config{}, false) - if err != nil { - return Certificate{}, err - } - client.Configure("") // TODO: Bind address of relevant listener, yuck - err = client.Renew(name) + err := cfg.renewCertName(name, false) if err != nil { return Certificate{}, err } - return getCertDuringHandshake(name, true, false) -} - -// stapleOCSP staples OCSP information to cert for hostname name. -// If you have it handy, you should pass in the PEM-encoded certificate -// bundle; otherwise the DER-encoded cert will have to be PEM-encoded. -// If you don't have the PEM blocks handy, just pass in nil. -// -// Errors here are not necessarily fatal, it could just be that the -// certificate doesn't have an issuer URL. -func stapleOCSP(cert *Certificate, pemBundle []byte) error { - if pemBundle == nil { - // The function in the acme package that gets OCSP requires a PEM-encoded cert - bundle := new(bytes.Buffer) - for _, derBytes := range cert.Certificate.Certificate { - pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - } - pemBundle = bundle.Bytes() - } - - ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle) - if err != nil { - return err - } - - cert.Certificate.OCSPStaple = ocspBytes - cert.OCSP = ocspResp - - return nil + return cg.getCertDuringHandshake(name, true, false) } // obtainCertWaitChans is used to coordinate obtaining certs for each hostname. @@ -318,3 +308,5 @@ var failedIssuanceMu sync.RWMutex // If this value is recent, do not make any on-demand certificate requests. var lastIssueTime time.Time var lastIssueTimeMu sync.Mutex + +var errNoCert = errors.New("no certificate available") diff --git a/caddy/https/handshake_test.go b/caddytls/handshake_test.go similarity index 83% rename from caddy/https/handshake_test.go rename to caddytls/handshake_test.go index cf70eb17d..6abfb767f 100644 --- a/caddy/https/handshake_test.go +++ b/caddytls/handshake_test.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "crypto/tls" @@ -9,16 +9,18 @@ import ( func TestGetCertificate(t *testing.T) { defer func() { certCache = make(map[string]Certificate) }() + cg := make(configGroup) + hello := &tls.ClientHelloInfo{ServerName: "example.com"} helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"} helloNoSNI := &tls.ClientHelloInfo{} helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"} // When cache is empty - if cert, err := GetCertificate(hello); err == nil { + if cert, err := cg.GetCertificate(hello); err == nil { t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert) } - if cert, err := GetCertificate(helloNoSNI); err == nil { + if cert, err := cg.GetCertificate(helloNoSNI); err == nil { t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert) } @@ -26,12 +28,12 @@ func TestGetCertificate(t *testing.T) { defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}} certCache[""] = defaultCert certCache["example.com"] = defaultCert - if cert, err := GetCertificate(hello); err != nil { + if cert, err := cg.GetCertificate(hello); err != nil { t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err) } else if cert.Leaf.DNSNames[0] != "example.com" { t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert) } - if cert, err := GetCertificate(helloNoSNI); err != nil { + if cert, err := cg.GetCertificate(helloNoSNI); err != nil { t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err) } else if cert.Leaf.DNSNames[0] != "example.com" { t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert) @@ -39,14 +41,14 @@ func TestGetCertificate(t *testing.T) { // When retrieving wildcard certificate certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}} - if cert, err := GetCertificate(helloSub); err != nil { + if cert, err := cg.GetCertificate(helloSub); err != nil { t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err) } else if cert.Leaf.DNSNames[0] != "*.example.com" { t.Errorf("Got wrong certificate, expected wildcard: %v", cert) } // When no certificate matches, the default is returned - if cert, err := GetCertificate(helloNoMatch); err != nil { + if cert, err := cg.GetCertificate(helloNoMatch); err != nil { t.Errorf("Expected default certificate with no error when no matches, got err: %v", err) } else if cert.Leaf.DNSNames[0] != "example.com" { t.Errorf("Expected default cert with no matches, got: %v", cert) diff --git a/caddytls/httphandler.go b/caddytls/httphandler.go new file mode 100644 index 000000000..8115f9450 --- /dev/null +++ b/caddytls/httphandler.go @@ -0,0 +1,42 @@ +package caddytls + +import ( + "crypto/tls" + "log" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +const challengeBasePath = "/.well-known/acme-challenge" + +// HTTPChallengeHandler 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 HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, altPort string) bool { + if !strings.HasPrefix(r.URL.Path, challengeBasePath) { + return false + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + upstream, err := url.Parse(scheme + "://localhost:" + altPort) + 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 +} diff --git a/caddy/https/handler_test.go b/caddytls/httphandler_test.go similarity index 76% rename from caddy/https/handler_test.go rename to caddytls/httphandler_test.go index 016799ffb..fc04e8eeb 100644 --- a/caddy/https/handler_test.go +++ b/caddytls/httphandler_test.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "net" @@ -7,7 +7,7 @@ import ( "testing" ) -func TestRequestCallbackNoOp(t *testing.T) { +func TestHTTPChallengeHandlerNoOp(t *testing.T) { // try base paths that aren't handled by this handler for _, url := range []string{ "http://localhost/", @@ -21,13 +21,13 @@ func TestRequestCallbackNoOp(t *testing.T) { t.Fatalf("Could not craft request, got error: %v", err) } rw := httptest.NewRecorder() - if RequestCallback(rw, req) { + if HTTPChallengeHandler(rw, req, DefaultHTTPAlternatePort) { t.Errorf("Got true with this URL, but shouldn't have: %s", url) } } } -func TestRequestCallbackSuccess(t *testing.T) { +func TestHTTPChallengeHandlerSuccess(t *testing.T) { expectedPath := challengeBasePath + "/asdf" // Set up fake acme handler backend to make sure proxying succeeds @@ -40,7 +40,7 @@ func TestRequestCallbackSuccess(t *testing.T) { })) // Custom listener that uses the port we expect - ln, err := net.Listen("tcp", "127.0.0.1:"+AlternatePort) + ln, err := net.Listen("tcp", "127.0.0.1:"+DefaultHTTPAlternatePort) if err != nil { t.Fatalf("Unable to start test server listener: %v", err) } @@ -49,13 +49,13 @@ func TestRequestCallbackSuccess(t *testing.T) { // Start our engines and run the test ts.Start() defer ts.Close() - req, err := http.NewRequest("GET", "http://127.0.0.1:"+AlternatePort+expectedPath, nil) + req, err := http.NewRequest("GET", "http://127.0.0.1:"+DefaultHTTPAlternatePort+expectedPath, nil) if err != nil { t.Fatalf("Could not craft request, got error: %v", err) } rw := httptest.NewRecorder() - RequestCallback(rw, req) + HTTPChallengeHandler(rw, req, DefaultHTTPAlternatePort) if !proxySuccess { t.Fatal("Expected request to be proxied, but it wasn't") diff --git a/caddy/https/maintain.go b/caddytls/maintain.go similarity index 74% rename from caddy/https/maintain.go rename to caddytls/maintain.go index a0fb0557b..96514ac24 100644 --- a/caddy/https/maintain.go +++ b/caddytls/maintain.go @@ -1,31 +1,39 @@ -package https +package caddytls import ( "log" "time" - "github.com/mholt/caddy/server" - "golang.org/x/crypto/ocsp" ) +func init() { + // maintain assets while this package is imported, which is + // always. we don't ever stop it, since we need it running. + go maintainAssets(make(chan struct{})) +} + const ( // RenewInterval is how often to check certificates for renewal. RenewInterval = 12 * time.Hour // OCSPInterval is how often to check if OCSP stapling needs updating. OCSPInterval = 1 * time.Hour + + // RenewDurationBefore is how long before expiration to renew certificates. + RenewDurationBefore = (24 * time.Hour) * 30 ) // maintainAssets is a permanently-blocking function // that loops indefinitely and, on a regular schedule, checks // certificates for expiration and initiates a renewal of certs // that are expiring soon. It also updates OCSP stapling and -// performs other maintenance of assets. +// performs other maintenance of assets. It should only be +// called once per process. // // You must pass in the channel which you'll close when // maintenance should stop, to allow this goroutine to clean up -// after itself and unblock. +// after itself and unblock. (Not that you HAVE to stop it...) func maintainAssets(stopChan chan struct{}) { renewalTicker := time.NewTicker(RenewInterval) ocspTicker := time.NewTicker(OCSPInterval) @@ -34,11 +42,11 @@ func maintainAssets(stopChan chan struct{}) { select { case <-renewalTicker.C: log.Println("[INFO] Scanning for expiring certificates") - renewManagedCertificates(false) + RenewManagedCertificates(false) log.Println("[INFO] Done checking certificates") case <-ocspTicker.C: log.Println("[INFO] Scanning for stale OCSP staples") - updateOCSPStaples() + UpdateOCSPStaples() log.Println("[INFO] Done checking OCSP staples") case <-stopChan: renewalTicker.Stop() @@ -49,20 +57,20 @@ func maintainAssets(stopChan chan struct{}) { } } -func renewManagedCertificates(allowPrompts bool) (err error) { +// RenewManagedCertificates renews managed certificates. +func RenewManagedCertificates(allowPrompts bool) (err error) { var renewed, deleted []Certificate - var client *ACMEClient visitedNames := make(map[string]struct{}) certCacheMu.RLock() for name, cert := range certCache { - if !cert.Managed { + if !cert.Config.Managed || cert.Config.SelfSigned { continue } // the list of names on this cert should never be empty... if cert.Names == nil || len(cert.Names) == 0 { - log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v", name, cert.Names) + log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v - removing from cache", name, cert.Names) deleted = append(deleted, cert) continue } @@ -75,21 +83,21 @@ func renewManagedCertificates(allowPrompts bool) (err error) { visitedNames[name] = struct{}{} } + // if its time is up or ending soon, we need to try to renew it timeLeft := cert.NotAfter.Sub(time.Now().UTC()) - if timeLeft < renewDurationBefore { + if timeLeft < RenewDurationBefore { log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft) - if client == nil { - client, err = NewACMEClientGetEmail(server.Config{}, allowPrompts) - if err != nil { - return err - } - client.Configure("") // TODO: Bind address of relevant listener, yuck + if cert.Config == nil { + log.Printf("[ERROR] %s: No associated TLS config; unable to renew", name) + continue } - err := client.Renew(cert.Names[0]) // managed certs better have only one name + // this works well because managed certs are only associated with one name per config + err := cert.Config.RenewCert(allowPrompts) + if err != nil { - if client.AllowPrompts && timeLeft < 0 { + if allowPrompts && timeLeft < 0 { // Certificate renewal failed, the operator is present, and the certificate // is already expired; we should stop immediately and return the error. Note // that we used to do this any time a renewal failed at startup. However, @@ -100,7 +108,7 @@ func renewManagedCertificates(allowPrompts bool) (err error) { return err } log.Printf("[ERROR] %v", err) - if cert.OnDemand { + if cert.Config.OnDemand { deleted = append(deleted, cert) } } else { @@ -113,20 +121,21 @@ func renewManagedCertificates(allowPrompts bool) (err error) { // Apply changes to the cache for _, cert := range renewed { if cert.Names[len(cert.Names)-1] == "" { - // Special case: This is the default certificate, so we must - // ensure it gets updated as well, otherwise the renewal - // routine will find it and think it still needs to be renewed, - // even though we already renewed it... + // Special case: This is the default certificate. We must + // flush it out of the cache so that we no longer point to + // the old, un-renewed certificate. Otherwise it will be + // renewed on every scan, which is too often. When we cache + // this certificate in a moment, it will be the default again. certCacheMu.Lock() delete(certCache, "") certCacheMu.Unlock() } - _, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand) + _, err := CacheManagedCertificate(cert.Names[0], cert.Config) if err != nil { - if client.AllowPrompts { + if allowPrompts { return err // operator is present, so report error immediately } - log.Printf("[ERROR] Caching renewed certificate: %v", err) + log.Printf("[ERROR] %v", err) } } for _, cert := range deleted { @@ -140,7 +149,9 @@ func renewManagedCertificates(allowPrompts bool) (err error) { return nil } -func updateOCSPStaples() { +// UpdateOCSPStaples updates the OCSP stapling in all +// eligible, cached certificates. +func UpdateOCSPStaples() { // Create a temporary place to store updates // until we release the potentially long-lived // read lock and use a short-lived write lock. @@ -186,7 +197,7 @@ func updateOCSPStaples() { err := stapleOCSP(&cert, nil) if err != nil { if cert.OCSP != nil { - // if it was no staple before, that's fine, otherwise we should log the error + // if there was no staple before, that's fine; otherwise we should log the error log.Printf("[ERROR] Checking OCSP for %v: %v", cert.Names, err) } continue @@ -215,6 +226,3 @@ func updateOCSPStaples() { } certCacheMu.Unlock() } - -// renewDurationBefore is how long before expiration to renew certificates. -const renewDurationBefore = (24 * time.Hour) * 30 diff --git a/caddytls/setup.go b/caddytls/setup.go new file mode 100644 index 000000000..f9cfc9847 --- /dev/null +++ b/caddytls/setup.go @@ -0,0 +1,278 @@ +package caddytls + +import ( + "bytes" + "crypto/tls" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "tls", + Action: setupTLS, + }) +} + +// setupTLS 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 setupTLS(c *caddy.Controller) error { + configGetter, ok := configGetters[c.ServerType()] + if !ok { + return fmt.Errorf("no caddytls.ConfigGetter for %s server type; must call RegisterConfigGetter", c.ServerType()) + } + config := configGetter(c.Key) + if config == nil { + return fmt.Errorf("no caddytls.Config to set up for %s", c.Key) + } + + config.Enabled = true + + for c.Next() { + var certificateFile, keyFile, loadDir, maxCerts string + + args := c.RemainingArgs() + switch len(args) { + case 1: + // even if the email is one of the special values below, + // it is still necessary for future analysis that we store + // that value in the ACMEEmail field. + config.ACMEEmail = args[0] + + // user can force-disable managed TLS this way + if args[0] == "off" { + config.Enabled = false + return nil + } + + // user might want a temporary, in-memory, self-signed cert + if args[0] == "self_signed" { + config.SelfSigned = true + } + case 2: + certificateFile = args[0] + keyFile = args[1] + config.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 c.Errf("Wrong key type name or key type not supported: '%s'", c.Val()) + } + config.KeyType = value + case "protocols": + args := c.RemainingArgs() + if len(args) != 2 { + return c.ArgErr() + } + value, ok := supportedProtocols[strings.ToLower(args[0])] + if !ok { + return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) + } + config.ProtocolMinVersion = value + value, ok = supportedProtocols[strings.ToLower(args[1])] + if !ok { + return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) + } + config.ProtocolMaxVersion = value + case "ciphers": + for c.NextArg() { + value, ok := supportedCiphersMap[strings.ToUpper(c.Val())] + if !ok { + return c.Errf("Wrong cipher name or cipher not supported: '%s'", c.Val()) + } + config.Ciphers = append(config.Ciphers, value) + } + case "clients": + clientCertList := c.RemainingArgs() + if len(clientCertList) == 0 { + return c.ArgErr() + } + + listStart, mustProvideCA := 1, true + switch clientCertList[0] { + case "request": + config.ClientAuth = tls.RequestClientCert + mustProvideCA = false + case "require": + config.ClientAuth = tls.RequireAnyClientCert + mustProvideCA = false + case "verify_if_given": + config.ClientAuth = tls.VerifyClientCertIfGiven + default: + config.ClientAuth = tls.RequireAndVerifyClientCert + listStart = 0 + } + if mustProvideCA && len(clientCertList) <= listStart { + return c.ArgErr() + } + + config.ClientCerts = clientCertList[listStart:] + case "load": + c.Args(&loadDir) + config.Manual = true + case "max_certs": + c.Args(&maxCerts) + config.OnDemand = true + case "dns": + args := c.RemainingArgs() + if len(args) != 1 { + return c.ArgErr() + } + dnsProvName := args[0] + if _, ok := dnsProviders[dnsProvName]; !ok { + return c.Errf("Unsupported DNS provider '%s'", args[0]) + } + config.DNSProvider = args[0] + default: + return 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 c.ArgErr() + } + + // set certificate limit if on-demand TLS is enabled + if maxCerts != "" { + maxCertsNum, err := strconv.Atoi(maxCerts) + if err != nil || maxCertsNum < 1 { + return 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 !config.Enabled || !config.Manual { + continue + } + + // load a single certificate and key, if specified + if certificateFile != "" && keyFile != "" { + err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile) + if err != nil { + return c.Errf("Unable to load certificate and key files for '%s': %v", c.Key, 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 err + } + } + } + + SetDefaultTLSParams(config) + + // generate self-signed cert if needed + if config.SelfSigned { + err := makeSelfSignedCert(config) + if err != nil { + return fmt.Errorf("self-signed: %v", err) + } + } + + return 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 *caddy.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.Key, err) + } + log.Printf("[INFO] Successfully loaded TLS assets from %s", path) + } + return nil + }) +} diff --git a/caddy/https/setup_test.go b/caddytls/setup_test.go similarity index 70% rename from caddy/https/setup_test.go rename to caddytls/setup_test.go index 59a772c45..ceb1c2a4a 100644 --- a/caddy/https/setup_test.go +++ b/caddytls/setup_test.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "crypto/tls" @@ -7,7 +7,7 @@ import ( "os" "testing" - "github.com/mholt/caddy/caddy/setup" + "github.com/mholt/caddy" "github.com/xenolf/lego/acme" ) @@ -32,32 +32,29 @@ func TestMain(m *testing.M) { } func TestSetupParseBasic(t *testing.T) { - c := setup.NewTestController(`tls ` + certFile + ` ` + keyFile + ``) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(`tls ` + certFile + ` ` + keyFile + ``) - _, err := Setup(c) + err := setupTLS(c) if err != nil { t.Errorf("Expected no errors, got: %v", err) } // Basic checks - if !c.TLS.Manual { + if !cfg.Manual { t.Error("Expected TLS Manual=true, but was false") } - if !c.TLS.Enabled { + if !cfg.Enabled { t.Error("Expected TLS Enabled=true, but was false") } // Security defaults - if c.TLS.ProtocolMinVersion != tls.VersionTLS10 { - t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion) + if cfg.ProtocolMinVersion != tls.VersionTLS11 { + t.Errorf("Expected 'tls1.1 (0x0302)' as ProtocolMinVersion, got %#v", cfg.ProtocolMinVersion) } - if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 { - t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion) - } - - // KeyType default - if KeyType != acme.RSA2048 { - t.Errorf("Expected '2048' as KeyType, got %#v", KeyType) + if cfg.ProtocolMaxVersion != tls.VersionTLS12 { + t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", cfg.ProtocolMaxVersion) } // Cipher checks @@ -76,27 +73,27 @@ func TestSetupParseBasic(t *testing.T) { } // Ensure count is correct (plus one for TLS_FALLBACK_SCSV) - if len(c.TLS.Ciphers) != len(expectedCiphers) { + if len(cfg.Ciphers) != len(expectedCiphers) { t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v", - len(expectedCiphers), len(c.TLS.Ciphers)) + len(expectedCiphers), len(cfg.Ciphers)) } // Ensure ordering is correct - for i, actual := range c.TLS.Ciphers { + for i, actual := range cfg.Ciphers { if actual != expectedCiphers[i] { t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual) } } - if !c.TLS.PreferServerCipherSuites { + if !cfg.PreferServerCipherSuites { t.Error("Expected PreferServerCipherSuites = true, but was false") } } func TestSetupParseIncompleteParams(t *testing.T) { // Using tls without args is an error because it's unnecessary. - c := setup.NewTestController(`tls`) - _, err := Setup(c) + c := caddy.NewTestController(`tls`) + err := setupTLS(c) if err == nil { t.Error("Expected an error, but didn't get one") } @@ -104,26 +101,28 @@ func TestSetupParseIncompleteParams(t *testing.T) { func TestSetupParseWithOptionalParams(t *testing.T) { params := `tls ` + certFile + ` ` + keyFile + ` { - protocols ssl3.0 tls1.2 + protocols tls1.0 tls1.2 ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 }` - c := setup.NewTestController(params) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(params) - _, err := Setup(c) + err := setupTLS(c) if err != nil { t.Errorf("Expected no errors, got: %v", err) } - if c.TLS.ProtocolMinVersion != tls.VersionSSL30 { - t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion) + if cfg.ProtocolMinVersion != tls.VersionTLS10 { + t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", cfg.ProtocolMinVersion) } - if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 { - t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion) + if cfg.ProtocolMaxVersion != tls.VersionTLS12 { + t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %#v", cfg.ProtocolMaxVersion) } - if len(c.TLS.Ciphers)-1 != 3 { - t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1) + if len(cfg.Ciphers)-1 != 3 { + t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1) } } @@ -131,38 +130,28 @@ func TestSetupDefaultWithOptionalParams(t *testing.T) { params := `tls { ciphers RSA-3DES-EDE-CBC-SHA }` - c := setup.NewTestController(params) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(params) - _, err := Setup(c) + err := setupTLS(c) if err != nil { t.Errorf("Expected no errors, got: %v", err) } - if len(c.TLS.Ciphers)-1 != 1 { - t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1) + if len(cfg.Ciphers)-1 != 1 { + t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(cfg.Ciphers)-1) } } -// TODO: If we allow this... but probably not a good idea. -// func TestSetupDisableHTTPRedirect(t *testing.T) { -// c := NewTestController(`tls { -// allow_http -// }`) -// _, err := TLS(c) -// if err != nil { -// t.Errorf("Expected no error, but got %v", err) -// } -// if !c.TLS.DisableHTTPRedir { -// t.Error("Expected HTTP redirect to be disabled, but it wasn't") -// } -// } - func TestSetupParseWithWrongOptionalParams(t *testing.T) { // Test protocols wrong params params := `tls ` + certFile + ` ` + keyFile + ` { protocols ssl tls }` - c := setup.NewTestController(params) - _, err := Setup(c) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(params) + err := setupTLS(c) if err == nil { t.Errorf("Expected errors, but no error returned") } @@ -171,8 +160,10 @@ func TestSetupParseWithWrongOptionalParams(t *testing.T) { params = `tls ` + certFile + ` ` + keyFile + ` { ciphers not-valid-cipher }` - c = setup.NewTestController(params) - _, err = Setup(c) + cfg = new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c = caddy.NewTestController(params) + err = setupTLS(c) if err == nil { t.Errorf("Expected errors, but no error returned") } @@ -181,8 +172,10 @@ func TestSetupParseWithWrongOptionalParams(t *testing.T) { params = `tls { key_type ab123 }` - c = setup.NewTestController(params) - _, err = Setup(c) + cfg = new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c = caddy.NewTestController(params) + err = setupTLS(c) if err == nil { t.Errorf("Expected errors, but no error returned") } @@ -193,8 +186,10 @@ func TestSetupParseWithClientAuth(t *testing.T) { params := `tls ` + certFile + ` ` + keyFile + ` { clients }` - c := setup.NewTestController(params) - _, err := Setup(c) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(params) + err := setupTLS(c) if err == nil { t.Errorf("Expected an error, but no error returned") } @@ -224,8 +219,10 @@ func TestSetupParseWithClientAuth(t *testing.T) { clients verify_if_given }`, tls.VerifyClientCertIfGiven, true, noCAs}, } { - c := setup.NewTestController(caseData.params) - _, err := Setup(c) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(caseData.params) + err := setupTLS(c) if caseData.expectedErr { if err == nil { t.Errorf("In case %d: Expected an error, got: %v", caseNumber, err) @@ -236,17 +233,17 @@ func TestSetupParseWithClientAuth(t *testing.T) { t.Errorf("In case %d: Expected no errors, got: %v", caseNumber, err) } - if caseData.clientAuthType != c.TLS.ClientAuth { + if caseData.clientAuthType != cfg.ClientAuth { t.Errorf("In case %d: Expected TLS client auth type %v, got: %v", - caseNumber, caseData.clientAuthType, c.TLS.ClientAuth) + caseNumber, caseData.clientAuthType, cfg.ClientAuth) } - if count := len(c.TLS.ClientCerts); count < len(caseData.expectedCAs) { + if count := len(cfg.ClientCerts); count < len(caseData.expectedCAs) { t.Fatalf("In case %d: Expected %d client certs, had %d", caseNumber, len(caseData.expectedCAs), count) } for idx, expected := range caseData.expectedCAs { - if actual := c.TLS.ClientCerts[idx]; actual != expected { + if actual := cfg.ClientCerts[idx]; actual != expected { t.Errorf("In case %d: Expected %dth client cert file to be '%s', but was '%s'", caseNumber, idx, expected, actual) } @@ -258,15 +255,17 @@ func TestSetupParseWithKeyType(t *testing.T) { params := `tls { key_type p384 }` - c := setup.NewTestController(params) + cfg := new(Config) + RegisterConfigGetter("", func(key string) *Config { return cfg }) + c := caddy.NewTestController(params) - _, err := Setup(c) + err := setupTLS(c) if err != nil { t.Errorf("Expected no errors, got: %v", err) } - if KeyType != acme.EC384 { - t.Errorf("Expected 'P384' as KeyType, got %#v", KeyType) + if cfg.KeyType != acme.EC384 { + t.Errorf("Expected 'P384' as KeyType, got %#v", cfg.KeyType) } } diff --git a/caddy/https/storage.go b/caddytls/storage.go similarity index 62% rename from caddy/https/storage.go rename to caddytls/storage.go index 5d487837f..1a00a9de7 100644 --- a/caddy/https/storage.go +++ b/caddytls/storage.go @@ -1,19 +1,48 @@ -package https +package caddytls import ( + "fmt" + "net/url" "path/filepath" "strings" - "github.com/mholt/caddy/caddy/assets" + "github.com/mholt/caddy" ) -// storage is used to get file paths in a consistent, -// cross-platform way for persisting Let's Encrypt assets -// on the file system. -var storage = Storage(filepath.Join(assets.Path(), "letsencrypt")) +// StorageFor gets the storage value associated with the +// caURL, which should be unique for every different +// ACME CA. +func StorageFor(caURL string) (Storage, error) { + if caURL == "" { + caURL = DefaultCAUrl + } + if caURL == "" { + return "", fmt.Errorf("cannot create storage without CA URL") + } + caURL = strings.ToLower(caURL) + + // scheme required or host will be parsed as path (as of Go 1.6) + if !strings.Contains(caURL, "://") { + caURL = "https://" + caURL + } + + u, err := url.Parse(caURL) + if err != nil { + return "", fmt.Errorf("%s: unable to parse CA URL: %v", caURL, err) + } + + if u.Host == "" { + return "", fmt.Errorf("%s: no host in CA URL", caURL) + } + + return Storage(filepath.Join(storageBasePath, u.Host)), nil +} // Storage is a root directory and facilitates -// forming file paths derived from it. +// forming file paths derived from it. It is used +// to get file paths in a consistent, cross- +// platform way for persisting ACME assets. +// on the file system. type Storage string // Sites gets the directory that stores site certificate and keys. @@ -23,21 +52,25 @@ func (s Storage) Sites() string { // Site returns the path to the folder containing assets for domain. func (s Storage) Site(domain string) string { + domain = strings.ToLower(domain) return filepath.Join(s.Sites(), domain) } // SiteCertFile returns the path to the certificate file for domain. func (s Storage) SiteCertFile(domain string) string { + domain = strings.ToLower(domain) return filepath.Join(s.Site(domain), domain+".crt") } // SiteKeyFile returns the path to domain's private key file. func (s Storage) SiteKeyFile(domain string) string { + domain = strings.ToLower(domain) return filepath.Join(s.Site(domain), domain+".key") } // SiteMetaFile returns the path to the domain's asset metadata file. func (s Storage) SiteMetaFile(domain string) string { + domain = strings.ToLower(domain) return filepath.Join(s.Site(domain), domain+".json") } @@ -51,6 +84,7 @@ func (s Storage) User(email string) string { if email == "" { email = emptyEmail } + email = strings.ToLower(email) return filepath.Join(s.Users(), email) } @@ -60,6 +94,7 @@ func (s Storage) UserRegFile(email string) string { if email == "" { email = emptyEmail } + email = strings.ToLower(email) fileName := emailUsername(email) if fileName == "" { fileName = "registration" @@ -73,6 +108,7 @@ func (s Storage) UserKeyFile(email string) string { if email == "" { email = emptyEmail } + email = strings.ToLower(email) fileName := emailUsername(email) if fileName == "" { fileName = "private" @@ -92,3 +128,7 @@ func emailUsername(email string) string { } return email[:at] } + +// storageBasePath is the root path in which all TLS/ACME assets are +// stored. Do not change this value during the lifetime of the program. +var storageBasePath = filepath.Join(caddy.AssetsPath(), "acme") diff --git a/caddy/https/storage_test.go b/caddytls/storage_test.go similarity index 62% rename from caddy/https/storage_test.go rename to caddytls/storage_test.go index 85c2220eb..e9175af96 100644 --- a/caddy/https/storage_test.go +++ b/caddytls/storage_test.go @@ -1,35 +1,82 @@ -package https +package caddytls import ( "path/filepath" "testing" ) +func TestStorageFor(t *testing.T) { + // first try without DefaultCAUrl set + DefaultCAUrl = "" + _, err := StorageFor("") + if err == nil { + t.Errorf("Without a default CA, expected error, but didn't get one") + } + st, err := StorageFor("https://example.com/foo") + if err != nil { + t.Errorf("Without a default CA but given input, expected no error, but got: %v", err) + } + if string(st) != filepath.Join(storageBasePath, "example.com") { + t.Errorf("Without a default CA but given input, expected '%s' not '%s'", "example.com", st) + } + + // try with the DefaultCAUrl set + DefaultCAUrl = "https://defaultCA/directory" + for i, test := range []struct { + input, expect string + shouldErr bool + }{ + {"https://acme-staging.api.letsencrypt.org/directory", "acme-staging.api.letsencrypt.org", false}, + {"https://foo/boo?bar=q", "foo", false}, + {"http://foo", "foo", false}, + {"", "defaultca", false}, + {"https://FooBar/asdf", "foobar", false}, + {"noscheme/path", "noscheme", false}, + {"/nohost", "", true}, + {"https:///nohost", "", true}, + {"FooBar", "foobar", false}, + } { + st, err := StorageFor(test.input) + if err == nil && test.shouldErr { + t.Errorf("Test %d: Expected an error, but didn't get one", i) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d: Expected no errors, but got: %v", i, err) + } + want := filepath.Join(storageBasePath, test.expect) + if test.shouldErr { + want = "" + } + if string(st) != want { + t.Errorf("Test %d: Expected '%s' but got '%s'", i, want, string(st)) + } + } +} + func TestStorage(t *testing.T) { - storage = Storage("./le_test") + storage := Storage("./le_test") if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected { t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual) } - if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("test.com"); actual != expected { + if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("Test.com"); actual != expected { t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual) } - if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected { + if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("Test.com"); actual != expected { t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual) } if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected { t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual) } - if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected { + if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("TEST.COM"); actual != expected { t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual) } if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected { t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual) } - if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("me@example.com"); actual != expected { + if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("Me@example.com"); actual != expected { t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual) } - if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected { + if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("ME@EXAMPLE.COM"); actual != expected { t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual) } if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected { diff --git a/caddytls/tls.go b/caddytls/tls.go new file mode 100644 index 000000000..601556a11 --- /dev/null +++ b/caddytls/tls.go @@ -0,0 +1,187 @@ +// Package caddytls facilitates the management of TLS assets and integrates +// Let's Encrypt functionality into Caddy with first-class support for +// creating and renewing certificates automatically. +package caddytls + +import ( + "encoding/json" + "io/ioutil" + "net" + "os" + "strings" + + "github.com/xenolf/lego/acme" +) + +// 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) != "" && + + // must not contain wildcard (*) characters (until CA supports it) + !strings.Contains(hostname, "*") && + + // must not start or end with a dot + !strings.HasPrefix(hostname, ".") && + !strings.HasSuffix(hostname, ".") && + + // cannot be an IP address, see + // https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt + net.ParseIP(hostname) == nil +} + +// existingCertAndKey returns true if the hostname has +// a certificate and private key in storage already under +// the storage provided, otherwise it returns false. +func existingCertAndKey(storage Storage, hostname string) bool { + _, err := os.Stat(storage.SiteCertFile(hostname)) + if err != nil { + return false + } + _, err = os.Stat(storage.SiteKeyFile(hostname)) + 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(storage Storage, 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 +} + +// Revoke revokes the certificate for host via ACME protocol. +// It assumes the certificate was obtained from the +// CA at DefaultCAUrl. +func Revoke(host string) error { + client, err := newACMEClient(new(Config), true) + if err != nil { + return err + } + return client.Revoke(host) +} + +// tlsSniSolver is a type that can solve tls-sni challenges using +// an existing listener and our custom, in-memory certificate cache. +type tlsSniSolver struct{} + +// Present adds the challenge certificate to the cache. +func (s tlsSniSolver) Present(domain, token, keyAuth string) error { + cert, err := acme.TLSSNI01ChallengeCert(keyAuth) + if err != nil { + return err + } + cacheCertificate(Certificate{ + Certificate: cert, + Names: []string{domain}, + }) + return nil +} + +// CleanUp removes the challenge certificate from the cache. +func (s tlsSniSolver) CleanUp(domain, token, keyAuth string) error { + uncacheCertificate(domain) + return nil +} + +// ConfigHolder is any type that has a Config; it presumably is +// connected to a hostname and port on which it is serving. +type ConfigHolder interface { + TLSConfig() *Config + Host() string + Port() string +} + +// QualifiesForManagedTLS returns true if c qualifies for +// for managed TLS (but not on-demand TLS specifically). +// It does NOT check to see if a cert and key already exist +// for the config. If the return value is true, you should +// be OK to set c.TLSConfig().Managed to true; then you should +// check that value in the future instead, because the process +// of setting up the config may make it look like it doesn't +// qualify even though it originally did. +func QualifiesForManagedTLS(c ConfigHolder) bool { + if c == nil { + return false + } + tlsConfig := c.TLSConfig() + if tlsConfig == nil { + return false + } + + return (!tlsConfig.Manual || tlsConfig.OnDemand) && // user might provide own cert and key + + // if self-signed, we've already generated one to use + !tlsConfig.SelfSigned && + + // user can force-disable managed TLS + c.Port() != "80" && + tlsConfig.ACMEEmail != "off" && + + // we get can't certs for some kinds of hostnames, but + // on-demand TLS allows empty hostnames at startup + (HostQualifies(c.Host()) || tlsConfig.OnDemand) +} + +// DNSProviderConstructor is a function that takes credentials and +// returns a type that can solve the ACME DNS challenges. +type DNSProviderConstructor func(credentials ...string) (acme.ChallengeProvider, error) + +// dnsProviders is the list of DNS providers that have been plugged in. +var dnsProviders = make(map[string]DNSProviderConstructor) + +// RegisterDNSProvider registers provider by name for solving the ACME DNS challenge. +func RegisterDNSProvider(name string, provider DNSProviderConstructor) { + dnsProviders[name] = provider +} + +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 + + // DefaultCAUrl is the default URL to the CA's ACME directory endpoint. + // It's very important to set this unless you set it in every Config. + DefaultCAUrl string + + // DefaultKeyType is used as the type of key for new certificates + // when no other key type is specified. + DefaultKeyType = acme.RSA2048 +) diff --git a/caddytls/tls_test.go b/caddytls/tls_test.go new file mode 100644 index 000000000..c46e24947 --- /dev/null +++ b/caddytls/tls_test.go @@ -0,0 +1,165 @@ +package caddytls + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/xenolf/lego/acme" +) + +func TestHostQualifies(t *testing.T) { + for i, test := range []struct { + host string + expect bool + }{ + {"example.com", true}, + {"sub.example.com", true}, + {"Sub.Example.COM", true}, + {"127.0.0.1", false}, + {"127.0.1.5", false}, + {"69.123.43.94", false}, + {"::1", false}, + {"::", false}, + {"0.0.0.0", false}, + {"", false}, + {" ", false}, + {"*.example.com", false}, + {".com", false}, + {"example.com.", false}, + {"localhost", false}, + {"local", true}, + {"devsite", true}, + {"192.168.1.3", false}, + {"10.0.2.1", false}, + {"169.112.53.4", false}, + } { + actual := HostQualifies(test.host) + if actual != test.expect { + t.Errorf("Test %d: Expected HostQualifies(%s)=%v, but got %v", + i, test.host, test.expect, actual) + } + } +} + +type holder struct { + host, port string + cfg *Config +} + +func (h holder) TLSConfig() *Config { return h.cfg } +func (h holder) Host() string { return h.host } +func (h holder) Port() string { return h.port } + +func TestQualifiesForManagedTLS(t *testing.T) { + for i, test := range []struct { + cfg ConfigHolder + expect bool + }{ + {holder{host: ""}, false}, + {holder{host: "localhost"}, false}, + {holder{host: "123.44.3.21"}, false}, + {holder{host: "example.com"}, false}, + {holder{host: "", cfg: new(Config)}, false}, + {holder{host: "localhost", cfg: new(Config)}, false}, + {holder{host: "123.44.3.21", cfg: new(Config)}, false}, + {holder{host: "example.com", cfg: new(Config)}, true}, + {holder{host: "*.example.com", cfg: new(Config)}, false}, + {holder{host: "example.com", cfg: &Config{Manual: true}}, false}, + {holder{host: "example.com", cfg: &Config{ACMEEmail: "off"}}, false}, + {holder{host: "example.com", cfg: &Config{ACMEEmail: "foo@bar.com"}}, true}, + {holder{host: "example.com", port: "80"}, false}, + {holder{host: "example.com", port: "1234", cfg: new(Config)}, true}, + {holder{host: "example.com", port: "443", cfg: new(Config)}, true}, + {holder{host: "example.com", port: "80"}, false}, + } { + if got, want := QualifiesForManagedTLS(test.cfg), test.expect; got != want { + t.Errorf("Test %d: Expected %v but got %v", i, want, got) + } + } +} + +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(storage, 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(storage, domain) { + t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain) + } + + err := saveCertResource(storage, acme.CertificateResource{ + Domain: domain, + PrivateKey: []byte("key"), + Certificate: []byte("cert"), + }) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if !existingCertAndKey(storage, domain) { + t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain) + } +} diff --git a/caddy/https/user.go b/caddytls/user.go similarity index 80% rename from caddy/https/user.go rename to caddytls/user.go index a7e6e5f62..d10680b91 100644 --- a/caddy/https/user.go +++ b/caddytls/user.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "bufio" @@ -14,7 +14,6 @@ import ( "os" "strings" - "github.com/mholt/caddy/server" "github.com/xenolf/lego/acme" ) @@ -40,11 +39,77 @@ func (u User) GetPrivateKey() crypto.PrivateKey { return u.key } -// getUser loads the user with the given email from disk. -// If the user does not exist, it will create a new one, -// but it does NOT save new users to the disk or register -// them via ACME. It does NOT prompt the user. -func getUser(email string) (User, error) { +// newUser creates a new User for the given email address +// with a new private key. This function does NOT save the +// user to disk or register it via ACME. If you want to use +// a user account that might already exist, call getUser +// instead. It does NOT prompt the user. +func newUser(email string) (User, error) { + user := User{Email: email} + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return user, errors.New("error generating private key: " + err.Error()) + } + user.key = privateKey + return user, nil +} + +// getEmail does everything it can to obtain an email +// address from the user within the scope of storage +// to use for ACME TLS. If it cannot get an email +// address, it returns empty string. (It will warn the +// user of the consequences of an empty email.) This +// function MAY prompt the user for input. If userPresent +// is false, the operator will NOT be prompted and an +// empty email may be returned. +func getEmail(storage Storage, userPresent bool) string { + // First try memory (command line flag or typed by user previously) + leEmail := DefaultEmail + if leEmail == "" { + // Then try to get most recent user email + userDirs, err := ioutil.ReadDir(storage.Users()) + if err == nil { + var mostRecent os.FileInfo + for _, dir := range userDirs { + if !dir.IsDir() { + continue + } + if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) { + leEmail = dir.Name() + DefaultEmail = leEmail // save for next time + mostRecent = dir + } + } + } + } + if leEmail == "" && userPresent { + // Alas, we must bother the user and ask for an email address; + // if they proceed they also agree to the SA. + reader := bufio.NewReader(stdin) + fmt.Println("\nYour sites will be served over HTTPS automatically using Let's Encrypt.") + fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:") + fmt.Println(" " + saURL) // TODO: Show current SA link + fmt.Println("Please enter your email address so you can recover your account if needed.") + fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.") + fmt.Print("Email address: ") + var err error + leEmail, err = reader.ReadString('\n') + if err != nil { + return "" + } + leEmail = strings.TrimSpace(leEmail) + DefaultEmail = leEmail + Agreed = true + } + return strings.ToLower(leEmail) +} + +// getUser loads the user with the given email from disk +// using the provided storage. If the user does not exist, +// it will create a new one, but it does NOT save new +// users to the disk or register them via ACME. It does +// NOT prompt the user. +func getUser(storage Storage, email string) (User, error) { var user User // open user file @@ -75,8 +140,10 @@ func getUser(email string) (User, error) { // saveUser persists a user's key and account registration // to the file system. It does NOT register the user via ACME -// or prompt the user. -func saveUser(user User) error { +// or prompt the user. You must also pass in the storage +// wherein the user should be saved. It should be the storage +// for the CA with which user has an account. +func saveUser(storage Storage, user User) error { // make user account folder err := os.MkdirAll(storage.User(user.Email), 0700) if err != nil { @@ -98,73 +165,6 @@ func saveUser(user User) error { return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600) } -// newUser creates a new User for the given email address -// with a new private key. This function does NOT save the -// user to disk or register it via ACME. If you want to use -// a user account that might already exist, call getUser -// instead. It does NOT prompt the user. -func newUser(email string) (User, error) { - user := User{Email: email} - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - return user, errors.New("error generating private key: " + err.Error()) - } - user.key = privateKey - return user, nil -} - -// getEmail does everything it can to obtain an email -// address from the user to use for TLS for cfg. If it -// cannot get an email address, it returns empty string. -// (It will warn the user of the consequences of an -// empty email.) This function MAY prompt the user for -// input. If userPresent is false, the operator will -// NOT be prompted and an empty email may be returned. -func getEmail(cfg server.Config, userPresent bool) string { - // First try the tls directive from the Caddyfile - leEmail := cfg.TLS.LetsEncryptEmail - if leEmail == "" { - // Then try memory (command line flag or typed by user previously) - leEmail = DefaultEmail - } - if leEmail == "" { - // Then try to get most recent user email ~/.caddy/users file - userDirs, err := ioutil.ReadDir(storage.Users()) - if err == nil { - var mostRecent os.FileInfo - for _, dir := range userDirs { - if !dir.IsDir() { - continue - } - if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) { - leEmail = dir.Name() - DefaultEmail = leEmail // save for next time - } - } - } - } - if leEmail == "" && userPresent { - // Alas, we must bother the user and ask for an email address; - // if they proceed they also agree to the SA. - reader := bufio.NewReader(stdin) - fmt.Println("\nYour sites will be served over HTTPS automatically using Let's Encrypt.") - fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:") - fmt.Println(" " + saURL) // TODO: Show current SA link - fmt.Println("Please enter your email address so you can recover your account if needed.") - fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.") - fmt.Print("Email address: ") - var err error - leEmail, err = reader.ReadString('\n') - if err != nil { - return "" - } - leEmail = strings.TrimSpace(leEmail) - DefaultEmail = leEmail - Agreed = true - } - return leEmail -} - // promptUserAgreement prompts the user to agree to the agreement // at agreementURL via stdin. If the agreement has changed, then pass // true as the second argument. If this is the user's first time diff --git a/caddy/https/user_test.go b/caddytls/user_test.go similarity index 74% rename from caddy/https/user_test.go rename to caddytls/user_test.go index c1d115e1f..67f730827 100644 --- a/caddy/https/user_test.go +++ b/caddytls/user_test.go @@ -1,4 +1,4 @@ -package https +package caddytls import ( "bytes" @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/mholt/caddy/server" "github.com/xenolf/lego/acme" ) @@ -54,8 +53,7 @@ func TestNewUser(t *testing.T) { } func TestSaveUser(t *testing.T) { - storage = Storage("./testdata") - defer os.RemoveAll(string(storage)) + defer os.RemoveAll(string(testStorage)) email := "me@foobar.com" user, err := newUser(email) @@ -63,25 +61,24 @@ func TestSaveUser(t *testing.T) { t.Fatalf("Error creating user: %v", err) } - err = saveUser(user) + err = saveUser(testStorage, user) if err != nil { t.Fatalf("Error saving user: %v", err) } - _, err = os.Stat(storage.UserRegFile(email)) + _, err = os.Stat(testStorage.UserRegFile(email)) if err != nil { t.Errorf("Cannot access user registration file, error: %v", err) } - _, err = os.Stat(storage.UserKeyFile(email)) + _, err = os.Stat(testStorage.UserKeyFile(email)) if err != nil { t.Errorf("Cannot access user private key file, error: %v", err) } } func TestGetUserDoesNotAlreadyExist(t *testing.T) { - storage = Storage("./testdata") - defer os.RemoveAll(string(storage)) + defer os.RemoveAll(string(testStorage)) - user, err := getUser("user_does_not_exist@foobar.com") + user, err := getUser(testStorage, "user_does_not_exist@foobar.com") if err != nil { t.Fatalf("Error getting user: %v", err) } @@ -92,8 +89,7 @@ func TestGetUserDoesNotAlreadyExist(t *testing.T) { } func TestGetUserAlreadyExists(t *testing.T) { - storage = Storage("./testdata") - defer os.RemoveAll(string(storage)) + defer os.RemoveAll(string(testStorage)) email := "me@foobar.com" @@ -102,13 +98,13 @@ func TestGetUserAlreadyExists(t *testing.T) { if err != nil { t.Fatalf("Error creating user: %v", err) } - err = saveUser(user) + err = saveUser(testStorage, user) if err != nil { t.Fatalf("Error saving user: %v", err) } // Expect to load user from disk - user2, err := getUser(email) + user2, err := getUser(testStorage, email) if err != nil { t.Fatalf("Error getting user: %v", err) } @@ -125,48 +121,38 @@ func TestGetUserAlreadyExists(t *testing.T) { } func TestGetEmail(t *testing.T) { + storageBasePath = string(testStorage) // to contain calls that create a new Storage... + // let's not clutter up the output origStdout := os.Stdout os.Stdout = nil defer func() { os.Stdout = origStdout }() - storage = Storage("./testdata") - defer os.RemoveAll(string(storage)) + defer os.RemoveAll(string(testStorage)) DefaultEmail = "test2@foo.com" - // Test1: Use email in config - config := server.Config{ - TLS: server.TLSConfig{ - LetsEncryptEmail: "test1@foo.com", - }, - } - actual := getEmail(config, true) - if actual != "test1@foo.com" { - t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "test1@foo.com", actual) - } - - // Test2: Use default email from flag (or user previously typing it) - actual = getEmail(server.Config{}, true) + // Test1: Use default email from flag (or user previously typing it) + actual := getEmail(testStorage, true) if actual != DefaultEmail { - t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual) + t.Errorf("Did not get correct email from memory; expected '%s' but got '%s'", DefaultEmail, actual) } - // Test3: Get input from user + // Test2: Get input from user DefaultEmail = "" stdin = new(bytes.Buffer) _, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n")) if err != nil { t.Fatalf("Could not simulate user input, error: %v", err) } - actual = getEmail(server.Config{}, true) + actual = getEmail(testStorage, true) if actual != "test3@foo.com" { t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual) } - // Test4: Get most recent email from before + // Test3: Get most recent email from before DefaultEmail = "" for i, eml := range []string{ - "test4-3@foo.com", + "TEST4-3@foo.com", // test case insensitivity "test4-2@foo.com", "test4-1@foo.com", } { @@ -174,23 +160,25 @@ func TestGetEmail(t *testing.T) { if err != nil { t.Fatalf("Error creating user %d: %v", i, err) } - err = saveUser(u) + err = saveUser(testStorage, u) if err != nil { t.Fatalf("Error saving user %d: %v", i, err) } // Change modified time so they're all different, so the test becomes deterministic - f, err := os.Stat(storage.User(eml)) + f, err := os.Stat(testStorage.User(eml)) if err != nil { t.Fatalf("Could not access user folder for '%s': %v", eml, err) } chTime := f.ModTime().Add(-(time.Duration(i) * time.Second)) - if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil { + if err := os.Chtimes(testStorage.User(eml), chTime, chTime); err != nil { t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err) } } - actual = getEmail(server.Config{}, true) + actual = getEmail(testStorage, true) if actual != "test4-3@foo.com" { t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual) } } + +var testStorage = Storage("./testdata") diff --git a/middleware/commands.go b/commands.go similarity index 94% rename from middleware/commands.go rename to commands.go index 2aaeb6141..3e64c90b9 100644 --- a/middleware/commands.go +++ b/commands.go @@ -1,4 +1,4 @@ -package middleware +package caddy import ( "errors" @@ -10,8 +10,8 @@ import ( var runtimeGoos = runtime.GOOS -// SplitCommandAndArgs takes a command string and parses it -// shell-style into the command and its separate arguments. +// SplitCommandAndArgs takes a command string and parses it shell-style into the +// command and its separate arguments. func SplitCommandAndArgs(command string) (cmd string, args []string, err error) { var parts []string diff --git a/middleware/commands_test.go b/commands_test.go similarity index 99% rename from middleware/commands_test.go rename to commands_test.go index 3001e65a5..5de37c761 100644 --- a/middleware/commands_test.go +++ b/commands_test.go @@ -1,4 +1,4 @@ -package middleware +package caddy import ( "fmt" diff --git a/controller.go b/controller.go new file mode 100644 index 000000000..4be794821 --- /dev/null +++ b/controller.go @@ -0,0 +1,86 @@ +package caddy + +import ( + "strings" + + "github.com/mholt/caddy/caddyfile" +) + +// Controller is given to the setup function of directives which +// gives them access to be able to read tokens and do whatever +// they need to do. +type Controller struct { + caddyfile.Dispenser + + // The instance in which the setup is occurring + instance *Instance + + // Key is the key from the top of the server block, usually + // an address, hostname, or identifier of some sort. + Key string + + // 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 + + // ServerBlockKeyIndex is the 0-based index of this + // key as it appeared in the input at the head of the + // server block. + ServerBlockKeyIndex int + + // ServerBlockKeys is a list of keys that are + // associated with this server block. All these + // keys, consequently, share the same tokens. + ServerBlockKeys []string + + // ServerBlockStorage is used by a directive's + // setup function to persist state between all + // the keys on a server block. + ServerBlockStorage interface{} +} + +// ServerType gets the name of the server type that is being set up. +func (c *Controller) ServerType() string { + return c.instance.serverType +} + +// OnStartup adds fn to the list of callback functions to execute +// when the server is about to be started. +func (c *Controller) OnStartup(fn func() error) { + c.instance.onStartup = append(c.instance.onStartup, fn) +} + +// OnRestart adds fn to the list of callback functions to execute +// when the server is about to be restarted. +func (c *Controller) OnRestart(fn func() error) { + c.instance.onRestart = append(c.instance.onRestart, fn) +} + +// OnShutdown adds fn to the list of callback functions to execute +// when the server is about to be shut down.. +func (c *Controller) OnShutdown(fn func() error) { + c.instance.onShutdown = append(c.instance.onShutdown, fn) +} + +// 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{ + instance: &Instance{serverType: ""}, + Dispenser: caddyfile.NewDispenser("Testfile", strings.NewReader(input)), + OncePerServerBlock: func(f func() error) error { return f() }, + } +} diff --git a/dist/README.txt b/dist/README.txt index 298f35f7a..19699f047 100644 --- a/dist/README.txt +++ b/dist/README.txt @@ -1,4 +1,4 @@ -CADDY 0.8.3 +CADDY 0.9 beta 1 Website https://caddyserver.com @@ -14,15 +14,21 @@ Source Code https://github.com/caddyserver -For instructions on using Caddy, please see the user guide on the website. -For a list of what's new in this version, see CHANGES.txt. +For instructions on using Caddy, please see the user guide on +the website. For a list of what's new in this version, see +CHANGES.txt. -Please consider donating to the project if you think it is helpful, -especially if your company is using Caddy. There are also sponsorship -opportunities available! +The Caddy project accepts pull requests! That means you can make +changes to the code and submit it for review, and if it's good, +we'll use it! You can help thousands of Caddy users and level +up your Go programming game by contributing to Caddy's source. -If you have a question, bug report, or would like to contribute, please open an -issue or submit a pull request on GitHub. Your contributions do not go unnoticed! +To report bugs or request features, open an issue on GitHub. + +Want to support the project financially? Consider donating, +especially if your company is using Caddy. Believe me, your +contributions do not go unnoticed! We also have sponsorship +opportunities available. For a good time, follow @mholt6 on Twitter. diff --git a/dist/automate.go b/dist/automate.go index 594233f56..9b65475a1 100644 --- a/dist/automate.go +++ b/dist/automate.go @@ -12,12 +12,12 @@ import ( "github.com/mholt/archiver" ) -var buildScript, pkgDir, distDir, buildDir, releaseDir string +var buildScript, repoDir, distDir, buildDir, releaseDir string func init() { - pkgDir = filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "mholt", "caddy") - buildScript = filepath.Join(pkgDir, "build.bash") - distDir = filepath.Join(pkgDir, "dist") + repoDir = filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "mholt", "caddy") + buildScript = filepath.Join(repoDir, "caddy", "build.bash") + distDir = filepath.Join(repoDir, "dist") buildDir = filepath.Join(distDir, "builds") releaseDir = filepath.Join(distDir, "release") } @@ -98,7 +98,7 @@ func main() { func build(p platform, out string) error { cmd := exec.Command(buildScript, out) - cmd.Dir = pkgDir + cmd.Dir = repoDir cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "CGO_ENABLED=0") cmd.Env = append(cmd.Env, "GOOS="+p.os) @@ -132,8 +132,8 @@ func numProcs() int { // Not all supported platforms are listed since some are // problematic and we only build the most common ones. // These are just the pre-made, readily-available static -// builds, and we can add more upon request if there is -// enough demand. +// builds, and we can try to add more upon request if there +// is enough demand. var platforms = []platform{ {os: "darwin", arch: "amd64", archive: "zip"}, {os: "freebsd", arch: "386", archive: "tar.gz"}, diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 01722ed60..000000000 --- a/main_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "runtime" - "testing" -) - -func TestSetCPU(t *testing.T) { - currentCPU := runtime.GOMAXPROCS(-1) - maxCPU := runtime.NumCPU() - halfCPU := int(0.5 * float32(maxCPU)) - if halfCPU < 1 { - halfCPU = 1 - } - for i, test := range []struct { - input string - output int - shouldErr bool - }{ - {"1", 1, false}, - {"-1", currentCPU, true}, - {"0", currentCPU, true}, - {"100%", maxCPU, false}, - {"50%", halfCPU, false}, - {"110%", currentCPU, true}, - {"-10%", currentCPU, true}, - {"invalid input", currentCPU, true}, - {"invalid input%", currentCPU, true}, - {"9999", maxCPU, false}, // over available CPU - } { - err := setCPU(test.input) - if test.shouldErr && err == nil { - t.Errorf("Test %d: Expected error, but there wasn't any", i) - } - if !test.shouldErr && err != nil { - t.Errorf("Test %d: Expected no error, but there was one: %v", i, err) - } - if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected { - t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected) - } - // teardown - runtime.GOMAXPROCS(currentCPU) - } -} - -func TestSetVersion(t *testing.T) { - setVersion() - if !devBuild { - t.Error("Expected default to assume development build, but it didn't") - } - if got, want := appVersion, "(untracked dev build)"; got != want { - t.Errorf("Expected appVersion='%s', got: '%s'", want, got) - } - - gitTag = "v1.1" - setVersion() - if devBuild { - t.Error("Expected a stable build if gitTag is set with no changes") - } - if got, want := appVersion, "1.1"; got != want { - t.Errorf("Expected appVersion='%s', got: '%s'", want, got) - } - - gitTag = "" - gitNearestTag = "v1.0" - gitCommit = "deadbeef" - buildDate = "Fri Feb 26 06:53:17 UTC 2016" - setVersion() - if !devBuild { - t.Error("Expected inferring a dev build when gitTag is empty") - } - if got, want := appVersion, "1.0 (+deadbeef Fri Feb 26 06:53:17 UTC 2016)"; got != want { - t.Errorf("Expected appVersion='%s', got: '%s'", want, got) - } -} diff --git a/middleware/extensions/ext_test.go b/middleware/extensions/ext_test.go deleted file mode 100644 index f03eaa2f3..000000000 --- a/middleware/extensions/ext_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package extensions - -import ( - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/mholt/caddy/middleware" -) - -func TestExtensions(t *testing.T) { - rootDir := os.TempDir() - - // create a temporary page - path := filepath.Join(rootDir, "extensions_test.html") - _, err := os.Create(path) - if err != nil { - t.Fatal(err) - } - defer os.Remove(path) - - for i, test := range []struct { - path string - extensions []string - expectedURL string - }{ - {"/extensions_test", []string{".html"}, "/extensions_test.html"}, - {"/extensions_test/", []string{".html"}, "/extensions_test/"}, - {"/extensions_test", []string{".json"}, "/extensions_test"}, - {"/another_test", []string{".html"}, "/another_test"}, - {"", []string{".html"}, ""}, - } { - ex := Ext{ - Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - return 0, nil - }), - Root: rootDir, - Extensions: test.extensions, - } - - req, err := http.NewRequest("GET", test.path, nil) - if err != nil { - t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) - } - - rec := httptest.NewRecorder() - - ex.ServeHTTP(rec, req) - - if got := req.URL.String(); got != test.expectedURL { - t.Fatalf("Test %d: Got unexpected request URL: %q, wanted %q", i, got, test.expectedURL) - } - } -} diff --git a/middleware/markdown/testdata/blog/test.md b/middleware/markdown/testdata/blog/test.md deleted file mode 100644 index 93f07a493..000000000 --- a/middleware/markdown/testdata/blog/test.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Markdown test 1 -sitename: A Caddy website ---- - -## Welcome on the blog - -Body - -``` go -func getTrue() bool { - return true -} -``` diff --git a/middleware/markdown/testdata/docflags/template.txt b/middleware/markdown/testdata/docflags/template.txt deleted file mode 100644 index 2760d18d1..000000000 --- a/middleware/markdown/testdata/docflags/template.txt +++ /dev/null @@ -1,4 +0,0 @@ -Doc.var_string {{.Doc.var_string}} -Doc.var_bool {{.Doc.var_bool}} -DocFlags.var_string {{.DocFlags.var_string}} -DocFlags.var_bool {{.DocFlags.var_bool}} diff --git a/middleware/markdown/testdata/docflags/test.md b/middleware/markdown/testdata/docflags/test.md deleted file mode 100644 index 64ca7f78d..000000000 --- a/middleware/markdown/testdata/docflags/test.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -var_string: hello -var_bool: true ---- diff --git a/middleware/markdown/testdata/header.html b/middleware/markdown/testdata/header.html deleted file mode 100644 index cfbdc75b5..000000000 --- a/middleware/markdown/testdata/header.html +++ /dev/null @@ -1 +0,0 @@ -

Header for: {{.Doc.title}}

\ No newline at end of file diff --git a/middleware/markdown/testdata/log/test.md b/middleware/markdown/testdata/log/test.md deleted file mode 100644 index 476ab3015..000000000 --- a/middleware/markdown/testdata/log/test.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Markdown test 2 -sitename: A Caddy website ---- - -## Welcome on the blog - -Body - -``` go -func getTrue() bool { - return true -} -``` diff --git a/middleware/markdown/testdata/markdown_tpl.html b/middleware/markdown/testdata/markdown_tpl.html deleted file mode 100644 index 7c6978500..000000000 --- a/middleware/markdown/testdata/markdown_tpl.html +++ /dev/null @@ -1,11 +0,0 @@ - - - -{{.Doc.title}} - - -{{.Include "header.html"}} -Welcome to {{.Doc.sitename}}! -{{.Doc.body}} - - diff --git a/middleware/markdown/testdata/og/first.md b/middleware/markdown/testdata/og/first.md deleted file mode 100644 index 4d7a4251f..000000000 --- a/middleware/markdown/testdata/og/first.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: first_post -sitename: title ---- -# Test h1 diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go deleted file mode 100644 index 62fa4e250..000000000 --- a/middleware/middleware_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func TestIndexfile(t *testing.T) { - tests := []struct { - rootDir http.FileSystem - fpath string - indexFiles []string - shouldErr bool - expectedFilePath string //retun value - expectedBoolValue bool //return value - }{ - { - http.Dir("./templates/testdata"), - "/images/", - []string{"img.htm"}, - false, - "/images/img.htm", - true, - }, - } - for i, test := range tests { - actualFilePath, actualBoolValue := IndexFile(test.rootDir, test.fpath, test.indexFiles) - if actualBoolValue == true && test.shouldErr { - t.Errorf("Test %d didn't error, but it should have", i) - } else if actualBoolValue != true && !test.shouldErr { - t.Errorf("Test %d errored, but it shouldn't have; got %s", i, "Please Add a / at the end of fpath or the indexFiles doesnt exist") - } - if actualFilePath != test.expectedFilePath { - t.Fatalf("Test %d expected returned filepath to be %s, but got %s ", - i, test.expectedFilePath, actualFilePath) - - } - if actualBoolValue != test.expectedBoolValue { - t.Fatalf("Test %d expected returned bool value to be %v, but got %v ", - i, test.expectedBoolValue, actualBoolValue) - - } - } -} - -func TestSetLastModified(t *testing.T) { - nowTime := time.Now() - - // ovewrite the function to return reliable time - originalGetCurrentTimeFunc := currentTime - currentTime = func() time.Time { - return nowTime - } - defer func() { - currentTime = originalGetCurrentTimeFunc - }() - - pastTime := nowTime.Truncate(1 * time.Hour) - futureTime := nowTime.Add(1 * time.Hour) - - tests := []struct { - inputModTime time.Time - expectedIsHeaderSet bool - expectedLastModified string - }{ - { - inputModTime: pastTime, - expectedIsHeaderSet: true, - expectedLastModified: pastTime.UTC().Format(http.TimeFormat), - }, - { - inputModTime: nowTime, - expectedIsHeaderSet: true, - expectedLastModified: nowTime.UTC().Format(http.TimeFormat), - }, - { - inputModTime: futureTime, - expectedIsHeaderSet: true, - expectedLastModified: nowTime.UTC().Format(http.TimeFormat), - }, - { - inputModTime: time.Time{}, - expectedIsHeaderSet: false, - }, - } - - for i, test := range tests { - responseRecorder := httptest.NewRecorder() - errorPrefix := fmt.Sprintf("Test [%d]: ", i) - SetLastModifiedHeader(responseRecorder, test.inputModTime) - actualLastModifiedHeader := responseRecorder.Header().Get("Last-Modified") - - if test.expectedIsHeaderSet && actualLastModifiedHeader == "" { - t.Fatalf(errorPrefix + "Expected to find Last-Modified header, but found nothing") - } - - if !test.expectedIsHeaderSet && actualLastModifiedHeader != "" { - t.Fatalf(errorPrefix+"Did not expect to find Last-Modified header, but found one [%s].", actualLastModifiedHeader) - } - - if test.expectedLastModified != actualLastModifiedHeader { - t.Errorf(errorPrefix+"Expected Last-Modified content [%s], found [%s}", test.expectedLastModified, actualLastModifiedHeader) - } - } -} diff --git a/middleware/path.go b/middleware/path.go deleted file mode 100644 index 9c831e771..000000000 --- a/middleware/path.go +++ /dev/null @@ -1,44 +0,0 @@ -package middleware - -import ( - "os" - "strings" -) - -const caseSensitivePathEnv = "CASE_SENSITIVE_PATH" - -func init() { - initCaseSettings() -} - -// CaseSensitivePath determines if paths should be case sensitive. -// This is configurable via CASE_SENSITIVE_PATH environment variable. -// It defaults to false. -var CaseSensitivePath = true - -// initCaseSettings loads case sensitivity config from environment variable. -// -// This could have been in init, but init cannot be called from tests. -func initCaseSettings() { - switch os.Getenv(caseSensitivePathEnv) { - case "0", "false": - CaseSensitivePath = false - default: - CaseSensitivePath = true - } -} - -// Path represents a URI path, maybe with pattern characters. -type Path string - -// Matches checks to see if other matches p. -// -// Path matching will probably not always be a direct -// comparison; this method assures that paths can be -// easily and consistently matched. -func (p Path) Matches(other string) bool { - if CaseSensitivePath { - return strings.HasPrefix(string(p), other) - } - return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other)) -} diff --git a/middleware/roller.go b/middleware/roller.go deleted file mode 100644 index 995cabf91..000000000 --- a/middleware/roller.go +++ /dev/null @@ -1,27 +0,0 @@ -package middleware - -import ( - "io" - - "gopkg.in/natefinch/lumberjack.v2" -) - -// LogRoller implements a middleware that provides a rolling logger. -type LogRoller struct { - Filename string - MaxSize int - MaxAge int - MaxBackups int - LocalTime bool -} - -// GetLogWriter returns an io.Writer that writes to a rolling logger. -func (l LogRoller) GetLogWriter() io.Writer { - return &lumberjack.Logger{ - Filename: l.Filename, - MaxSize: l.MaxSize, - MaxAge: l.MaxAge, - MaxBackups: l.MaxBackups, - LocalTime: l.LocalTime, - } -} diff --git a/middleware/templates/testdata/images/header.html b/middleware/templates/testdata/images/header.html deleted file mode 100644 index 9c96e0e37..000000000 --- a/middleware/templates/testdata/images/header.html +++ /dev/null @@ -1 +0,0 @@ -

Header title

diff --git a/plugins.go b/plugins.go new file mode 100644 index 000000000..f66e24cda --- /dev/null +++ b/plugins.go @@ -0,0 +1,289 @@ +package caddy + +import ( + "fmt" + "net" + "sort" + + "github.com/mholt/caddy/caddyfile" +) + +// These are all the registered plugins. +var ( + // serverTypes is a map of registered server types. + serverTypes = make(map[string]ServerType) + + // plugins is a map of server type to map of plugin name to + // Plugin. These are the "general" plugins that may or may + // not be associated with a specific server type. If it's + // applicable to multiple server types or the server type is + // irrelevant, the key is empty string (""). But all plugins + // must have a name. + plugins = make(map[string]map[string]Plugin) + + // parsingCallbacks maps server type to map of directive + // to list of callback functions. These aren't really + // plugins on their own, but are often registered from + // plugins. + parsingCallbacks = make(map[string]map[string][]func() error) + + // caddyfileLoaders is the list of all Caddyfile loaders + // in registration order. + caddyfileLoaders []caddyfileLoader +) + +// DescribePlugins returns a string describing the registered plugins. +func DescribePlugins() string { + str := "Server types:\n" + for name := range serverTypes { + str += " " + name + "\n" + } + + // List the loaders in registration order + str += "\nCaddyfile loaders:\n" + for _, loader := range caddyfileLoaders { + str += " " + loader.name + "\n" + } + if defaultCaddyfileLoader.name != "" { + str += " " + defaultCaddyfileLoader.name + "\n" + } + + // Let's alphabetize the rest of these... + var others []string + for stype, stypePlugins := range plugins { + for name := range stypePlugins { + var s string + if stype != "" { + s = stype + "." + } + s += name + others = append(others, s) + } + } + sort.Strings(others) + str += "\nOther plugins:\n" + for _, name := range others { + str += " " + name + "\n" + } + + return str +} + +// ValidDirectives returns the list of all directives that are +// recognized for the server type serverType. However, not all +// directives may be installed. This makes it possible to give +// more helpful error messages, like "did you mean ..." or +// "maybe you need to plug in ...". +func ValidDirectives(serverType string) []string { + stype, err := getServerType(serverType) + if err != nil { + return nil + } + return stype.Directives +} + +// serverListener pairs a server to its listener. +type serverListener struct { + server Server + listener net.Listener +} + +// Context is a type that carries a server type through +// the load and setup phase; it maintains the state +// between loading the Caddyfile, then executing its +// directives, then making the servers for Caddy to +// manage. Typically, such state involves configuration +// structs, etc. +type Context interface { + InspectServerBlocks(string, []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) + MakeServers() ([]Server, error) +} + +// RegisterServerType registers a server type srv by its +// name, typeName. +func RegisterServerType(typeName string, srv ServerType) { + if _, ok := serverTypes[typeName]; ok { + panic("server type already registered") + } + serverTypes[typeName] = srv +} + +// ServerType contains information about a server type. +type ServerType struct { + // List of directives, in execution order, that are + // valid for this server type. Directives should be + // one word if possible and lower-cased. + Directives []string + + // InspectServerBlocks is an optional callback that is + // executed after loading the tokens for each server + // block but before executing the directives in them. + // This func may modify the server blocks and return + // new ones to be used. + InspectServerBlocks func(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) + + // MakeServers is a callback that makes the server + // instances. + MakeServers func() ([]Server, error) + + // DefaultInput returns a default config input if none + // is otherwise loaded. + DefaultInput func() Input + + NewContext func() Context +} + +// Plugin is a type which holds information about a plugin. +type Plugin struct { + // The plugin must have a name: lower case and one word. + // If this plugin has an action, it must be the name of + // the directive to attach to. A name is always required. + Name string + + // ServerType is the type of server this plugin is for. + // Can be empty if not applicable, or if the plugin + // can associate with any server type. + ServerType string + + // Action is the plugin's setup function, if associated + // with a directive in the Caddyfile. + Action SetupFunc +} + +// RegisterPlugin plugs in plugin. All plugins should register +// themselves, even if they do not perform an action associated +// with a directive. It is important for the process to know +// which plugins are available. +func RegisterPlugin(plugin Plugin) { + if plugin.Name == "" { + panic("plugin must have a name") + } + if _, ok := plugins[plugin.ServerType]; !ok { + plugins[plugin.ServerType] = make(map[string]Plugin) + } + if _, dup := plugins[plugin.ServerType][plugin.Name]; dup { + panic("plugin named " + plugin.Name + " already registered for server type " + plugin.ServerType) + } + plugins[plugin.ServerType][plugin.Name] = plugin +} + +// RegisterParsingCallback registers callback to be called after +// executing the directive afterDir for server type serverType. +func RegisterParsingCallback(serverType, afterDir string, callback func() error) { + if _, ok := parsingCallbacks[serverType]; !ok { + parsingCallbacks[serverType] = make(map[string][]func() error) + } + parsingCallbacks[serverType][afterDir] = append(parsingCallbacks[serverType][afterDir], callback) +} + +// SetupFunc is used to set up a plugin, or in other words, +// execute a directive. It will be called once per key for +// each server block it appears in. +type SetupFunc func(c *Controller) error + +// DirectiveAction gets the action for directive dir of +// server type serverType. +func DirectiveAction(serverType, dir string) (SetupFunc, error) { + if stypePlugins, ok := plugins[serverType]; ok { + if plugin, ok := stypePlugins[dir]; ok { + return plugin.Action, nil + } + } + if genericPlugins, ok := plugins[""]; ok { + if plugin, ok := genericPlugins[dir]; ok { + return plugin.Action, nil + } + } + return nil, fmt.Errorf("no action found for directive '%s' with server type '%s' (missing a plugin?)", + dir, serverType) +} + +// Loader is a type that can load a Caddyfile. +// It is passed the name of the server type. +// It returns an error only if something went +// wrong, not simply if there is no Caddyfile +// for this loader to load. +// +// A Loader should only load the Caddyfile if +// a certain condition or requirement is met, +// as returning a non-nil Input value along with +// another Loader will result in an error. +// In other words, loading the Caddyfile must +// be deliberate & deterministic, not haphazard. +// +// The exception is the default Caddyfile loader, +// which will be called only if no other Caddyfile +// loaders return a non-nil Input. The default +// loader may always return an Input value. +type Loader interface { + Load(string) (Input, error) +} + +// LoaderFunc is a convenience type similar to http.HandlerFunc +// that allows you to use a plain function as a Load() method. +type LoaderFunc func(string) (Input, error) + +// Load loads a Caddyfile. +func (lf LoaderFunc) Load(serverType string) (Input, error) { + return lf(serverType) +} + +// RegisterCaddyfileLoader registers loader named name. +func RegisterCaddyfileLoader(name string, loader Loader) { + caddyfileLoaders = append(caddyfileLoaders, caddyfileLoader{name: name, loader: loader}) +} + +// SetDefaultCaddyfileLoader registers loader by name +// as the default Caddyfile loader if no others produce +// a Caddyfile. If another Caddyfile loader has already +// been set as the default, this replaces it. +// +// Do not call RegisterCaddyfileLoader on the same +// loader; that would be redundant. +func SetDefaultCaddyfileLoader(name string, loader Loader) { + defaultCaddyfileLoader = caddyfileLoader{name: name, loader: loader} +} + +// loadCaddyfileInput iterates the registered Caddyfile loaders +// and, if needed, calls the default loader, to load a Caddyfile. +// It is an error if any of the loaders return an error or if +// more than one loader returns a Caddyfile. +func loadCaddyfileInput(serverType string) (Input, error) { + var loadedBy string + var caddyfileToUse Input + for _, l := range caddyfileLoaders { + if cdyfile, err := l.loader.Load(serverType); cdyfile != nil { + if caddyfileToUse != nil { + return nil, fmt.Errorf("Caddyfile loaded multiple times; first by %s, then by %s", loadedBy, l.name) + } + if err != nil { + return nil, err + } + loaderUsed = l + caddyfileToUse = cdyfile + loadedBy = l.name + } + } + if caddyfileToUse == nil && defaultCaddyfileLoader.loader != nil { + cdyfile, err := defaultCaddyfileLoader.loader.Load(serverType) + if err != nil { + return nil, err + } + if cdyfile != nil { + loaderUsed = defaultCaddyfileLoader + caddyfileToUse = cdyfile + } + } + return caddyfileToUse, nil +} + +// caddyfileLoader pairs the name of a loader to the loader. +type caddyfileLoader struct { + name string + loader Loader +} + +var ( + defaultCaddyfileLoader caddyfileLoader // the default loader if all else fail + loaderUsed caddyfileLoader // the loader that was used (relevant for reloads) +) diff --git a/server/config.go b/server/config.go deleted file mode 100644 index e66ec801c..000000000 --- a/server/config.go +++ /dev/null @@ -1,80 +0,0 @@ -package server - -import ( - "crypto/tls" - "net" - - "github.com/mholt/caddy/middleware" -) - -// Config configuration for a single server. -type Config struct { - // The hostname or IP on which to serve - Host string - - // The host address to bind on - defaults to (virtual) Host if empty - BindHost string - - // The port to listen on - Port string - - // The protocol (http/https) to serve with this config; only set if user explicitly specifies it - Scheme string - - // The directory from which to serve files - Root string - - // HTTPS configuration - TLS TLSConfig - - // Middleware stack - Middleware []middleware.Middleware - - // Startup is a list of functions (or methods) to execute at - // server startup and restart; these are executed before any - // parts of the server are configured, and the functions are - // blocking. These are good for setting up middlewares and - // starting goroutines. - Startup []func() error - - // FirstStartup is like Startup but these functions only execute - // during the initial startup, not on subsequent restarts. - // - // (Note: The server does not ever run these on its own; it is up - // to the calling application to do so, and do so only once, as the - // server itself has no notion whether it's a restart or not.) - FirstStartup []func() error - - // Functions (or methods) to execute when the server quits; - // these are executed in response to SIGINT and are blocking - Shutdown []func() error - - // The path to the configuration file from which this was loaded - ConfigFile string - - // The name of the application - AppName string - - // The application's version - AppVersion string -} - -// Address returns the host:port of c as a string. -func (c Config) Address() string { - return net.JoinHostPort(c.Host, c.Port) -} - -// TLSConfig describes how TLS should be configured and used. -type TLSConfig struct { - Enabled bool // will be set to true if TLS is enabled - LetsEncryptEmail string - Manual bool // will be set to true if user provides own certs and keys - Managed bool // will be set to true if config qualifies for implicit automatic/managed HTTPS - OnDemand bool // will be set to true if user enables on-demand TLS (obtain certs during handshakes) - Ciphers []uint16 - ProtocolMinVersion uint16 - ProtocolMaxVersion uint16 - PreferServerCipherSuites bool - ClientCerts []string - ClientAuth tls.ClientAuthType -} diff --git a/server/config_test.go b/server/config_test.go deleted file mode 100644 index 8787e467b..000000000 --- a/server/config_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package server - -import "testing" - -func TestConfigAddress(t *testing.T) { - cfg := Config{Host: "foobar", Port: "1234"} - if actual, expected := cfg.Address(), "foobar:1234"; expected != actual { - t.Errorf("Expected '%s' but got '%s'", expected, actual) - } - - cfg = Config{Host: "", Port: "1234"} - if actual, expected := cfg.Address(), ":1234"; expected != actual { - t.Errorf("Expected '%s' but got '%s'", expected, actual) - } - - cfg = Config{Host: "foobar", Port: ""} - if actual, expected := cfg.Address(), "foobar:"; expected != actual { - t.Errorf("Expected '%s' but got '%s'", expected, actual) - } - - cfg = Config{Host: "::1", Port: "443"} - if actual, expected := cfg.Address(), "[::1]:443"; expected != actual { - t.Errorf("Expected '%s' but got '%s'", expected, actual) - } -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index ea98f5e5f..000000000 --- a/server/server.go +++ /dev/null @@ -1,544 +0,0 @@ -// Package server implements a configurable, general-purpose web server. -// It relies on configurations obtained from the adjacent config package -// and can execute middleware as defined by the adjacent middleware package. -package server - -import ( - "crypto/rand" - "crypto/tls" - "crypto/x509" - "fmt" - "io" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "path" - "runtime" - "strings" - "sync" - "time" -) - -const ( - tlsNewTicketEvery = time.Hour * 10 // generate a new ticket for TLS PFS encryption every so often - tlsNumTickets = 4 // hold and consider that many tickets to decrypt TLS sessions -) - -// Server represents an instance of a server, which serves -// HTTP requests at a particular address (host and port). A -// server is capable of serving numerous virtual hosts on -// the same address and the listener may be stopped for -// graceful termination (POSIX only). -type Server struct { - *http.Server - HTTP2 bool // whether to enable HTTP/2 - tls bool // whether this server is serving all HTTPS hosts or not - OnDemandTLS bool // whether this server supports on-demand TLS (load certs at handshake-time) - tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine - vhosts map[string]virtualHost // virtual hosts keyed by their address - listener ListenerFile // the listener which is bound to the socket - listenerMu sync.Mutex // protects listener - httpWg sync.WaitGroup // used to wait on outstanding connections - startChan chan struct{} // used to block until server is finished starting - connTimeout time.Duration // the maximum duration of a graceful shutdown - ReqCallback OptionalCallback // if non-nil, is executed at the beginning of every request - SNICallback func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) -} - -// ListenerFile represents a listener. -type ListenerFile interface { - net.Listener - File() (*os.File, error) -} - -// OptionalCallback is a function that may or may not handle a request. -// It returns whether or not it handled the request. If it handled the -// request, it is presumed that no further request handling should occur. -type OptionalCallback func(http.ResponseWriter, *http.Request) bool - -// New creates a new Server which will bind to addr and serve -// the sites/hosts configured in configs. Its listener will -// gracefully close when the server is stopped which will take -// no longer than gracefulTimeout. -// -// This function does not start serving. -// -// Do not re-use a server (start, stop, then start again). We -// could probably add more locking to make this possible, but -// as it stands, you should dispose of a server after stopping it. -// The behavior of serving with a spent server is undefined. -func New(addr string, configs []Config, gracefulTimeout time.Duration) (*Server, error) { - var useTLS, useOnDemandTLS bool - if len(configs) > 0 { - useTLS = configs[0].TLS.Enabled - useOnDemandTLS = configs[0].TLS.OnDemand - } - - s := &Server{ - Server: &http.Server{ - Addr: addr, - TLSConfig: new(tls.Config), - // TODO: Make these values configurable? - // ReadTimeout: 2 * time.Minute, - // WriteTimeout: 2 * time.Minute, - // MaxHeaderBytes: 1 << 16, - }, - tls: useTLS, - OnDemandTLS: useOnDemandTLS, - vhosts: make(map[string]virtualHost), - startChan: make(chan struct{}), - connTimeout: gracefulTimeout, - } - s.Handler = s // this is weird, but whatever - - // We have to bound our wg with one increment - // to prevent a "race condition" that is hard-coded - // into sync.WaitGroup.Wait() - basically, an add - // with a positive delta must be guaranteed to - // occur before Wait() is called on the wg. - // In a way, this kind of acts as a safety barrier. - s.httpWg.Add(1) - - // Set up each virtualhost - for _, conf := range configs { - if _, exists := s.vhosts[conf.Host]; exists { - return nil, fmt.Errorf("cannot serve %s - host already defined for address %s", conf.Address(), s.Addr) - } - - vh := virtualHost{config: conf} - - // Build middleware stack - err := vh.buildStack() - if err != nil { - return nil, err - } - - s.vhosts[conf.Host] = vh - } - - return s, nil -} - -// Serve starts the server with an existing listener. It blocks until the -// server stops. -func (s *Server) Serve(ln ListenerFile) error { - err := s.setup() - if err != nil { - defer close(s.startChan) // MUST defer so error is properly reported, same with all cases in this file - return err - } - return s.serve(ln) -} - -// ListenAndServe starts the server with a new listener. It blocks until the server stops. -func (s *Server) ListenAndServe() error { - err := s.setup() - if err != nil { - defer close(s.startChan) - return err - } - - ln, err := net.Listen("tcp", s.Addr) - if err != nil { - var succeeded bool - if runtime.GOOS == "windows" { // TODO: Limit this to Windows only? (it keeps sockets open after closing listeners) - for i := 0; i < 20; i++ { - time.Sleep(100 * time.Millisecond) - ln, err = net.Listen("tcp", s.Addr) - if err == nil { - succeeded = true - break - } - } - } - if !succeeded { - defer close(s.startChan) - return err - } - } - - return s.serve(ln.(*net.TCPListener)) -} - -// serve prepares s to listen on ln by wrapping ln in a -// tcpKeepAliveListener (if ln is a *net.TCPListener) and -// then in a gracefulListener, so that keep-alive is supported -// as well as graceful shutdown/restart. It also configures -// TLS listener on top of that if applicable. -func (s *Server) serve(ln ListenerFile) error { - if tcpLn, ok := ln.(*net.TCPListener); ok { - ln = tcpKeepAliveListener{TCPListener: tcpLn} - } - - s.listenerMu.Lock() - s.listener = newGracefulListener(ln, &s.httpWg) - s.listenerMu.Unlock() - - if s.tls { - var tlsConfigs []TLSConfig - for _, vh := range s.vhosts { - tlsConfigs = append(tlsConfigs, vh.config.TLS) - } - return serveTLS(s, s.listener, tlsConfigs) - } - - close(s.startChan) // unblock anyone waiting for this to start listening - return s.Server.Serve(s.listener) -} - -// setup prepares the server s to begin listening; it should be -// called just before the listener announces itself on the network -// and should only be called when the server is just starting up. -func (s *Server) setup() error { - if !s.HTTP2 { - s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) - } - - // Execute startup functions now - for _, vh := range s.vhosts { - for _, startupFunc := range vh.config.Startup { - err := startupFunc() - if err != nil { - return err - } - } - } - - return nil -} - -// serveTLS serves TLS with SNI and client auth support if s has them enabled. It -// blocks until s quits. -func serveTLS(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error { - // Customize our TLS configuration - s.TLSConfig.MinVersion = tlsConfigs[0].ProtocolMinVersion - s.TLSConfig.MaxVersion = tlsConfigs[0].ProtocolMaxVersion - s.TLSConfig.CipherSuites = tlsConfigs[0].Ciphers - s.TLSConfig.PreferServerCipherSuites = tlsConfigs[0].PreferServerCipherSuites - - // TLS client authentication, if user enabled it - err := setupClientAuth(tlsConfigs, s.TLSConfig) - if err != nil { - defer close(s.startChan) - return err - } - - // Setup any goroutines governing over TLS settings - s.tlsGovChan = make(chan struct{}) - timer := time.NewTicker(tlsNewTicketEvery) - go runTLSTicketKeyRotation(s.TLSConfig, timer, s.tlsGovChan) - - // Create TLS listener - note that we do not replace s.listener - // with this TLS listener; tls.listener is unexported and does - // not implement the File() method we need for graceful restarts - // on POSIX systems. - ln = tls.NewListener(ln, s.TLSConfig) - - close(s.startChan) // unblock anyone waiting for this to start listening - return s.Server.Serve(ln) -} - -// Stop stops the server. It blocks until the server is -// totally stopped. On POSIX systems, it will wait for -// connections to close (up to a max timeout of a few -// seconds); on Windows it will close the listener -// immediately. -func (s *Server) Stop() (err error) { - s.Server.SetKeepAlivesEnabled(false) - - if runtime.GOOS != "windows" { - // force connections to close after timeout - done := make(chan struct{}) - go func() { - s.httpWg.Done() // decrement our initial increment used as a barrier - s.httpWg.Wait() - close(done) - }() - - // Wait for remaining connections to finish or - // force them all to close after timeout - select { - case <-time.After(s.connTimeout): - case <-done: - } - } - - // Close the listener now; this stops the server without delay - s.listenerMu.Lock() - if s.listener != nil { - err = s.listener.Close() - } - s.listenerMu.Unlock() - - // Closing this signals any TLS governor goroutines to exit - if s.tlsGovChan != nil { - close(s.tlsGovChan) - } - - return -} - -// WaitUntilStarted blocks until the server s is started, meaning -// that practically the next instruction is to start the server loop. -// It also unblocks if the server encounters an error during startup. -func (s *Server) WaitUntilStarted() { - <-s.startChan -} - -// ListenerFd gets a dup'ed file of the listener. If there -// is no underlying file, the return value will be nil. It -// is the caller's responsibility to close the file. -func (s *Server) ListenerFd() *os.File { - s.listenerMu.Lock() - defer s.listenerMu.Unlock() - if s.listener != nil { - file, _ := s.listener.File() - return file - } - return nil -} - -// ServeHTTP is the entry point for every request to the address that s -// is bound to. It acts as a multiplexer for the requests hostname as -// defined in the Host header so that the correct virtualhost -// (configuration and middleware stack) will handle the request. -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer func() { - // In case the user doesn't enable error middleware, we still - // need to make sure that we stay alive up here - if rec := recover(); rec != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), - http.StatusInternalServerError) - } - }() - - w.Header().Set("Server", "Caddy") - - host, _, err := net.SplitHostPort(r.Host) - if err != nil { - host = r.Host // oh well - } - - // "The host subcomponent is case-insensitive." (RFC 3986) - host = strings.ToLower(host) - - // Try the host as given, or try falling back to 0.0.0.0 (wildcard) - if _, ok := s.vhosts[host]; !ok { - if _, ok2 := s.vhosts["0.0.0.0"]; ok2 { - host = "0.0.0.0" - } else if _, ok2 := s.vhosts[""]; ok2 { - host = "" - } - } - - // Use URL.RawPath If you need the original, "raw" URL.Path in your middleware. - // Collapse any ./ ../ /// madness here instead of doing that in every plugin. - if r.URL.Path != "/" { - cleanedPath := path.Clean(r.URL.Path) - if cleanedPath == "." { - r.URL.Path = "/" - } else { - if !strings.HasPrefix(cleanedPath, "/") { - cleanedPath = "/" + cleanedPath - } - if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(cleanedPath, "/") { - cleanedPath = cleanedPath + "/" - } - r.URL.Path = cleanedPath - } - } - - // Execute the optional request callback if it exists and it's not disabled - if s.ReqCallback != nil && !s.vhosts[host].config.TLS.Manual && s.ReqCallback(w, r) { - return - } - - if vh, ok := s.vhosts[host]; ok { - status, _ := vh.stack.ServeHTTP(w, r) - - // Fallback error response in case error handling wasn't chained in - if status >= 400 { - DefaultErrorFunc(w, r, status) - } - } else { - // Get the remote host - remoteHost, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - remoteHost = r.RemoteAddr - } - - w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "No such host at %s", s.Server.Addr) - log.Printf("[INFO] %s - No such host at %s (Remote: %s, Referer: %s)", - host, s.Server.Addr, remoteHost, r.Header.Get("Referer")) - } -} - -// DefaultErrorFunc responds to an HTTP request with a simple description -// of the specified HTTP status code. -func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) { - w.WriteHeader(status) - fmt.Fprintf(w, "%d %s", status, http.StatusText(status)) -} - -// setupClientAuth sets up TLS client authentication only if -// any of the TLS configs specified at least one cert file. -func setupClientAuth(tlsConfigs []TLSConfig, config *tls.Config) error { - whatClientAuth := tls.NoClientCert - for _, cfg := range tlsConfigs { - if whatClientAuth < cfg.ClientAuth { // Use the most restrictive. - whatClientAuth = cfg.ClientAuth - } - } - - if whatClientAuth != tls.NoClientCert { - pool := x509.NewCertPool() - for _, cfg := range tlsConfigs { - if len(cfg.ClientCerts) == 0 { - continue - } - for _, caFile := range cfg.ClientCerts { - caCrt, err := ioutil.ReadFile(caFile) // Anyone that gets a cert from this CA can connect - if err != nil { - return err - } - if !pool.AppendCertsFromPEM(caCrt) { - return fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile) - } - } - } - config.ClientCAs = pool - config.ClientAuth = whatClientAuth - } - - return nil -} - -var runTLSTicketKeyRotation = standaloneTLSTicketKeyRotation - -var setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { - return keys -} - -// standaloneTLSTicketKeyRotation governs over the array of TLS ticket keys used to de/crypt TLS tickets. -// It periodically sets a new ticket key as the first one, used to encrypt (and decrypt), -// pushing any old ticket keys to the back, where they are considered for decryption only. -// -// Lack of entropy for the very first ticket key results in the feature being disabled (as does Go), -// later lack of entropy temporarily disables ticket key rotation. -// Old ticket keys are still phased out, though. -// -// Stops the timer when returning. -func standaloneTLSTicketKeyRotation(c *tls.Config, timer *time.Ticker, exitChan chan struct{}) { - defer timer.Stop() - // The entire page should be marked as sticky, but Go cannot do that - // without resorting to syscall#Mlock. And, we don't have madvise (for NODUMP), too. ☹ - keys := make([][32]byte, 1, tlsNumTickets) - - rng := c.Rand - if rng == nil { - rng = rand.Reader - } - if _, err := io.ReadFull(rng, keys[0][:]); err != nil { - c.SessionTicketsDisabled = true // bail if we don't have the entropy for the first one - return - } - c.SessionTicketKey = keys[0] // SetSessionTicketKeys doesn't set a 'tls.keysAlreadSet' - c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys)) - - for { - select { - case _, isOpen := <-exitChan: - if !isOpen { - return - } - case <-timer.C: - rng = c.Rand // could've changed since the start - if rng == nil { - rng = rand.Reader - } - var newTicketKey [32]byte - _, err := io.ReadFull(rng, newTicketKey[:]) - - if len(keys) < tlsNumTickets { - keys = append(keys, keys[0]) // manipulates the internal length - } - for idx := len(keys) - 1; idx >= 1; idx-- { - keys[idx] = keys[idx-1] // yes, this makes copies - } - - if err == nil { - keys[0] = newTicketKey - } - // pushes the last key out, doesn't matter that we don't have a new one - c.SetSessionTicketKeys(setSessionTicketKeysTestHook(keys)) - } - } -} - -// RunFirstStartupFuncs runs all of the server's FirstStartup -// callback functions unless one of them returns an error first. -// It is the caller's responsibility to call this only once and -// at the correct time. The functions here should not be executed -// at restarts or where the user does not explicitly start a new -// instance of the server. -func (s *Server) RunFirstStartupFuncs() error { - for _, vh := range s.vhosts { - for _, f := range vh.config.FirstStartup { - if err := f(); err != nil { - return err - } - } - } - return nil -} - -// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted -// connections. It's used by ListenAndServe and ListenAndServeTLS so -// dead TCP connections (e.g. closing laptop mid-download) eventually -// go away. -// -// Borrowed from the Go standard library. -type tcpKeepAliveListener struct { - *net.TCPListener -} - -// Accept accepts the connection with a keep-alive enabled. -func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { - tc, err := ln.AcceptTCP() - if err != nil { - return - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} - -// File implements ListenerFile; returns the underlying file of the listener. -func (ln tcpKeepAliveListener) File() (*os.File, error) { - return ln.TCPListener.File() -} - -// ShutdownCallbacks executes all the shutdown callbacks -// for all the virtualhosts in servers, and returns all the -// errors generated during their execution. In other words, -// an error executing one shutdown callback does not stop -// execution of others. Only one shutdown callback is executed -// at a time. You must protect the servers that are passed in -// if they are shared across threads. -func ShutdownCallbacks(servers []*Server) []error { - var errs []error - for _, s := range servers { - for _, vhost := range s.vhosts { - for _, shutdownFunc := range vhost.config.Shutdown { - err := shutdownFunc() - if err != nil { - errs = append(errs, err) - } - } - } - } - return errs -} diff --git a/server/server_test.go b/server/server_test.go deleted file mode 100644 index 08f1915bd..000000000 --- a/server/server_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package server - -import ( - "crypto/tls" - "testing" - "time" -) - -func TestStandaloneTLSTicketKeyRotation(t *testing.T) { - tlsGovChan := make(chan struct{}) - defer close(tlsGovChan) - callSync := make(chan bool, 1) - defer close(callSync) - - oldHook := setSessionTicketKeysTestHook - defer func() { - setSessionTicketKeysTestHook = oldHook - }() - var keysInUse [][32]byte - setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { - keysInUse = keys - callSync <- true - return keys - } - - c := new(tls.Config) - timer := time.NewTicker(time.Millisecond * 1) - - go standaloneTLSTicketKeyRotation(c, timer, tlsGovChan) - - rounds := 0 - var lastTicketKey [32]byte - for { - select { - case <-callSync: - if lastTicketKey == keysInUse[0] { - close(tlsGovChan) - t.Errorf("The same TLS ticket key has been used again (not rotated): %x.", lastTicketKey) - return - } - lastTicketKey = keysInUse[0] - rounds++ - if rounds <= tlsNumTickets && len(keysInUse) != rounds { - close(tlsGovChan) - t.Errorf("Expected TLS ticket keys in use: %d; Got instead: %d.", rounds, len(keysInUse)) - return - } - if c.SessionTicketsDisabled == true { - t.Error("Session tickets have been disabled unexpectedly.") - return - } - if rounds >= tlsNumTickets+1 { - return - } - case <-time.After(time.Second * 1): - t.Errorf("Timeout after %d rounds.", rounds) - return - } - } -} diff --git a/server/virtualhost.go b/server/virtualhost.go deleted file mode 100644 index 0f44cc68c..000000000 --- a/server/virtualhost.go +++ /dev/null @@ -1,35 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/mholt/caddy/middleware" -) - -// virtualHost represents a virtual host/server. While a Server -// is what actually binds to the address, a user may want to serve -// multiple sites on a single address, and this is what a -// virtualHost allows us to do. -type virtualHost struct { - config Config - fileServer middleware.Handler - stack middleware.Handler -} - -// buildStack builds the server's middleware stack based -// on its config. This method should be called last before -// ListenAndServe begins. -func (vh *virtualHost) buildStack() error { - vh.fileServer = middleware.FileServer(http.Dir(vh.config.Root), []string{vh.config.ConfigFile}) - vh.compile(vh.config.Middleware) - return nil -} - -// compile is an elegant alternative to nesting middleware function -// calls like handler1(handler2(handler3(finalHandler))). -func (vh *virtualHost) compile(layers []middleware.Middleware) { - vh.stack = vh.fileServer // core app layer - for i := len(layers) - 1; i >= 0; i-- { - vh.stack = layers[i](vh.stack) - } -} diff --git a/caddy/sigtrap.go b/sigtrap.go similarity index 69% rename from caddy/sigtrap.go rename to sigtrap.go index 6fd00cac5..7acdbc491 100644 --- a/caddy/sigtrap.go +++ b/sigtrap.go @@ -5,15 +5,13 @@ import ( "os" "os/signal" "sync" - - "github.com/mholt/caddy/server" ) // TrapSignals create signal handlers for all applicable signals for this // system. If your Go program uses signals, this is a rather invasive // function; best to implement them yourself in that case. Signals are not // required for the caddy package to function properly, but this is a -// convenient way to allow the user to control this package of your program. +// convenient way to allow the user to control this part of your program. func TrapSignals() { trapSignalsCrossPlatform() trapSignalsPosix() @@ -54,10 +52,7 @@ func trapSignalsCrossPlatform() { // This function is idempotent; subsequent invocations always return 0. func executeShutdownCallbacks(signame string) (exitCode int) { shutdownCallbacksOnce.Do(func() { - serversMu.Lock() - errs := server.ShutdownCallbacks(servers) - serversMu.Unlock() - + errs := allShutdownCallbacks() if len(errs) > 0 { for _, err := range errs { log.Printf("[ERROR] %s shutdown: %v", signame, err) @@ -68,4 +63,21 @@ func executeShutdownCallbacks(signame string) (exitCode int) { return } +// allShutdownCallbacks executes all the shutdown callbacks +// for all the instances, and returns all the errors generated +// during their execution. An error executing one shutdown +// callback does not stop execution of others. Only one shutdown +// callback is executed at a time. +func allShutdownCallbacks() []error { + var errs []error + instancesMu.Lock() + for _, inst := range instances { + errs = append(errs, inst.shutdownCallbacks()...) + } + instancesMu.Unlock() + return errs +} + +// shutdownCallbacksOnce ensures that shutdown callbacks +// for all instances are only executed once. var shutdownCallbacksOnce sync.Once diff --git a/caddy/sigtrap_posix.go b/sigtrap_posix.go similarity index 62% rename from caddy/sigtrap_posix.go rename to sigtrap_posix.go index ac3000d76..9ee7bbba3 100644 --- a/caddy/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -3,7 +3,6 @@ package caddy import ( - "io/ioutil" "log" "os" "os/signal" @@ -48,28 +47,34 @@ func trapSignalsPosix() { case syscall.SIGUSR1: log.Println("[INFO] SIGUSR1: Reloading") - var updatedCaddyfile Input - - caddyfileMu.Lock() - if caddyfile == nil { + // Start with the existing Caddyfile + instancesMu.Lock() + inst := instances[0] // we only support one instance at this time + instancesMu.Unlock() + updatedCaddyfile := inst.caddyfileInput + if updatedCaddyfile == nil { // Hmm, did spawing process forget to close stdin? Anyhow, this is unusual. log.Println("[ERROR] SIGUSR1: no Caddyfile to reload (was stdin left open?)") - caddyfileMu.Unlock() continue } - if caddyfile.IsFile() { - body, err := ioutil.ReadFile(caddyfile.Path()) - if err == nil { - updatedCaddyfile = CaddyfileInput{ - Filepath: caddyfile.Path(), - Contents: body, - RealFile: true, - } - } + if loaderUsed.loader == nil { + // This also should never happen + log.Println("[ERROR] SIGUSR1: no Caddyfile loader with which to reload Caddyfile") + continue } - caddyfileMu.Unlock() - err := Restart(updatedCaddyfile) + // Load the updated Caddyfile + newCaddyfile, err := loaderUsed.loader.Load(inst.serverType) + if err != nil { + log.Printf("[ERROR] SIGUSR1: loading updated Caddyfile: %v", err) + continue + } + if newCaddyfile != nil { + updatedCaddyfile = newCaddyfile + } + + // Kick off the restart; our work is done + inst, err = inst.Restart(updatedCaddyfile) if err != nil { log.Printf("[ERROR] SIGUSR1: %v", err) } diff --git a/caddy/sigtrap_windows.go b/sigtrap_windows.go similarity index 100% rename from caddy/sigtrap_windows.go rename to sigtrap_windows.go diff --git a/caddy/setup/startupshutdown.go b/startupshutdown/startupshutdown.go similarity index 51% rename from caddy/setup/startupshutdown.go rename to startupshutdown/startupshutdown.go index 7a21ef47a..911e1b015 100644 --- a/caddy/setup/startupshutdown.go +++ b/startupshutdown/startupshutdown.go @@ -1,27 +1,38 @@ -package setup +package startupshutdown import ( "os" "os/exec" "strings" - "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy" ) -// Startup registers a startup callback to execute during server start. -func Startup(c *Controller) (middleware.Middleware, error) { - return nil, registerCallback(c, &c.FirstStartup) +func init() { + caddy.RegisterPlugin(caddy.Plugin{ + Name: "startup", + Action: Startup, + }) + caddy.RegisterPlugin(caddy.Plugin{ + Name: "shutdown", + Action: Shutdown, + }) } -// Shutdown registers a shutdown callback to execute during process exit. -func Shutdown(c *Controller) (middleware.Middleware, error) { - return nil, registerCallback(c, &c.Shutdown) +// Startup registers a startup callback to execute during server start. +func Startup(c *caddy.Controller) error { + return registerCallback(c, c.OnStartup) +} + +// Shutdown registers a shutdown callback to execute during server stop. +func Shutdown(c *caddy.Controller) error { + return registerCallback(c, c.OnShutdown) } // registerCallback registers a callback function to execute by -// using c to parse the line. It appends the callback function -// to the list of callback functions passed in by reference. -func registerCallback(c *Controller, list *[]func() error) error { +// using c to parse the directive. It registers the callback +// to be executed using registerFunc. +func registerCallback(c *caddy.Controller, registerFunc func(func() error)) error { var funcs []func() error for c.Next() { @@ -37,7 +48,7 @@ func registerCallback(c *Controller, list *[]func() error) error { args = args[:len(args)-1] } - command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " ")) + command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " ")) if err != nil { return c.Err(err.Error()) } @@ -47,7 +58,6 @@ func registerCallback(c *Controller, list *[]func() error) error { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if nonblock { return cmd.Start() } @@ -58,7 +68,9 @@ func registerCallback(c *Controller, list *[]func() error) error { } return c.OncePerServerBlock(func() error { - *list = append(*list, funcs...) + for _, fn := range funcs { + registerFunc(fn) + } return nil }) } diff --git a/caddy/setup/startupshutdown_test.go b/startupshutdown/startupshutdown_test.go similarity index 78% rename from caddy/setup/startupshutdown_test.go rename to startupshutdown/startupshutdown_test.go index 871a64214..8bc98f9ab 100644 --- a/caddy/setup/startupshutdown_test.go +++ b/startupshutdown/startupshutdown_test.go @@ -1,4 +1,4 @@ -package setup +package startupshutdown import ( "os" @@ -6,16 +6,15 @@ import ( "strconv" "testing" "time" + + "github.com/mholt/caddy" ) // The Startup function's tests are symmetrical to Shutdown tests, // because the Startup and Shutdown functions share virtually the // same functionality func TestStartup(t *testing.T) { - tempDirPath, err := getTempDirPath() - if err != nil { - t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) - } + tempDirPath := os.TempDir() testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown") defer func() { @@ -26,6 +25,11 @@ func TestStartup(t *testing.T) { osSenitiveTestDir := filepath.FromSlash(testDir) os.RemoveAll(osSenitiveTestDir) // start with a clean slate + var registeredFunction func() error + fakeRegister := func(fn func() error) { + registeredFunction = fn + } + tests := []struct { input string shouldExecutionErr bool @@ -42,12 +46,15 @@ func TestStartup(t *testing.T) { } for i, test := range tests { - c := NewTestController(test.input) - _, err = Startup(c) + c := caddy.NewTestController(test.input) + err := registerCallback(c, fakeRegister) if err != nil { t.Errorf("Expected no errors, got: %v", err) } - err = c.FirstStartup[0]() + if registeredFunction == nil { + t.Fatalf("Expected function to be registered, but it wasn't") + } + err = registeredFunction() if err != nil && !test.shouldExecutionErr { t.Errorf("Test %d recieved an error of:\n%v", i, err) }