mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-30 18:22:49 -04:00 
			
		
		
		
	Merge pull request #866 from mholt/0.9-wip
Merge 0.9 into master (warning: huge diff)
This commit is contained in:
		
						commit
						9b4134b287
					
				| @ -1,4 +1,4 @@ | |||||||
| (Are you asking for help with Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!) | (Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!) | ||||||
| 
 | 
 | ||||||
| #### 1. What version of Caddy are you running (`caddy -version`)? | #### 1. What version of Caddy are you running (`caddy -version`)? | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| package assets | package caddy | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"os" | 	"os" | ||||||
| @ -6,10 +6,15 @@ import ( | |||||||
| 	"runtime" | 	"runtime" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Path returns the path to the folder | // AssetsPath returns the path to the folder | ||||||
| // where the application may store data. This | // where the application may store data. If | ||||||
| // currently resolves to ~/.caddy | // CADDYPATH env variable is set, that value | ||||||
| func Path() string { | // 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") | 	return filepath.Join(userHomeDir(), ".caddy") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
							
								
								
									
										19
									
								
								assets_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								assets_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | package caddy | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestAssetsPath(t *testing.T) { | ||||||
|  | 	if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") { | ||||||
|  | 		t.Errorf("Expected path to be a .caddy folder, got: %v", actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	os.Setenv("CADDYPATH", "testpath") | ||||||
|  | 	if actual, expected := AssetsPath(), "testpath"; actual != expected { | ||||||
|  | 		t.Errorf("Expected path to be %v, got: %v", expected, actual) | ||||||
|  | 	} | ||||||
|  | 	os.Setenv("CADDYPATH", "") | ||||||
|  | } | ||||||
							
								
								
									
										745
									
								
								caddy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										745
									
								
								caddy.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,745 @@ | |||||||
|  | package caddy | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"path" | ||||||
|  | 	"runtime" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mholt/caddy/caddyfile" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Configurable application parameters | ||||||
|  | var ( | ||||||
|  | 	// AppName is the name of the application. | ||||||
|  | 	AppName string | ||||||
|  | 
 | ||||||
|  | 	// AppVersion is the version of the application. | ||||||
|  | 	AppVersion string | ||||||
|  | 
 | ||||||
|  | 	// Quiet mode will not show any informative output on initialization. | ||||||
|  | 	Quiet bool | ||||||
|  | 
 | ||||||
|  | 	// PidFile is the path to the pidfile to create. | ||||||
|  | 	PidFile string | ||||||
|  | 
 | ||||||
|  | 	// GracefulTimeout is the maximum duration of a graceful shutdown. | ||||||
|  | 	GracefulTimeout time.Duration | ||||||
|  | 
 | ||||||
|  | 	// isUpgrade will be set to true if this process | ||||||
|  | 	// was started as part of an upgrade, where a parent | ||||||
|  | 	// Caddy process started this one. | ||||||
|  | 	isUpgrade bool | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Instance contains the state of servers created as a result of | ||||||
|  | // calling Start and can be used to access or control those servers. | ||||||
|  | type Instance struct { | ||||||
|  | 	// serverType is the name of the instance's server type | ||||||
|  | 	serverType string | ||||||
|  | 
 | ||||||
|  | 	// caddyfileInput is the input configuration text used for this process | ||||||
|  | 	caddyfileInput Input | ||||||
|  | 
 | ||||||
|  | 	// wg is used to wait for all servers to shut down | ||||||
|  | 	wg sync.WaitGroup | ||||||
|  | 
 | ||||||
|  | 	// servers is the list of servers with their listeners... | ||||||
|  | 	servers []serverListener | ||||||
|  | 
 | ||||||
|  | 	// these are callbacks to execute when certain events happen | ||||||
|  | 	onStartup  []func() error | ||||||
|  | 	onRestart  []func() error | ||||||
|  | 	onShutdown []func() error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Stop stops all servers contained in i. It does NOT | ||||||
|  | // execute shutdown callbacks. | ||||||
|  | func (i *Instance) Stop() error { | ||||||
|  | 	// stop the servers | ||||||
|  | 	for _, s := range i.servers { | ||||||
|  | 		if gs, ok := s.server.(GracefulServer); ok { | ||||||
|  | 			if err := gs.Stop(); err != nil { | ||||||
|  | 				log.Printf("[ERROR] Stopping %s: %v", gs.Address(), err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// splice instance list to delete this one | ||||||
|  | 	for j, other := range instances { | ||||||
|  | 		if other == i { | ||||||
|  | 			instances = append(instances[:j], instances[j+1:]...) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // shutdownCallbacks executes all the shutdown callbacks of i. | ||||||
|  | // An error returned from one does not stop execution of the rest. | ||||||
|  | // All the errors will be returned. | ||||||
|  | func (i *Instance) shutdownCallbacks() []error { | ||||||
|  | 	var errs []error | ||||||
|  | 	for _, shutdownFunc := range i.onShutdown { | ||||||
|  | 		err := shutdownFunc() | ||||||
|  | 		if err != nil { | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Restart replaces the servers in i with new servers created from | ||||||
|  | // executing the newCaddyfile. Upon success, it returns the new | ||||||
|  | // instance to replace i. Upon failure, i will not be replaced. | ||||||
|  | func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) { | ||||||
|  | 	log.Println("[INFO] Reloading") | ||||||
|  | 
 | ||||||
|  | 	// run restart callbacks | ||||||
|  | 	for _, fn := range i.onRestart { | ||||||
|  | 		err := fn() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return i, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if newCaddyfile == nil { | ||||||
|  | 		newCaddyfile = i.caddyfileInput | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Add file descriptors of all the sockets that are capable of it | ||||||
|  | 	restartFds := make(map[string]restartPair) | ||||||
|  | 	for _, s := range i.servers { | ||||||
|  | 		gs, srvOk := s.server.(GracefulServer) | ||||||
|  | 		ln, lnOk := s.listener.(Listener) | ||||||
|  | 		if srvOk && lnOk { | ||||||
|  | 			restartFds[gs.Address()] = restartPair{server: gs, listener: ln} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create new instance; if the restart fails, it is simply discarded | ||||||
|  | 	newInst := &Instance{serverType: newCaddyfile.ServerType()} | ||||||
|  | 
 | ||||||
|  | 	// attempt to start new instance | ||||||
|  | 	err := startWithListenerFds(newCaddyfile, newInst, restartFds) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return i, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// success! bump the old instance out so it will be garbage-collected | ||||||
|  | 	instancesMu.Lock() | ||||||
|  | 	for j, other := range instances { | ||||||
|  | 		if other == i { | ||||||
|  | 			instances = append(instances[:j], instances[j+1:]...) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	instancesMu.Unlock() | ||||||
|  | 
 | ||||||
|  | 	log.Println("[INFO] Reloading complete") | ||||||
|  | 
 | ||||||
|  | 	return newInst, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SaveServer adds s and its associated listener ln to the | ||||||
|  | // internally-kept list of servers that is running. For | ||||||
|  | // saved servers, graceful restarts will be provided. | ||||||
|  | func (i *Instance) SaveServer(s Server, ln net.Listener) { | ||||||
|  | 	i.servers = append(i.servers, serverListener{server: s, listener: ln}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HasListenerWithAddress returns whether this package is | ||||||
|  | // tracking a server using a listener with the address | ||||||
|  | // addr. | ||||||
|  | func HasListenerWithAddress(addr string) bool { | ||||||
|  | 	instancesMu.Lock() | ||||||
|  | 	defer instancesMu.Unlock() | ||||||
|  | 	for _, inst := range instances { | ||||||
|  | 		for _, sln := range inst.servers { | ||||||
|  | 			if listenerAddrEqual(sln.listener, addr) { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // listenerAddrEqual compares a listener's address with | ||||||
|  | // addr. Extra care is taken to match addresses with an | ||||||
|  | // empty hostname portion, as listeners tend to report | ||||||
|  | // [::]:80, for example, when the matching address that | ||||||
|  | // created the listener might be simply :80. | ||||||
|  | func listenerAddrEqual(ln net.Listener, addr string) bool { | ||||||
|  | 	lnAddr := ln.Addr().String() | ||||||
|  | 	hostname, port, err := net.SplitHostPort(addr) | ||||||
|  | 	if err != nil || hostname != "" { | ||||||
|  | 		return lnAddr == addr | ||||||
|  | 	} | ||||||
|  | 	if lnAddr == net.JoinHostPort("::", port) { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if lnAddr == net.JoinHostPort("0.0.0.0", port) { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | // TODO: We should be able to support UDP servers... I'm considering this pattern. | ||||||
|  | 
 | ||||||
|  | type UDPListener struct { | ||||||
|  | 	*net.UDPConn | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u UDPListener) Accept() (net.Conn, error) { | ||||||
|  | 	return u.UDPConn, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u UDPListener) Close() error { | ||||||
|  | 	return u.UDPConn.Close() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u UDPListener) Addr() net.Addr { | ||||||
|  | 	return u.UDPConn.LocalAddr() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ net.Listener = UDPListener{} | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | // Server is a type that can listen and serve. A Server | ||||||
|  | // must associate with exactly zero or one listeners. | ||||||
|  | type Server interface { | ||||||
|  | 	// Listen starts listening by creating a new listener | ||||||
|  | 	// and returning it. It does not start accepting | ||||||
|  | 	// connections. | ||||||
|  | 	Listen() (net.Listener, error) | ||||||
|  | 
 | ||||||
|  | 	// Serve starts serving using the provided listener. | ||||||
|  | 	// Serve must start the server loop nearly immediately, | ||||||
|  | 	// or at least not return any errors before the server | ||||||
|  | 	// loop begins. Serve blocks indefinitely, or in other | ||||||
|  | 	// words, until the server is stopped. | ||||||
|  | 	Serve(net.Listener) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Stopper is a type that can stop serving. The stop | ||||||
|  | // does not necessarily have to be graceful. | ||||||
|  | type Stopper interface { | ||||||
|  | 	// Stop stops the server. It blocks until the | ||||||
|  | 	// server is completely stopped. | ||||||
|  | 	Stop() error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GracefulServer is a Server and Stopper, the stopping | ||||||
|  | // of which is graceful (whatever that means for the kind | ||||||
|  | // of server being implemented). It must be able to return | ||||||
|  | // the address it is configured to listen on so that its | ||||||
|  | // listener can be paired with it upon graceful restarts. | ||||||
|  | // The net.Listener that a GracefulServer creates must | ||||||
|  | // implement the Listener interface for restarts to be | ||||||
|  | // graceful (assuming the listener is for TCP). | ||||||
|  | type GracefulServer interface { | ||||||
|  | 	Server | ||||||
|  | 	Stopper | ||||||
|  | 
 | ||||||
|  | 	// Address returns the address the server should | ||||||
|  | 	// listen on; it is used to pair the server to | ||||||
|  | 	// its listener during a graceful/zero-downtime | ||||||
|  | 	// restart. Thus when implementing this method, | ||||||
|  | 	// you must not access a listener to get the | ||||||
|  | 	// address; you must store the address the | ||||||
|  | 	// server is to serve on some other way. | ||||||
|  | 	Address() string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Listener is a net.Listener with an underlying file descriptor. | ||||||
|  | // A server's listener should implement this interface if it is | ||||||
|  | // to support zero-downtime reloads. | ||||||
|  | type Listener interface { | ||||||
|  | 	net.Listener | ||||||
|  | 	File() (*os.File, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // AfterStartup is an interface that can be implemented | ||||||
|  | // by a server type that wants to run some code after all | ||||||
|  | // servers for the same Instance have started. | ||||||
|  | type AfterStartup interface { | ||||||
|  | 	OnStartupComplete() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LoadCaddyfile loads a Caddyfile by calling the plugged in | ||||||
|  | // Caddyfile loader methods. An error is returned if more than | ||||||
|  | // one loader returns a non-nil Caddyfile input. If no loaders | ||||||
|  | // load a Caddyfile, the default loader is used. If no default | ||||||
|  | // loader is registered or it returns nil, the server type's | ||||||
|  | // default Caddyfile is loaded. If the server type does not | ||||||
|  | // specify any default Caddyfile value, then an empty Caddyfile | ||||||
|  | // is returned. Consequently, this function never returns a nil | ||||||
|  | // value as long as there are no errors. | ||||||
|  | func LoadCaddyfile(serverType string) (Input, error) { | ||||||
|  | 	// Ask plugged-in loaders for a Caddyfile | ||||||
|  | 	cdyfile, err := loadCaddyfileInput(serverType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Otherwise revert to default | ||||||
|  | 	if cdyfile == nil { | ||||||
|  | 		cdyfile = DefaultInput(serverType) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Still nil? Geez. | ||||||
|  | 	if cdyfile == nil { | ||||||
|  | 		cdyfile = CaddyfileInput{ServerTypeName: serverType} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return cdyfile, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Wait blocks until all of i's servers have stopped. | ||||||
|  | func (i *Instance) Wait() { | ||||||
|  | 	i.wg.Wait() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CaddyfileFromPipe loads the Caddyfile input from f if f is | ||||||
|  | // not interactive input. f is assumed to be a pipe or stream, | ||||||
|  | // such as os.Stdin. If f is not a pipe, no error is returned | ||||||
|  | // but the Input value will be nil. An error is only returned | ||||||
|  | // if there was an error reading the pipe, even if the length | ||||||
|  | // of what was read is 0. | ||||||
|  | func CaddyfileFromPipe(f *os.File) (Input, error) { | ||||||
|  | 	fi, err := f.Stat() | ||||||
|  | 	if err == nil && fi.Mode()&os.ModeCharDevice == 0 { | ||||||
|  | 		// Note that a non-nil error is not a problem. Windows | ||||||
|  | 		// will not create a stdin if there is no pipe, which | ||||||
|  | 		// produces an error when calling Stat(). But Unix will | ||||||
|  | 		// make one either way, which is why we also check that | ||||||
|  | 		// bitmask. | ||||||
|  | 		// NOTE: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X) | ||||||
|  | 		confBody, err := ioutil.ReadAll(f) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		return CaddyfileInput{ | ||||||
|  | 			Contents: confBody, | ||||||
|  | 			Filepath: f.Name(), | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// not having input from the pipe is not itself an error, | ||||||
|  | 	// just means no input to return. | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Caddyfile returns the Caddyfile used to create i. | ||||||
|  | func (i *Instance) Caddyfile() Input { | ||||||
|  | 	return i.caddyfileInput | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Start starts Caddy with the given Caddyfile. | ||||||
|  | // | ||||||
|  | // This function blocks until all the servers are listening. | ||||||
|  | func Start(cdyfile Input) (*Instance, error) { | ||||||
|  | 	writePidFile() | ||||||
|  | 	inst := &Instance{serverType: cdyfile.ServerType()} | ||||||
|  | 	return inst, startWithListenerFds(cdyfile, inst, nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartPair) error { | ||||||
|  | 	if cdyfile == nil { | ||||||
|  | 		cdyfile = CaddyfileInput{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	stypeName := cdyfile.ServerType() | ||||||
|  | 
 | ||||||
|  | 	stype, err := getServerType(stypeName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	inst.caddyfileInput = cdyfile | ||||||
|  | 
 | ||||||
|  | 	sblocks, err := loadServerBlocks(stypeName, path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx := stype.NewContext() | ||||||
|  | 
 | ||||||
|  | 	sblocks, err = ctx.InspectServerBlocks(cdyfile.Path(), sblocks) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = executeDirectives(inst, cdyfile.Path(), stype.Directives, sblocks) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	slist, err := ctx.MakeServers() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if restartFds == nil { | ||||||
|  | 		// run startup callbacks since this is not a restart | ||||||
|  | 		for _, startupFunc := range inst.onStartup { | ||||||
|  | 			err := startupFunc() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = startServers(slist, inst, restartFds) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	instancesMu.Lock() | ||||||
|  | 	instances = append(instances, inst) | ||||||
|  | 	instancesMu.Unlock() | ||||||
|  | 
 | ||||||
|  | 	// run any AfterStartup callbacks if this is not | ||||||
|  | 	// part of a restart; then show file descriptor notice | ||||||
|  | 	if restartFds == nil { | ||||||
|  | 		for _, srvln := range inst.servers { | ||||||
|  | 			if srv, ok := srvln.server.(AfterStartup); ok { | ||||||
|  | 				srv.OnStartupComplete() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if !Quiet { | ||||||
|  | 			for _, srvln := range inst.servers { | ||||||
|  | 				if !IsLoopback(srvln.listener.Addr().String()) { | ||||||
|  | 					checkFdlimit() | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func executeDirectives(inst *Instance, filename string, | ||||||
|  | 	directives []string, sblocks []caddyfile.ServerBlock) error { | ||||||
|  | 
 | ||||||
|  | 	// map of server block ID to map of directive name to whatever. | ||||||
|  | 	storages := make(map[int]map[string]interface{}) | ||||||
|  | 
 | ||||||
|  | 	// It is crucial that directives are executed in the proper order. | ||||||
|  | 	// We loop with the directives on the outer loop so we execute | ||||||
|  | 	// a directive for all server blocks before going to the next directive. | ||||||
|  | 	// This is important mainly due to the parsing callbacks (below). | ||||||
|  | 	for _, dir := range directives { | ||||||
|  | 		for i, sb := range sblocks { | ||||||
|  | 			var once sync.Once | ||||||
|  | 			if _, ok := storages[i]; !ok { | ||||||
|  | 				storages[i] = make(map[string]interface{}) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			for j, key := range sb.Keys { | ||||||
|  | 				// Execute directive if it is in the server block | ||||||
|  | 				if tokens, ok := sb.Tokens[dir]; ok { | ||||||
|  | 					controller := &Controller{ | ||||||
|  | 						instance:  inst, | ||||||
|  | 						Key:       key, | ||||||
|  | 						Dispenser: caddyfile.NewDispenserTokens(filename, tokens), | ||||||
|  | 						OncePerServerBlock: func(f func() error) error { | ||||||
|  | 							var err error | ||||||
|  | 							once.Do(func() { | ||||||
|  | 								err = f() | ||||||
|  | 							}) | ||||||
|  | 							return err | ||||||
|  | 						}, | ||||||
|  | 						ServerBlockIndex:    i, | ||||||
|  | 						ServerBlockKeyIndex: j, | ||||||
|  | 						ServerBlockKeys:     sb.Keys, | ||||||
|  | 						ServerBlockStorage:  storages[i][dir], | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					setup, err := DirectiveAction(inst.serverType, dir) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return err | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					err = setup(controller) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return err | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					storages[i][dir] = controller.ServerBlockStorage // persist for this server block | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// See if there are any callbacks to execute after this directive | ||||||
|  | 		if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok { | ||||||
|  | 			callbacks := allCallbacks[dir] | ||||||
|  | 			for _, callback := range callbacks { | ||||||
|  | 				if err := callback(); err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func startServers(serverList []Server, inst *Instance, restartFds map[string]restartPair) error { | ||||||
|  | 	errChan := make(chan error, len(serverList)) | ||||||
|  | 
 | ||||||
|  | 	for _, s := range serverList { | ||||||
|  | 		var ln net.Listener | ||||||
|  | 		var err error | ||||||
|  | 
 | ||||||
|  | 		// If this is a reload and s is a GracefulServer, | ||||||
|  | 		// reuse the listener for a graceful restart. | ||||||
|  | 		if gs, ok := s.(GracefulServer); ok && restartFds != nil { | ||||||
|  | 			addr := gs.Address() | ||||||
|  | 			if old, ok := restartFds[addr]; ok { | ||||||
|  | 				file, err := old.listener.File() | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				ln, err = net.FileListener(file) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				file.Close() | ||||||
|  | 				delete(restartFds, addr) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if ln == nil { | ||||||
|  | 			ln, err = s.Listen() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		inst.wg.Add(1) | ||||||
|  | 		go func(s Server, ln net.Listener, inst *Instance) { | ||||||
|  | 			defer inst.wg.Done() | ||||||
|  | 			errChan <- s.Serve(ln) | ||||||
|  | 		}(s, ln, inst) | ||||||
|  | 
 | ||||||
|  | 		inst.servers = append(inst.servers, serverListener{server: s, listener: ln}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Close the remaining (unused) file descriptors to free up resources | ||||||
|  | 	// and stop old servers that aren't used anymore | ||||||
|  | 	for key, old := range restartFds { | ||||||
|  | 		if err := old.server.Stop(); err != nil { | ||||||
|  | 			log.Printf("[ERROR] Stopping %s: %v", old.server.Address(), err) | ||||||
|  | 		} | ||||||
|  | 		delete(restartFds, key) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Log errors that may be returned from Serve() calls, | ||||||
|  | 	// these errors should only be occurring in the server loop. | ||||||
|  | 	go func() { | ||||||
|  | 		for err := range errChan { | ||||||
|  | 			if err == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if strings.Contains(err.Error(), "use of closed network connection") { | ||||||
|  | 				// this error is normal when closing the listener | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			log.Println(err) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getServerType(serverType string) (ServerType, error) { | ||||||
|  | 	stype, ok := serverTypes[serverType] | ||||||
|  | 	if ok { | ||||||
|  | 		return stype, nil | ||||||
|  | 	} | ||||||
|  | 	if len(serverTypes) == 0 { | ||||||
|  | 		return ServerType{}, fmt.Errorf("no server types plugged in") | ||||||
|  | 	} | ||||||
|  | 	if serverType == "" { | ||||||
|  | 		if len(serverTypes) == 1 { | ||||||
|  | 			for _, stype := range serverTypes { | ||||||
|  | 				return stype, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return ServerType{}, fmt.Errorf("multiple server types available; must choose one") | ||||||
|  | 	} | ||||||
|  | 	return ServerType{}, fmt.Errorf("unknown server type '%s'", serverType) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) { | ||||||
|  | 	validDirectives := ValidDirectives(serverType) | ||||||
|  | 	serverBlocks, err := caddyfile.ServerBlocks(filename, input, validDirectives) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if len(serverBlocks) == 0 && serverTypes[serverType].DefaultInput != nil { | ||||||
|  | 		newInput := serverTypes[serverType].DefaultInput() | ||||||
|  | 		serverBlocks, err = caddyfile.ServerBlocks(newInput.Path(), | ||||||
|  | 			bytes.NewReader(newInput.Body()), validDirectives) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return serverBlocks, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Stop stops ALL servers. It blocks until they are all stopped. | ||||||
|  | // It does NOT execute shutdown callbacks, and it deletes all | ||||||
|  | // instances after stopping is completed. Do not re-use any | ||||||
|  | // references to old instances after calling Stop. | ||||||
|  | func Stop() error { | ||||||
|  | 	instancesMu.Lock() | ||||||
|  | 	for _, inst := range instances { | ||||||
|  | 		if err := inst.Stop(); err != nil { | ||||||
|  | 			log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	instances = []*Instance{} | ||||||
|  | 	instancesMu.Unlock() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsLoopback returns true if the hostname of addr looks | ||||||
|  | // explicitly like a common local hostname. addr must only | ||||||
|  | // be a host or a host:port combination. | ||||||
|  | func IsLoopback(addr string) bool { | ||||||
|  | 	host, _, err := net.SplitHostPort(addr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		host = addr // happens if the addr is just a hostname | ||||||
|  | 	} | ||||||
|  | 	return host == "localhost" || | ||||||
|  | 		strings.Trim(host, "[]") == "::1" || | ||||||
|  | 		strings.HasPrefix(host, "127.") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // checkFdlimit issues a warning if the OS limit for | ||||||
|  | // max file descriptors is below a recommended minimum. | ||||||
|  | func checkFdlimit() { | ||||||
|  | 	const min = 8192 | ||||||
|  | 
 | ||||||
|  | 	// Warn if ulimit is too low for production sites | ||||||
|  | 	if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { | ||||||
|  | 		out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH | ||||||
|  | 		if err == nil { | ||||||
|  | 			lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) | ||||||
|  | 			if err == nil && lim < min { | ||||||
|  | 				fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+ | ||||||
|  | 					"At least %d is recommended. Fix with \"ulimit -n %d\".\n", lim, min, min) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Upgrade re-launches the process, preserving the listeners | ||||||
|  | // for a graceful restart. It does NOT load new configuration; | ||||||
|  | // it only starts the process anew with a fresh binary. | ||||||
|  | // | ||||||
|  | // TODO: This is not yet implemented | ||||||
|  | func Upgrade() error { | ||||||
|  | 	return fmt.Errorf("not implemented") | ||||||
|  | 	// TODO: have child process set isUpgrade = true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsUpgrade returns true if this process is part of an upgrade | ||||||
|  | // where a parent caddy process spawned this one to ugprade | ||||||
|  | // the binary. | ||||||
|  | func IsUpgrade() bool { | ||||||
|  | 	return isUpgrade | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CaddyfileInput represents a Caddyfile as input | ||||||
|  | // and is simply a convenient way to implement | ||||||
|  | // the Input interface. | ||||||
|  | type CaddyfileInput struct { | ||||||
|  | 	Filepath       string | ||||||
|  | 	Contents       []byte | ||||||
|  | 	ServerTypeName string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Body returns c.Contents. | ||||||
|  | func (c CaddyfileInput) Body() []byte { return c.Contents } | ||||||
|  | 
 | ||||||
|  | // Path returns c.Filepath. | ||||||
|  | func (c CaddyfileInput) Path() string { return c.Filepath } | ||||||
|  | 
 | ||||||
|  | // ServerType returns c.ServerType. | ||||||
|  | func (c CaddyfileInput) ServerType() string { return c.ServerTypeName } | ||||||
|  | 
 | ||||||
|  | // Input represents a Caddyfile; its contents and file path | ||||||
|  | // (which should include the file name at the end of the path). | ||||||
|  | // If path does not apply (e.g. piped input) you may use | ||||||
|  | // any understandable value. The path is mainly used for logging, | ||||||
|  | // error messages, and debugging. | ||||||
|  | type Input interface { | ||||||
|  | 	// Gets the Caddyfile contents | ||||||
|  | 	Body() []byte | ||||||
|  | 
 | ||||||
|  | 	// Gets the path to the origin file | ||||||
|  | 	Path() string | ||||||
|  | 
 | ||||||
|  | 	// The type of server this input is intended for | ||||||
|  | 	ServerType() string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DefaultInput returns the default Caddyfile input | ||||||
|  | // to use when it is otherwise empty or missing. | ||||||
|  | // It uses the default host and port (depends on | ||||||
|  | // host, e.g. localhost is 2015, otherwise 443) and | ||||||
|  | // root. | ||||||
|  | func DefaultInput(serverType string) Input { | ||||||
|  | 	if _, ok := serverTypes[serverType]; !ok { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if serverTypes[serverType].DefaultInput == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return serverTypes[serverType].DefaultInput() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // writePidFile writes the process ID to the file at PidFile. | ||||||
|  | // It does nothing if PidFile is not set. | ||||||
|  | func writePidFile() error { | ||||||
|  | 	if PidFile == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	pid := []byte(strconv.Itoa(os.Getpid()) + "\n") | ||||||
|  | 	return ioutil.WriteFile(PidFile, pid, 0644) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type restartPair struct { | ||||||
|  | 	server   GracefulServer | ||||||
|  | 	listener Listener | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// instances is the list of running Instances. | ||||||
|  | 	instances []*Instance | ||||||
|  | 
 | ||||||
|  | 	// instancesMu protects instances. | ||||||
|  | 	instancesMu sync.Mutex | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// DefaultConfigFile is the name of the configuration file that is loaded | ||||||
|  | 	// by default if no other file is specified. | ||||||
|  | 	DefaultConfigFile = "Caddyfile" | ||||||
|  | ) | ||||||
| @ -1,12 +0,0 @@ | |||||||
| package assets |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestPath(t *testing.T) { |  | ||||||
| 	if actual := Path(); !strings.HasSuffix(actual, ".caddy") { |  | ||||||
| 		t.Errorf("Expected path to be a .caddy folder, got: %v", actual) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -7,19 +7,18 @@ | |||||||
| #     $ ./build.bash [output_filename] [git_repo] | #     $ ./build.bash [output_filename] [git_repo] | ||||||
| # | # | ||||||
| # Outputs compiled program in current directory. | # Outputs compiled program in current directory. | ||||||
| # Default file name is 'ecaddy'. |  | ||||||
| # Default git repo is current directory. | # Default git repo is current directory. | ||||||
| # Builds always take place from current directory. | # Builds always take place from current directory. | ||||||
| 
 | 
 | ||||||
| set -euo pipefail | set -euo pipefail | ||||||
| 
 | 
 | ||||||
| : ${output_filename:="${1:-}"} | : ${output_filename:="${1:-}"} | ||||||
| : ${output_filename:="ecaddy"} | : ${output_filename:="caddy"} | ||||||
| 
 | 
 | ||||||
| : ${git_repo:="${2:-}"} | : ${git_repo:="${2:-}"} | ||||||
| : ${git_repo:="."} | : ${git_repo:="."} | ||||||
| 
 | 
 | ||||||
| pkg=main | pkg=github.com/mholt/caddy/caddy/caddymain | ||||||
| ldflags=() | ldflags=() | ||||||
| 
 | 
 | ||||||
| # Timestamp of build | # Timestamp of build | ||||||
							
								
								
									
										346
									
								
								caddy/caddy.go
									
									
									
									
									
								
							
							
						
						
									
										346
									
								
								caddy/caddy.go
									
									
									
									
									
								
							| @ -1,346 +0,0 @@ | |||||||
| // Package caddy implements the Caddy web server as a service |  | ||||||
| // in your own Go programs. |  | ||||||
| // |  | ||||||
| // To use this package, follow a few simple steps: |  | ||||||
| // |  | ||||||
| //   1. Set the AppName and AppVersion variables. |  | ||||||
| //   2. Call LoadCaddyfile() to get the Caddyfile. |  | ||||||
| //      You should pass in your own Caddyfile loader. |  | ||||||
| //   3. Call caddy.Start() to start Caddy, caddy.Stop() |  | ||||||
| //      to stop it, or caddy.Restart() to restart it. |  | ||||||
| // |  | ||||||
| // You should use caddy.Wait() to wait for all Caddy servers |  | ||||||
| // to quit before your process exits. |  | ||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"log" |  | ||||||
| 	"net" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 	"strings" |  | ||||||
| 	"sync" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/caddy/https" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Configurable application parameters |  | ||||||
| var ( |  | ||||||
| 	// AppName is the name of the application. |  | ||||||
| 	AppName string |  | ||||||
| 
 |  | ||||||
| 	// AppVersion is the version of the application. |  | ||||||
| 	AppVersion string |  | ||||||
| 
 |  | ||||||
| 	// Quiet when set to true, will not show any informative output on initialization. |  | ||||||
| 	Quiet bool |  | ||||||
| 
 |  | ||||||
| 	// HTTP2 indicates whether HTTP2 is enabled or not. |  | ||||||
| 	HTTP2 bool |  | ||||||
| 
 |  | ||||||
| 	// PidFile is the path to the pidfile to create. |  | ||||||
| 	PidFile string |  | ||||||
| 
 |  | ||||||
| 	// GracefulTimeout is the maximum duration of a graceful shutdown. |  | ||||||
| 	GracefulTimeout time.Duration |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// caddyfile is the input configuration text used for this process |  | ||||||
| 	caddyfile Input |  | ||||||
| 
 |  | ||||||
| 	// caddyfileMu protects caddyfile during changes |  | ||||||
| 	caddyfileMu sync.Mutex |  | ||||||
| 
 |  | ||||||
| 	// servers is a list of all the currently-listening servers |  | ||||||
| 	servers []*server.Server |  | ||||||
| 
 |  | ||||||
| 	// serversMu protects the servers slice during changes |  | ||||||
| 	serversMu sync.Mutex |  | ||||||
| 
 |  | ||||||
| 	// wg is used to wait for all servers to shut down |  | ||||||
| 	wg sync.WaitGroup |  | ||||||
| 
 |  | ||||||
| 	// restartFds keeps the servers' sockets for graceful in-process restart |  | ||||||
| 	restartFds = make(map[string]*os.File) |  | ||||||
| 
 |  | ||||||
| 	// startedBefore should be set to true if caddy has been started |  | ||||||
| 	// at least once (does not indicate whether currently running). |  | ||||||
| 	startedBefore bool |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// DefaultHost is the default host. |  | ||||||
| 	DefaultHost = "" |  | ||||||
| 	// DefaultPort is the default port. |  | ||||||
| 	DefaultPort = "2015" |  | ||||||
| 	// DefaultRoot is the default root folder. |  | ||||||
| 	DefaultRoot = "." |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Start starts Caddy with the given Caddyfile. If cdyfile |  | ||||||
| // is nil, the LoadCaddyfile function will be called to get |  | ||||||
| // one. |  | ||||||
| // |  | ||||||
| // This function blocks until all the servers are listening. |  | ||||||
| func Start(cdyfile Input) (err error) { |  | ||||||
| 	// Input must never be nil; try to load something |  | ||||||
| 	if cdyfile == nil { |  | ||||||
| 		cdyfile, err = LoadCaddyfile(nil) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	caddyfileMu.Lock() |  | ||||||
| 	caddyfile = cdyfile |  | ||||||
| 	caddyfileMu.Unlock() |  | ||||||
| 
 |  | ||||||
| 	// load the server configs (activates Let's Encrypt) |  | ||||||
| 	configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// group virtualhosts by address |  | ||||||
| 	groupings, err := arrangeBindings(configs) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Start each server with its one or more configurations |  | ||||||
| 	err = startServers(groupings) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	showInitializationOutput(groupings) |  | ||||||
| 
 |  | ||||||
| 	startedBefore = true |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // showInitializationOutput just outputs some basic information about |  | ||||||
| // what is being served to stdout, as well as any applicable, non-essential |  | ||||||
| // warnings for the user. |  | ||||||
| func showInitializationOutput(groupings bindingGroup) { |  | ||||||
| 	// Show initialization output |  | ||||||
| 	if !Quiet && !IsRestart() { |  | ||||||
| 		var checkedFdLimit bool |  | ||||||
| 		for _, group := range groupings { |  | ||||||
| 			for _, conf := range group.Configs { |  | ||||||
| 				// Print address of site |  | ||||||
| 				fmt.Println(conf.Address()) |  | ||||||
| 
 |  | ||||||
| 				// Note if non-localhost site resolves to loopback interface |  | ||||||
| 				if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { |  | ||||||
| 					fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", |  | ||||||
| 						conf.Host, group.BindAddr.IP.String()) |  | ||||||
| 				} |  | ||||||
| 				if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { |  | ||||||
| 					checkFdlimit() |  | ||||||
| 					checkedFdLimit = true |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // startServers starts all the servers in groupings, |  | ||||||
| // taking into account whether or not this process is |  | ||||||
| // from a graceful restart or not. It blocks until |  | ||||||
| // the servers are listening. |  | ||||||
| func startServers(groupings bindingGroup) error { |  | ||||||
| 	var startupWg sync.WaitGroup |  | ||||||
| 	errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later |  | ||||||
| 
 |  | ||||||
| 	for _, group := range groupings { |  | ||||||
| 		s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		s.HTTP2 = HTTP2 |  | ||||||
| 		s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running |  | ||||||
| 		if s.OnDemandTLS { |  | ||||||
| 			s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome! |  | ||||||
| 		} else { |  | ||||||
| 			s.TLSConfig.GetCertificate = https.GetCertificate |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		var ln server.ListenerFile |  | ||||||
| 		if len(restartFds) > 0 { |  | ||||||
| 			// Reuse the listeners for in-process restart |  | ||||||
| 			if file, ok := restartFds[s.Addr]; ok { |  | ||||||
| 				fln, err := net.FileListener(file) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				ln, ok = fln.(server.ListenerFile) |  | ||||||
| 				if !ok { |  | ||||||
| 					return errors.New("listener for " + s.Addr + " was not a ListenerFile") |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				file.Close() |  | ||||||
| 				delete(restartFds, s.Addr) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		wg.Add(1) |  | ||||||
| 		go func(s *server.Server, ln server.ListenerFile) { |  | ||||||
| 			defer wg.Done() |  | ||||||
| 
 |  | ||||||
| 			// run startup functions that should only execute when |  | ||||||
| 			// the original parent process is starting. |  | ||||||
| 			if !startedBefore { |  | ||||||
| 				err := s.RunFirstStartupFuncs() |  | ||||||
| 				if err != nil { |  | ||||||
| 					errChan <- err |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			// start the server |  | ||||||
| 			if ln != nil { |  | ||||||
| 				errChan <- s.Serve(ln) |  | ||||||
| 			} else { |  | ||||||
| 				errChan <- s.ListenAndServe() |  | ||||||
| 			} |  | ||||||
| 		}(s, ln) |  | ||||||
| 
 |  | ||||||
| 		startupWg.Add(1) |  | ||||||
| 		go func(s *server.Server) { |  | ||||||
| 			defer startupWg.Done() |  | ||||||
| 			s.WaitUntilStarted() |  | ||||||
| 		}(s) |  | ||||||
| 
 |  | ||||||
| 		serversMu.Lock() |  | ||||||
| 		servers = append(servers, s) |  | ||||||
| 		serversMu.Unlock() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Close the remaining (unused) file descriptors to free up resources |  | ||||||
| 	if len(restartFds) > 0 { |  | ||||||
| 		for key, file := range restartFds { |  | ||||||
| 			file.Close() |  | ||||||
| 			delete(restartFds, key) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Wait for all servers to finish starting |  | ||||||
| 	startupWg.Wait() |  | ||||||
| 
 |  | ||||||
| 	// Return the first error, if any |  | ||||||
| 	select { |  | ||||||
| 	case err := <-errChan: |  | ||||||
| 		// "use of closed network connection" is normal if it was a graceful shutdown |  | ||||||
| 		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	default: |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Stop stops all servers. It blocks until they are all stopped. |  | ||||||
| // It does NOT execute shutdown callbacks that may have been |  | ||||||
| // configured by middleware (they must be executed separately). |  | ||||||
| func Stop() error { |  | ||||||
| 	https.Deactivate() |  | ||||||
| 
 |  | ||||||
| 	serversMu.Lock() |  | ||||||
| 	for _, s := range servers { |  | ||||||
| 		if err := s.Stop(); err != nil { |  | ||||||
| 			log.Printf("[ERROR] Stopping %s: %v", s.Addr, err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	servers = []*server.Server{} // don't reuse servers |  | ||||||
| 	serversMu.Unlock() |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Wait blocks until all servers are stopped. |  | ||||||
| func Wait() { |  | ||||||
| 	wg.Wait() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // LoadCaddyfile loads a Caddyfile by calling the user's loader function, |  | ||||||
| // and if that returns nil, then this function resorts to the default |  | ||||||
| // configuration. Thus, if there are no other errors, this function |  | ||||||
| // always returns at least the default Caddyfile. |  | ||||||
| func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { |  | ||||||
| 	// Try user's loader |  | ||||||
| 	if cdyfile == nil && loader != nil { |  | ||||||
| 		cdyfile, err = loader() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Otherwise revert to default |  | ||||||
| 	if cdyfile == nil { |  | ||||||
| 		cdyfile = DefaultInput() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // CaddyfileFromPipe loads the Caddyfile input from f if f is |  | ||||||
| // not interactive input. f is assumed to be a pipe or stream, |  | ||||||
| // such as os.Stdin. If f is not a pipe, no error is returned |  | ||||||
| // but the Input value will be nil. An error is only returned |  | ||||||
| // if there was an error reading the pipe, even if the length |  | ||||||
| // of what was read is 0. |  | ||||||
| func CaddyfileFromPipe(f *os.File) (Input, error) { |  | ||||||
| 	fi, err := f.Stat() |  | ||||||
| 	if err == nil && fi.Mode()&os.ModeCharDevice == 0 { |  | ||||||
| 		// Note that a non-nil error is not a problem. Windows |  | ||||||
| 		// will not create a stdin if there is no pipe, which |  | ||||||
| 		// produces an error when calling Stat(). But Unix will |  | ||||||
| 		// make one either way, which is why we also check that |  | ||||||
| 		// bitmask. |  | ||||||
| 		// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X) |  | ||||||
| 		confBody, err := ioutil.ReadAll(f) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		return CaddyfileInput{ |  | ||||||
| 			Contents: confBody, |  | ||||||
| 			Filepath: f.Name(), |  | ||||||
| 		}, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// not having input from the pipe is not itself an error, |  | ||||||
| 	// just means no input to return. |  | ||||||
| 	return nil, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Caddyfile returns the current Caddyfile |  | ||||||
| func Caddyfile() Input { |  | ||||||
| 	caddyfileMu.Lock() |  | ||||||
| 	defer caddyfileMu.Unlock() |  | ||||||
| 	return caddyfile |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Input represents a Caddyfile; its contents and file path |  | ||||||
| // (which should include the file name at the end of the path). |  | ||||||
| // If path does not apply (e.g. piped input) you may use |  | ||||||
| // any understandable value. The path is mainly used for logging, |  | ||||||
| // error messages, and debugging. |  | ||||||
| type Input interface { |  | ||||||
| 	// Gets the Caddyfile contents |  | ||||||
| 	Body() []byte |  | ||||||
| 
 |  | ||||||
| 	// Gets the path to the origin file |  | ||||||
| 	Path() string |  | ||||||
| 
 |  | ||||||
| 	// IsFile returns true if the original input was a file on the file system |  | ||||||
| 	// that could be loaded again later if requested. |  | ||||||
| 	IsFile() bool |  | ||||||
| } |  | ||||||
| @ -1,32 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"net/http" |  | ||||||
| 	"testing" |  | ||||||
| 	"time" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestCaddyStartStop(t *testing.T) { |  | ||||||
| 	caddyfile := "localhost:1984" |  | ||||||
| 
 |  | ||||||
| 	for i := 0; i < 2; i++ { |  | ||||||
| 		err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatalf("Error starting, iteration %d: %v", i, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		client := http.Client{ |  | ||||||
| 			Timeout: time.Duration(2 * time.Second), |  | ||||||
| 		} |  | ||||||
| 		resp, err := client.Get("http://localhost:1984") |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) |  | ||||||
| 		} |  | ||||||
| 		resp.Body.Close() |  | ||||||
| 
 |  | ||||||
| 		err = Stop() |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatalf("Error stopping, iteration %d: %v", i, err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package main | package caddymain | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| @ -7,46 +7,55 @@ import ( | |||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/caddy" |  | ||||||
| 	"github.com/mholt/caddy/caddy/https" |  | ||||||
| 	"github.com/xenolf/lego/acme" |  | ||||||
| 	"gopkg.in/natefinch/lumberjack.v2" | 	"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() { | func init() { | ||||||
| 	caddy.TrapSignals() | 	caddy.TrapSignals() | ||||||
| 	setVersion() | 	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.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") | ||||||
| 	flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") | 	// 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(&cpu, "cpu", "100%", "CPU cap") | ||||||
| 	flag.StringVar(&https.DefaultEmail, "email", "", "Default Let's Encrypt account email address") | 	flag.BoolVar(&plugins, "plugins", false, "List installed plugins") | ||||||
| 	flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") | 	flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address") | ||||||
| 	flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host") |  | ||||||
| 	flag.BoolVar(&caddy.HTTP2, "http2", true, "Use HTTP/2") |  | ||||||
| 	flag.StringVar(&logfile, "log", "", "Process log file") | 	flag.StringVar(&logfile, "log", "", "Process log file") | ||||||
| 	flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid 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.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") | ||||||
| 	flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") | 	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(&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() { | // Run is Caddy's main() function. | ||||||
| 	flag.Parse() // called here in main() to allow other packages to set flags in their inits | 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.AppName = appName | ||||||
| 	caddy.AppVersion = appVersion | 	caddy.AppVersion = appVersion | ||||||
| 	acme.UserAgent = appName + "/" + appVersion | 	acme.UserAgent = appName + "/" + appVersion | ||||||
| 
 | 
 | ||||||
| 	// set up process log before anything bad happens | 	// Set up process log before anything bad happens | ||||||
| 	switch logfile { | 	switch logfile { | ||||||
| 	case "stdout": | 	case "stdout": | ||||||
| 		log.SetOutput(os.Stdout) | 		log.SetOutput(os.Stdout) | ||||||
| @ -63,8 +72,9 @@ func main() { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Check for one-time actions | ||||||
| 	if revoke != "" { | 	if revoke != "" { | ||||||
| 		err := https.Revoke(revoke) | 		err := caddytls.Revoke(revoke) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -78,10 +88,8 @@ func main() { | |||||||
| 		} | 		} | ||||||
| 		os.Exit(0) | 		os.Exit(0) | ||||||
| 	} | 	} | ||||||
| 	if directives { | 	if plugins { | ||||||
| 		for _, d := range caddy.Directives() { | 		fmt.Println(caddy.DescribePlugins()) | ||||||
| 			fmt.Println(d) |  | ||||||
| 		} |  | ||||||
| 		os.Exit(0) | 		os.Exit(0) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -92,37 +100,40 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get Caddyfile input | 	// Get Caddyfile input | ||||||
| 	caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile) | 	caddyfile, err := caddy.LoadCaddyfile(serverType) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		mustLogFatal(err) | 		mustLogFatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Start your engines | 	// Start your engines | ||||||
| 	err = caddy.Start(caddyfile) | 	instance, err := caddy.Start(caddyfile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		mustLogFatal(err) | 		mustLogFatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Twiddle your thumbs | 	// 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 | // 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 | // if the user is still there, even if the process log was not | ||||||
| // enabled. If this process is a restart, however, and the user | // enabled. If this process is an upgrade, however, and the user | ||||||
| // might not be there anymore, this just logs to the process log | // might not be there anymore, this just logs to the process | ||||||
| // and exits. | // log and exits. | ||||||
| func mustLogFatal(args ...interface{}) { | func mustLogFatal(args ...interface{}) { | ||||||
| 	if !caddy.IsRestart() { | 	if !caddy.IsUpgrade() { | ||||||
| 		log.SetOutput(os.Stderr) | 		log.SetOutput(os.Stderr) | ||||||
| 	} | 	} | ||||||
| 	log.Fatal(args...) | 	log.Fatal(args...) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func loadCaddyfile() (caddy.Input, error) { | // confLoader loads the Caddyfile using the -conf flag. | ||||||
| 	// Try -conf flag | func confLoader(serverType string) (caddy.Input, error) { | ||||||
| 	if conf != "" { | 	if conf == "" { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if conf == "stdin" { | 	if conf == "stdin" { | ||||||
| 		return caddy.CaddyfileFromPipe(os.Stdin) | 		return caddy.CaddyfileFromPipe(os.Stdin) | ||||||
| 	} | 	} | ||||||
| @ -131,38 +142,82 @@ func loadCaddyfile() (caddy.Input, error) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	return caddy.CaddyfileInput{ | 	return caddy.CaddyfileInput{ | ||||||
| 		Contents:       contents, | 		Contents:       contents, | ||||||
| 		Filepath:       conf, | 		Filepath:       conf, | ||||||
| 			RealFile: true, | 		ServerTypeName: serverType, | ||||||
| 	}, nil | 	}, nil | ||||||
| 	} | } | ||||||
| 
 | 
 | ||||||
| 	// command line args | // defaultLoader loads the Caddyfile from the current working directory. | ||||||
| 	if flag.NArg() > 0 { | func defaultLoader(serverType string) (caddy.Input, error) { | ||||||
| 		confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n") |  | ||||||
| 		return caddy.CaddyfileInput{ |  | ||||||
| 			Contents: []byte(confBody), |  | ||||||
| 			Filepath: "args", |  | ||||||
| 		}, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Caddyfile in cwd |  | ||||||
| 	contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) | 	contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if os.IsNotExist(err) { | 		if os.IsNotExist(err) { | ||||||
| 			return caddy.DefaultInput(), nil | 			return nil, nil | ||||||
| 		} | 		} | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return caddy.CaddyfileInput{ | 	return caddy.CaddyfileInput{ | ||||||
| 		Contents:       contents, | 		Contents:       contents, | ||||||
| 		Filepath:       caddy.DefaultConfigFile, | 		Filepath:       caddy.DefaultConfigFile, | ||||||
| 		RealFile: true, | 		ServerTypeName: serverType, | ||||||
| 	}, nil | 	}, 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 | // setCPU parses string cpu and sets GOMAXPROCS | ||||||
| // according to its value. It accepts either | // according to its value. It accepts either | ||||||
| // a number (e.g. 3) or a percent (e.g. 50%). | // a number (e.g. 3) or a percent (e.g. 50%). | ||||||
| @ -198,33 +253,17 @@ func setCPU(cpu string) error { | |||||||
| 	return nil | 	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" | const appName = "Caddy" | ||||||
| 
 | 
 | ||||||
| // Flags that control program flow or startup | // Flags that control program flow or startup | ||||||
| var ( | var ( | ||||||
|  | 	serverType string | ||||||
| 	conf       string | 	conf       string | ||||||
| 	cpu        string | 	cpu        string | ||||||
| 	logfile    string | 	logfile    string | ||||||
| 	revoke     string | 	revoke     string | ||||||
| 	version    bool | 	version    bool | ||||||
| 	directives bool | 	plugins    bool | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Build information obtained with the help of -ldflags | // Build information obtained with the help of -ldflags | ||||||
							
								
								
									
										348
									
								
								caddy/config.go
									
									
									
									
									
								
							
							
						
						
									
										348
									
								
								caddy/config.go
									
									
									
									
									
								
							| @ -1,348 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log" |  | ||||||
| 	"net" |  | ||||||
| 	"sync" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/caddy/https" |  | ||||||
| 	"github.com/mholt/caddy/caddy/parse" |  | ||||||
| 	"github.com/mholt/caddy/caddy/setup" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// DefaultConfigFile is the name of the configuration file that is loaded |  | ||||||
| 	// by default if no other file is specified. |  | ||||||
| 	DefaultConfigFile = "Caddyfile" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them, |  | ||||||
| // the parsed server blocks, the index of the last directive it processed, and an error (if any). |  | ||||||
| func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) { |  | ||||||
| 	var configs []server.Config |  | ||||||
| 
 |  | ||||||
| 	// Each server block represents similar hosts/addresses, since they |  | ||||||
| 	// were grouped together in the Caddyfile. |  | ||||||
| 	serverBlocks, err := parse.ServerBlocks(filename, input, true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, nil, 0, err |  | ||||||
| 	} |  | ||||||
| 	if len(serverBlocks) == 0 { |  | ||||||
| 		newInput := DefaultInput() |  | ||||||
| 		serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, nil, 0, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off |  | ||||||
| 
 |  | ||||||
| 	// Iterate each server block and make a config for each one, |  | ||||||
| 	// executing the directives that were parsed in order up to the tls |  | ||||||
| 	// directive; this is because we must activate Let's Encrypt. |  | ||||||
| 	for i, sb := range serverBlocks { |  | ||||||
| 		onces := makeOnces() |  | ||||||
| 		storages := makeStorages() |  | ||||||
| 
 |  | ||||||
| 		for j, addr := range sb.Addresses { |  | ||||||
| 			config := server.Config{ |  | ||||||
| 				Host:       addr.Host, |  | ||||||
| 				Port:       addr.Port, |  | ||||||
| 				Scheme:     addr.Scheme, |  | ||||||
| 				Root:       Root, |  | ||||||
| 				ConfigFile: filename, |  | ||||||
| 				AppName:    AppName, |  | ||||||
| 				AppVersion: AppVersion, |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			// It is crucial that directives are executed in the proper order. |  | ||||||
| 			for k, dir := range directiveOrder { |  | ||||||
| 				// Execute directive if it is in the server block |  | ||||||
| 				if tokens, ok := sb.Tokens[dir.name]; ok { |  | ||||||
| 					// Each setup function gets a controller, from which setup functions |  | ||||||
| 					// get access to the config, tokens, and other state information useful |  | ||||||
| 					// to set up its own host only. |  | ||||||
| 					controller := &setup.Controller{ |  | ||||||
| 						Config:    &config, |  | ||||||
| 						Dispenser: parse.NewDispenserTokens(filename, tokens), |  | ||||||
| 						OncePerServerBlock: func(f func() error) error { |  | ||||||
| 							var err error |  | ||||||
| 							onces[dir.name].Do(func() { |  | ||||||
| 								err = f() |  | ||||||
| 							}) |  | ||||||
| 							return err |  | ||||||
| 						}, |  | ||||||
| 						ServerBlockIndex:     i, |  | ||||||
| 						ServerBlockHostIndex: j, |  | ||||||
| 						ServerBlockHosts:     sb.HostList(), |  | ||||||
| 						ServerBlockStorage:   storages[dir.name], |  | ||||||
| 					} |  | ||||||
| 					// execute setup function and append middleware handler, if any |  | ||||||
| 					midware, err := dir.setup(controller) |  | ||||||
| 					if err != nil { |  | ||||||
| 						return nil, nil, lastDirectiveIndex, err |  | ||||||
| 					} |  | ||||||
| 					if midware != nil { |  | ||||||
| 						config.Middleware = append(config.Middleware, midware) |  | ||||||
| 					} |  | ||||||
| 					storages[dir.name] = controller.ServerBlockStorage // persist for this server block |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				// Stop after TLS setup, since we need to activate Let's Encrypt before continuing; |  | ||||||
| 				// it makes some changes to the configs that middlewares might want to know about. |  | ||||||
| 				if dir.name == "tls" { |  | ||||||
| 					lastDirectiveIndex = k |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			configs = append(configs, config) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return configs, serverBlocks, lastDirectiveIndex, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // loadConfigs reads input (named filename) and parses it, returning the |  | ||||||
| // server configurations in the order they appeared in the input. As part |  | ||||||
| // of this, it activates Let's Encrypt for the configs that are produced. |  | ||||||
| // Thus, the returned configs are already optimally configured for HTTPS. |  | ||||||
| func loadConfigs(filename string, input io.Reader) ([]server.Config, error) { |  | ||||||
| 	configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Now we have all the configs, but they have only been set up to the |  | ||||||
| 	// point of tls. We need to activate Let's Encrypt before setting up |  | ||||||
| 	// the rest of the middlewares so they have correct information regarding |  | ||||||
| 	// TLS configuration, if necessary. (this only appends, so our iterations |  | ||||||
| 	// over server blocks below shouldn't be affected) |  | ||||||
| 	if !IsRestart() && !Quiet { |  | ||||||
| 		fmt.Print("Activating privacy features...") |  | ||||||
| 	} |  | ||||||
| 	configs, err = https.Activate(configs) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} else if !IsRestart() && !Quiet { |  | ||||||
| 		fmt.Println(" done.") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Finish setting up the rest of the directives, now that TLS is |  | ||||||
| 	// optimally configured. These loops are similar to above except |  | ||||||
| 	// we don't iterate all the directives from the beginning and we |  | ||||||
| 	// don't create new configs. |  | ||||||
| 	configIndex := -1 |  | ||||||
| 	for i, sb := range serverBlocks { |  | ||||||
| 		onces := makeOnces() |  | ||||||
| 		storages := makeStorages() |  | ||||||
| 
 |  | ||||||
| 		for j := range sb.Addresses { |  | ||||||
| 			configIndex++ |  | ||||||
| 
 |  | ||||||
| 			for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ { |  | ||||||
| 				dir := directiveOrder[k] |  | ||||||
| 
 |  | ||||||
| 				if tokens, ok := sb.Tokens[dir.name]; ok { |  | ||||||
| 					controller := &setup.Controller{ |  | ||||||
| 						Config:    &configs[configIndex], |  | ||||||
| 						Dispenser: parse.NewDispenserTokens(filename, tokens), |  | ||||||
| 						OncePerServerBlock: func(f func() error) error { |  | ||||||
| 							var err error |  | ||||||
| 							onces[dir.name].Do(func() { |  | ||||||
| 								err = f() |  | ||||||
| 							}) |  | ||||||
| 							return err |  | ||||||
| 						}, |  | ||||||
| 						ServerBlockIndex:     i, |  | ||||||
| 						ServerBlockHostIndex: j, |  | ||||||
| 						ServerBlockHosts:     sb.HostList(), |  | ||||||
| 						ServerBlockStorage:   storages[dir.name], |  | ||||||
| 					} |  | ||||||
| 					midware, err := dir.setup(controller) |  | ||||||
| 					if err != nil { |  | ||||||
| 						return nil, err |  | ||||||
| 					} |  | ||||||
| 					if midware != nil { |  | ||||||
| 						configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware) |  | ||||||
| 					} |  | ||||||
| 					storages[dir.name] = controller.ServerBlockStorage // persist for this server block |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return configs, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // makeOnces makes a map of directive name to sync.Once |  | ||||||
| // instance. This is intended to be called once per server |  | ||||||
| // block when setting up configs so that Setup functions |  | ||||||
| // for each directive can perform a task just once per |  | ||||||
| // server block, even if there are multiple hosts on the block. |  | ||||||
| // |  | ||||||
| // We need one Once per directive, otherwise the first |  | ||||||
| // directive to use it would exclude other directives from |  | ||||||
| // using it at all, which would be a bug. |  | ||||||
| func makeOnces() map[string]*sync.Once { |  | ||||||
| 	onces := make(map[string]*sync.Once) |  | ||||||
| 	for _, dir := range directiveOrder { |  | ||||||
| 		onces[dir.name] = new(sync.Once) |  | ||||||
| 	} |  | ||||||
| 	return onces |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // makeStorages makes a map of directive name to interface{} |  | ||||||
| // so that directives' setup functions can persist state |  | ||||||
| // between different hosts on the same server block during the |  | ||||||
| // setup phase. |  | ||||||
| func makeStorages() map[string]interface{} { |  | ||||||
| 	storages := make(map[string]interface{}) |  | ||||||
| 	for _, dir := range directiveOrder { |  | ||||||
| 		storages[dir.name] = nil |  | ||||||
| 	} |  | ||||||
| 	return storages |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // arrangeBindings groups configurations by their bind address. For example, |  | ||||||
| // a server that should listen on localhost and another on 127.0.0.1 will |  | ||||||
| // be grouped into the same address: 127.0.0.1. It will return an error |  | ||||||
| // if an address is malformed or a TLS listener is configured on the |  | ||||||
| // same address as a plaintext HTTP listener. The return value is a map of |  | ||||||
| // bind address to list of configs that would become VirtualHosts on that |  | ||||||
| // server. Use the keys of the returned map to create listeners, and use |  | ||||||
| // the associated values to set up the virtualhosts. |  | ||||||
| func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) { |  | ||||||
| 	var groupings bindingGroup |  | ||||||
| 
 |  | ||||||
| 	// Group configs by bind address |  | ||||||
| 	for _, conf := range allConfigs { |  | ||||||
| 		// use default port if none is specified |  | ||||||
| 		if conf.Port == "" { |  | ||||||
| 			conf.Port = Port |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		bindAddr, warnErr, fatalErr := resolveAddr(conf) |  | ||||||
| 		if fatalErr != nil { |  | ||||||
| 			return groupings, fatalErr |  | ||||||
| 		} |  | ||||||
| 		if warnErr != nil { |  | ||||||
| 			log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Make sure to compare the string representation of the address, |  | ||||||
| 		// not the pointer, since a new *TCPAddr is created each time. |  | ||||||
| 		var existing bool |  | ||||||
| 		for i := 0; i < len(groupings); i++ { |  | ||||||
| 			if groupings[i].BindAddr.String() == bindAddr.String() { |  | ||||||
| 				groupings[i].Configs = append(groupings[i].Configs, conf) |  | ||||||
| 				existing = true |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		if !existing { |  | ||||||
| 			groupings = append(groupings, bindingMapping{ |  | ||||||
| 				BindAddr: bindAddr, |  | ||||||
| 				Configs:  []server.Config{conf}, |  | ||||||
| 			}) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Don't allow HTTP and HTTPS to be served on the same address |  | ||||||
| 	for _, group := range groupings { |  | ||||||
| 		isTLS := group.Configs[0].TLS.Enabled |  | ||||||
| 		for _, config := range group.Configs { |  | ||||||
| 			if config.TLS.Enabled != isTLS { |  | ||||||
| 				thisConfigProto, otherConfigProto := "HTTP", "HTTP" |  | ||||||
| 				if config.TLS.Enabled { |  | ||||||
| 					thisConfigProto = "HTTPS" |  | ||||||
| 				} |  | ||||||
| 				if group.Configs[0].TLS.Enabled { |  | ||||||
| 					otherConfigProto = "HTTPS" |  | ||||||
| 				} |  | ||||||
| 				return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address", |  | ||||||
| 					group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return groupings, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // resolveAddr determines the address (host and port) that a config will |  | ||||||
| // bind to. The returned address, resolvAddr, should be used to bind the |  | ||||||
| // listener or group the config with other configs using the same address. |  | ||||||
| // The first error, if not nil, is just a warning and should be reported |  | ||||||
| // but execution may continue. The second error, if not nil, is a real |  | ||||||
| // problem and the server should not be started. |  | ||||||
| // |  | ||||||
| // This function does not handle edge cases like port "http" or "https" if |  | ||||||
| // they are not known to the system. It does, however, serve on the wildcard |  | ||||||
| // host if resolving the address of the specific hostname fails. |  | ||||||
| func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) { |  | ||||||
| 	resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port)) |  | ||||||
| 	if warnErr != nil { |  | ||||||
| 		// the hostname probably couldn't be resolved, just bind to wildcard then |  | ||||||
| 		resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port)) |  | ||||||
| 		if fatalErr != nil { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // validDirective returns true if d is a valid |  | ||||||
| // directive; false otherwise. |  | ||||||
| func validDirective(d string) bool { |  | ||||||
| 	for _, dir := range directiveOrder { |  | ||||||
| 		if dir.name == d { |  | ||||||
| 			return true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // DefaultInput returns the default Caddyfile input |  | ||||||
| // to use when it is otherwise empty or missing. |  | ||||||
| // It uses the default host and port (depends on |  | ||||||
| // host, e.g. localhost is 2015, otherwise 443) and |  | ||||||
| // root. |  | ||||||
| func DefaultInput() CaddyfileInput { |  | ||||||
| 	port := Port |  | ||||||
| 	if https.HostQualifies(Host) && port == DefaultPort { |  | ||||||
| 		port = "443" |  | ||||||
| 	} |  | ||||||
| 	return CaddyfileInput{ |  | ||||||
| 		Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // These defaults are configurable through the command line |  | ||||||
| var ( |  | ||||||
| 	// Root is the site root |  | ||||||
| 	Root = DefaultRoot |  | ||||||
| 
 |  | ||||||
| 	// Host is the site host |  | ||||||
| 	Host = DefaultHost |  | ||||||
| 
 |  | ||||||
| 	// Port is the site port |  | ||||||
| 	Port = DefaultPort |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // bindingMapping maps a network address to configurations |  | ||||||
| // that will bind to it. The order of the configs is important. |  | ||||||
| type bindingMapping struct { |  | ||||||
| 	BindAddr *net.TCPAddr |  | ||||||
| 	Configs  []server.Config |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // bindingGroup maps network addresses to their configurations. |  | ||||||
| // Preserving the order of the groupings is important |  | ||||||
| // (related to graceful shutdown and restart) |  | ||||||
| // so this is a slice, not a literal map. |  | ||||||
| type bindingGroup []bindingMapping |  | ||||||
| @ -1,159 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"reflect" |  | ||||||
| 	"sync" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestDefaultInput(t *testing.T) { |  | ||||||
| 	if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected { |  | ||||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// next few tests simulate user providing -host and/or -port flags |  | ||||||
| 
 |  | ||||||
| 	Host = "not-localhost.com" |  | ||||||
| 	if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected { |  | ||||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	Host = "[::1]" |  | ||||||
| 	if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected { |  | ||||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	Host = "127.0.1.1" |  | ||||||
| 	if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected { |  | ||||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	Host = "not-localhost.com" |  | ||||||
| 	Port = "1234" |  | ||||||
| 	if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected { |  | ||||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	Host = DefaultHost |  | ||||||
| 	Port = "1234" |  | ||||||
| 	if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected { |  | ||||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestResolveAddr(t *testing.T) { |  | ||||||
| 	// NOTE: If tests fail due to comparing to string "127.0.0.1", |  | ||||||
| 	// it's possible that system env resolves with IPv6, or ::1. |  | ||||||
| 	// If that happens, maybe we should use actualAddr.IP.IsLoopback() |  | ||||||
| 	// for the assertion, rather than a direct string comparison. |  | ||||||
| 
 |  | ||||||
| 	// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""} |  | ||||||
| 	// will not behave the same cross-platform, so they have been omitted. |  | ||||||
| 
 |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		config         server.Config |  | ||||||
| 		shouldWarnErr  bool |  | ||||||
| 		shouldFatalErr bool |  | ||||||
| 		expectedIP     string |  | ||||||
| 		expectedPort   int |  | ||||||
| 	}{ |  | ||||||
| 		{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234}, |  | ||||||
| 		{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80}, |  | ||||||
| 		{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234}, |  | ||||||
| 		{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234}, |  | ||||||
| 		{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234}, |  | ||||||
| 		{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80}, |  | ||||||
| 		{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443}, |  | ||||||
| 		{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234}, |  | ||||||
| 		{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0}, |  | ||||||
| 		{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234}, |  | ||||||
| 		{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234}, |  | ||||||
| 		{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234}, |  | ||||||
| 	} { |  | ||||||
| 		actualAddr, warnErr, fatalErr := resolveAddr(test.config) |  | ||||||
| 
 |  | ||||||
| 		if test.shouldFatalErr && fatalErr == nil { |  | ||||||
| 			t.Errorf("Test %d: Expected error, but there wasn't any", i) |  | ||||||
| 		} |  | ||||||
| 		if !test.shouldFatalErr && fatalErr != nil { |  | ||||||
| 			t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr) |  | ||||||
| 		} |  | ||||||
| 		if fatalErr != nil { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if test.shouldWarnErr && warnErr == nil { |  | ||||||
| 			t.Errorf("Test %d: Expected warning, but there wasn't any", i) |  | ||||||
| 		} |  | ||||||
| 		if !test.shouldWarnErr && warnErr != nil { |  | ||||||
| 			t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected { |  | ||||||
| 			t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected) |  | ||||||
| 		} |  | ||||||
| 		if actual, expected := actualAddr.Port, test.expectedPort; actual != expected { |  | ||||||
| 			t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMakeOnces(t *testing.T) { |  | ||||||
| 	directives := []directive{ |  | ||||||
| 		{"dummy", nil}, |  | ||||||
| 		{"dummy2", nil}, |  | ||||||
| 	} |  | ||||||
| 	directiveOrder = directives |  | ||||||
| 	onces := makeOnces() |  | ||||||
| 	if len(onces) != len(directives) { |  | ||||||
| 		t.Errorf("onces had len %d , expected %d", len(onces), len(directives)) |  | ||||||
| 	} |  | ||||||
| 	expected := map[string]*sync.Once{ |  | ||||||
| 		"dummy":  new(sync.Once), |  | ||||||
| 		"dummy2": new(sync.Once), |  | ||||||
| 	} |  | ||||||
| 	if !reflect.DeepEqual(onces, expected) { |  | ||||||
| 		t.Errorf("onces was %v, expected %v", onces, expected) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMakeStorages(t *testing.T) { |  | ||||||
| 	directives := []directive{ |  | ||||||
| 		{"dummy", nil}, |  | ||||||
| 		{"dummy2", nil}, |  | ||||||
| 	} |  | ||||||
| 	directiveOrder = directives |  | ||||||
| 	storages := makeStorages() |  | ||||||
| 	if len(storages) != len(directives) { |  | ||||||
| 		t.Errorf("storages had len %d , expected %d", len(storages), len(directives)) |  | ||||||
| 	} |  | ||||||
| 	expected := map[string]interface{}{ |  | ||||||
| 		"dummy":  nil, |  | ||||||
| 		"dummy2": nil, |  | ||||||
| 	} |  | ||||||
| 	if !reflect.DeepEqual(storages, expected) { |  | ||||||
| 		t.Errorf("storages was %v, expected %v", storages, expected) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestValidDirective(t *testing.T) { |  | ||||||
| 	directives := []directive{ |  | ||||||
| 		{"dummy", nil}, |  | ||||||
| 		{"dummy2", nil}, |  | ||||||
| 	} |  | ||||||
| 	directiveOrder = directives |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		directive string |  | ||||||
| 		valid     bool |  | ||||||
| 	}{ |  | ||||||
| 		{"dummy", true}, |  | ||||||
| 		{"dummy2", true}, |  | ||||||
| 		{"dummy3", false}, |  | ||||||
| 	} { |  | ||||||
| 		if actual, expected := validDirective(test.directive), test.valid; actual != expected { |  | ||||||
| 			t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,109 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/mholt/caddy/caddy/https" |  | ||||||
| 	"github.com/mholt/caddy/caddy/parse" |  | ||||||
| 	"github.com/mholt/caddy/caddy/setup" |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	// The parse package must know which directives |  | ||||||
| 	// are valid, but it must not import the setup |  | ||||||
| 	// or config package. To solve this problem, we |  | ||||||
| 	// fill up this map in our init function here. |  | ||||||
| 	// The parse package does not need to know the |  | ||||||
| 	// ordering of the directives. |  | ||||||
| 	for _, dir := range directiveOrder { |  | ||||||
| 		parse.ValidDirectives[dir.name] = struct{}{} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Directives are registered in the order they should be |  | ||||||
| // executed. Middleware (directives that inject a handler) |  | ||||||
| // are executed in the order A-B-C-*-C-B-A, assuming |  | ||||||
| // they all call the Next handler in the chain. |  | ||||||
| // |  | ||||||
| // Ordering is VERY important. Every middleware will |  | ||||||
| // feel the effects of all other middleware below |  | ||||||
| // (after) them during a request, but they must not |  | ||||||
| // care what middleware above them are doing. |  | ||||||
| // |  | ||||||
| // For example, log needs to know the status code and |  | ||||||
| // exactly how many bytes were written to the client, |  | ||||||
| // which every other middleware can affect, so it gets |  | ||||||
| // registered first. The errors middleware does not |  | ||||||
| // care if gzip or log modifies its response, so it |  | ||||||
| // gets registered below them. Gzip, on the other hand, |  | ||||||
| // DOES care what errors does to the response since it |  | ||||||
| // must compress every output to the client, even error |  | ||||||
| // pages, so it must be registered before the errors |  | ||||||
| // middleware and any others that would write to the |  | ||||||
| // response. |  | ||||||
| var directiveOrder = []directive{ |  | ||||||
| 	// Essential directives that initialize vital configuration settings |  | ||||||
| 	{"root", setup.Root}, |  | ||||||
| 	{"bind", setup.BindHost}, |  | ||||||
| 	{"tls", https.Setup}, |  | ||||||
| 
 |  | ||||||
| 	// Other directives that don't create HTTP handlers |  | ||||||
| 	{"startup", setup.Startup}, |  | ||||||
| 	{"shutdown", setup.Shutdown}, |  | ||||||
| 
 |  | ||||||
| 	// Directives that inject handlers (middleware) |  | ||||||
| 	{"log", setup.Log}, |  | ||||||
| 	{"gzip", setup.Gzip}, |  | ||||||
| 	{"errors", setup.Errors}, |  | ||||||
| 	{"header", setup.Headers}, |  | ||||||
| 	{"rewrite", setup.Rewrite}, |  | ||||||
| 	{"redir", setup.Redir}, |  | ||||||
| 	{"ext", setup.Ext}, |  | ||||||
| 	{"mime", setup.Mime}, |  | ||||||
| 	{"basicauth", setup.BasicAuth}, |  | ||||||
| 	{"internal", setup.Internal}, |  | ||||||
| 	{"pprof", setup.PProf}, |  | ||||||
| 	{"expvar", setup.ExpVar}, |  | ||||||
| 	{"proxy", setup.Proxy}, |  | ||||||
| 	{"fastcgi", setup.FastCGI}, |  | ||||||
| 	{"websocket", setup.WebSocket}, |  | ||||||
| 	{"markdown", setup.Markdown}, |  | ||||||
| 	{"templates", setup.Templates}, |  | ||||||
| 	{"browse", setup.Browse}, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Directives returns the list of directives in order of priority. |  | ||||||
| func Directives() []string { |  | ||||||
| 	directives := make([]string, len(directiveOrder)) |  | ||||||
| 	for i, d := range directiveOrder { |  | ||||||
| 		directives[i] = d.name |  | ||||||
| 	} |  | ||||||
| 	return directives |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // RegisterDirective adds the given directive to caddy's list of directives. |  | ||||||
| // Pass the name of a directive you want it to be placed after, |  | ||||||
| // otherwise it will be placed at the bottom of the stack. |  | ||||||
| func RegisterDirective(name string, setup SetupFunc, after string) { |  | ||||||
| 	dir := directive{name: name, setup: setup} |  | ||||||
| 	idx := len(directiveOrder) |  | ||||||
| 	for i := range directiveOrder { |  | ||||||
| 		if directiveOrder[i].name == after { |  | ||||||
| 			idx = i + 1 |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...) |  | ||||||
| 	directiveOrder = newDirectives |  | ||||||
| 	parse.ValidDirectives[name] = struct{}{} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // directive ties together a directive name with its setup function. |  | ||||||
| type directive struct { |  | ||||||
| 	name  string |  | ||||||
| 	setup SetupFunc |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SetupFunc takes a controller and may optionally return a middleware. |  | ||||||
| // If the resulting middleware is not nil, it will be chained into |  | ||||||
| // the HTTP handlers in the order specified in this package. |  | ||||||
| type SetupFunc func(c *setup.Controller) (middleware.Middleware, error) |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"reflect" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestRegister(t *testing.T) { |  | ||||||
| 	directives := []directive{ |  | ||||||
| 		{"dummy", nil}, |  | ||||||
| 		{"dummy2", nil}, |  | ||||||
| 	} |  | ||||||
| 	directiveOrder = directives |  | ||||||
| 	RegisterDirective("foo", nil, "dummy") |  | ||||||
| 	if len(directiveOrder) != 3 { |  | ||||||
| 		t.Fatal("Should have 3 directives now") |  | ||||||
| 	} |  | ||||||
| 	getNames := func() (s []string) { |  | ||||||
| 		for _, d := range directiveOrder { |  | ||||||
| 			s = append(s, d.name) |  | ||||||
| 		} |  | ||||||
| 		return s |  | ||||||
| 	} |  | ||||||
| 	if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) { |  | ||||||
| 		t.Fatalf("directive order doesn't match: %s", getNames()) |  | ||||||
| 	} |  | ||||||
| 	RegisterDirective("bar", nil, "ASDASD") |  | ||||||
| 	if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) { |  | ||||||
| 		t.Fatalf("directive order doesn't match: %s", getNames()) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,64 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // isLocalhost returns true if host looks explicitly like a localhost address. |  | ||||||
| func isLocalhost(host string) bool { |  | ||||||
| 	return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. |  | ||||||
| func checkFdlimit() { |  | ||||||
| 	const min = 4096 |  | ||||||
| 
 |  | ||||||
| 	// Warn if ulimit is too low for production sites |  | ||||||
| 	if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { |  | ||||||
| 		out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH |  | ||||||
| 		if err == nil { |  | ||||||
| 			// Note that an error here need not be reported |  | ||||||
| 			lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) |  | ||||||
| 			if err == nil && lim < min { |  | ||||||
| 				fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IsRestart returns whether this process is, according |  | ||||||
| // to env variables, a fork as part of a graceful restart. |  | ||||||
| func IsRestart() bool { |  | ||||||
| 	return startedBefore |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // writePidFile writes the process ID to the file at PidFile, if specified. |  | ||||||
| func writePidFile() error { |  | ||||||
| 	pid := []byte(strconv.Itoa(os.Getpid()) + "\n") |  | ||||||
| 	return ioutil.WriteFile(PidFile, pid, 0644) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // CaddyfileInput represents a Caddyfile as input |  | ||||||
| // and is simply a convenient way to implement |  | ||||||
| // the Input interface. |  | ||||||
| type CaddyfileInput struct { |  | ||||||
| 	Filepath string |  | ||||||
| 	Contents []byte |  | ||||||
| 	RealFile bool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Body returns c.Contents. |  | ||||||
| func (c CaddyfileInput) Body() []byte { return c.Contents } |  | ||||||
| 
 |  | ||||||
| // Path returns c.Filepath. |  | ||||||
| func (c CaddyfileInput) Path() string { return c.Filepath } |  | ||||||
| 
 |  | ||||||
| // IsFile returns true if the original input was a real file on the file system. |  | ||||||
| func (c CaddyfileInput) IsFile() bool { return c.RealFile } |  | ||||||
| @ -1,57 +0,0 @@ | |||||||
| package https |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"crypto" |  | ||||||
| 	"crypto/ecdsa" |  | ||||||
| 	"crypto/rsa" |  | ||||||
| 	"crypto/x509" |  | ||||||
| 	"encoding/pem" |  | ||||||
| 	"errors" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"os" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // loadPrivateKey loads a PEM-encoded ECC/RSA private key from file. |  | ||||||
| func loadPrivateKey(file string) (crypto.PrivateKey, error) { |  | ||||||
| 	keyBytes, err := ioutil.ReadFile(file) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	keyBlock, _ := pem.Decode(keyBytes) |  | ||||||
| 
 |  | ||||||
| 	switch keyBlock.Type { |  | ||||||
| 	case "RSA PRIVATE KEY": |  | ||||||
| 		return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) |  | ||||||
| 	case "EC PRIVATE KEY": |  | ||||||
| 		return x509.ParseECPrivateKey(keyBlock.Bytes) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil, errors.New("unknown private key type") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // savePrivateKey saves a PEM-encoded ECC/RSA private key to file. |  | ||||||
| func savePrivateKey(key crypto.PrivateKey, file string) error { |  | ||||||
| 	var pemType string |  | ||||||
| 	var keyBytes []byte |  | ||||||
| 	switch key := key.(type) { |  | ||||||
| 	case *ecdsa.PrivateKey: |  | ||||||
| 		var err error |  | ||||||
| 		pemType = "EC" |  | ||||||
| 		keyBytes, err = x509.MarshalECPrivateKey(key) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	case *rsa.PrivateKey: |  | ||||||
| 		pemType = "RSA" |  | ||||||
| 		keyBytes = x509.MarshalPKCS1PrivateKey(key) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} |  | ||||||
| 	keyOut, err := os.Create(file) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	keyOut.Chmod(0600) |  | ||||||
| 	defer keyOut.Close() |  | ||||||
| 	return pem.Encode(keyOut, &pemKey) |  | ||||||
| } |  | ||||||
| @ -1,42 +0,0 @@ | |||||||
| package https |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"crypto/tls" |  | ||||||
| 	"log" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httputil" |  | ||||||
| 	"net/url" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const challengeBasePath = "/.well-known/acme-challenge" |  | ||||||
| 
 |  | ||||||
| // RequestCallback proxies challenge requests to ACME client if the |  | ||||||
| // request path starts with challengeBasePath. It returns true if it |  | ||||||
| // handled the request and no more needs to be done; it returns false |  | ||||||
| // if this call was a no-op and the request still needs handling. |  | ||||||
| func RequestCallback(w http.ResponseWriter, r *http.Request) bool { |  | ||||||
| 	if strings.HasPrefix(r.URL.Path, challengeBasePath) { |  | ||||||
| 		scheme := "http" |  | ||||||
| 		if r.TLS != nil { |  | ||||||
| 			scheme = "https" |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort) |  | ||||||
| 		if err != nil { |  | ||||||
| 			w.WriteHeader(http.StatusInternalServerError) |  | ||||||
| 			log.Printf("[ERROR] ACME proxy handler: %v", err) |  | ||||||
| 			return true |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		proxy := httputil.NewSingleHostReverseProxy(upstream) |  | ||||||
| 		proxy.Transport = &http.Transport{ |  | ||||||
| 			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs |  | ||||||
| 		} |  | ||||||
| 		proxy.ServeHTTP(w, r) |  | ||||||
| 
 |  | ||||||
| 		return true |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
| @ -1,411 +0,0 @@ | |||||||
| // Package https facilitates the management of TLS assets and integrates |  | ||||||
| // Let's Encrypt functionality into Caddy with first-class support for |  | ||||||
| // creating and renewing certificates automatically. It is designed to |  | ||||||
| // configure sites for HTTPS by default. |  | ||||||
| package https |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net" |  | ||||||
| 	"net/http" |  | ||||||
| 	"os" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/middleware/redirect" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| 	"github.com/xenolf/lego/acme" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Activate sets up TLS for each server config in configs |  | ||||||
| // as needed; this consists of acquiring and maintaining |  | ||||||
| // certificates and keys for qualifying configs and enabling |  | ||||||
| // OCSP stapling for all TLS-enabled configs. |  | ||||||
| // |  | ||||||
| // This function may prompt the user to provide an email |  | ||||||
| // address if none is available through other means. It |  | ||||||
| // prefers the email address specified in the config, but |  | ||||||
| // if that is not available it will check the command line |  | ||||||
| // argument. If absent, it will use the most recent email |  | ||||||
| // address from last time. If there isn't one, the user |  | ||||||
| // will be prompted and shown SA link. |  | ||||||
| // |  | ||||||
| // Also note that calling this function activates asset |  | ||||||
| // management automatically, which keeps certificates |  | ||||||
| // renewed and OCSP stapling updated. |  | ||||||
| // |  | ||||||
| // Activate returns the updated list of configs, since |  | ||||||
| // some may have been appended, for example, to redirect |  | ||||||
| // plaintext HTTP requests to their HTTPS counterpart. |  | ||||||
| // This function only appends; it does not splice. |  | ||||||
| func Activate(configs []server.Config) ([]server.Config, error) { |  | ||||||
| 	// just in case previous caller forgot... |  | ||||||
| 	Deactivate() |  | ||||||
| 
 |  | ||||||
| 	// pre-screen each config and earmark the ones that qualify for managed TLS |  | ||||||
| 	MarkQualified(configs) |  | ||||||
| 
 |  | ||||||
| 	// place certificates and keys on disk |  | ||||||
| 	err := ObtainCerts(configs, true, false) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return configs, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// update TLS configurations |  | ||||||
| 	err = EnableTLS(configs, true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return configs, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// set up redirects |  | ||||||
| 	configs = MakePlaintextRedirects(configs) |  | ||||||
| 
 |  | ||||||
| 	// renew all relevant certificates that need renewal. this is important |  | ||||||
| 	// to do right away for a couple reasons, mainly because each restart, |  | ||||||
| 	// the renewal ticker is reset, so if restarts happen more often than |  | ||||||
| 	// the ticker interval, renewals would never happen. but doing |  | ||||||
| 	// it right away at start guarantees that renewals aren't missed. |  | ||||||
| 	err = renewManagedCertificates(true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return configs, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// keep certificates renewed and OCSP stapling updated |  | ||||||
| 	go maintainAssets(stopChan) |  | ||||||
| 
 |  | ||||||
| 	return configs, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Deactivate cleans up long-term, in-memory resources |  | ||||||
| // allocated by calling Activate(). Essentially, it stops |  | ||||||
| // the asset maintainer from running, meaning that certificates |  | ||||||
| // will not be renewed, OCSP staples will not be updated, etc. |  | ||||||
| func Deactivate() (err error) { |  | ||||||
| 	defer func() { |  | ||||||
| 		if rec := recover(); rec != nil { |  | ||||||
| 			err = errors.New("already deactivated") |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 	close(stopChan) |  | ||||||
| 	stopChan = make(chan struct{}) |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // MarkQualified scans each config and, if it qualifies for managed |  | ||||||
| // TLS, it sets the Managed field of the TLSConfig to true. |  | ||||||
| func MarkQualified(configs []server.Config) { |  | ||||||
| 	for i := 0; i < len(configs); i++ { |  | ||||||
| 		if ConfigQualifies(configs[i]) { |  | ||||||
| 			configs[i].TLS.Managed = true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ObtainCerts obtains certificates for all these configs as long as a |  | ||||||
| // certificate does not already exist on disk. It does not modify the |  | ||||||
| // configs at all; it only obtains and stores certificates and keys to |  | ||||||
| // the disk. If allowPrompts is true, the user may be shown a prompt. |  | ||||||
| // If proxyACME is true, the ACME challenges will be proxied to our alt port. |  | ||||||
| func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error { |  | ||||||
| 	// We group configs by email so we don't make the same clients over and |  | ||||||
| 	// over. This has the potential to prompt the user for an email, but we |  | ||||||
| 	// prevent that by assuming that if we already have a listener that can |  | ||||||
| 	// proxy ACME challenge requests, then the server is already running and |  | ||||||
| 	// the operator is no longer present. |  | ||||||
| 	groupedConfigs := groupConfigsByEmail(configs, allowPrompts) |  | ||||||
| 
 |  | ||||||
| 	for email, group := range groupedConfigs { |  | ||||||
| 		// Wait as long as we can before creating the client, because it |  | ||||||
| 		// may not be needed, for example, if we already have what we |  | ||||||
| 		// need on disk. Creating a client involves the network and |  | ||||||
| 		// potentially prompting the user, etc., so only do if necessary. |  | ||||||
| 		var client *ACMEClient |  | ||||||
| 
 |  | ||||||
| 		for _, cfg := range group { |  | ||||||
| 			if !HostQualifies(cfg.Host) || existingCertAndKey(cfg.Host) { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			// Now we definitely do need a client |  | ||||||
| 			if client == nil { |  | ||||||
| 				var err error |  | ||||||
| 				client, err = NewACMEClient(email, allowPrompts) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return errors.New("error creating client: " + err.Error()) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			// c.Configure assumes that allowPrompts == !proxyACME, |  | ||||||
| 			// but that's not always true. For example, a restart where |  | ||||||
| 			// the user isn't present and we're not listening on port 80. |  | ||||||
| 			// TODO: This could probably be refactored better. |  | ||||||
| 			if proxyACME { |  | ||||||
| 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort)) |  | ||||||
| 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort)) |  | ||||||
| 				client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) |  | ||||||
| 			} else { |  | ||||||
| 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, "")) |  | ||||||
| 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, "")) |  | ||||||
| 				client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			err := client.Obtain([]string{cfg.Host}) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // groupConfigsByEmail groups configs by the email address to be used by an |  | ||||||
| // ACME client. It only groups configs that have TLS enabled and that are |  | ||||||
| // marked as Managed. If userPresent is true, the operator MAY be prompted |  | ||||||
| // for an email address. |  | ||||||
| func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config { |  | ||||||
| 	initMap := make(map[string][]server.Config) |  | ||||||
| 	for _, cfg := range configs { |  | ||||||
| 		if !cfg.TLS.Managed { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		leEmail := getEmail(cfg, userPresent) |  | ||||||
| 		initMap[leEmail] = append(initMap[leEmail], cfg) |  | ||||||
| 	} |  | ||||||
| 	return initMap |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // EnableTLS configures each config to use TLS according to default settings. |  | ||||||
| // It will only change configs that are marked as managed, and assumes that |  | ||||||
| // certificates and keys are already on disk. If loadCertificates is true, |  | ||||||
| // the certificates will be loaded from disk into the cache for this process |  | ||||||
| // to use. If false, TLS will still be enabled and configured with default |  | ||||||
| // settings, but no certificates will be parsed loaded into the cache, and |  | ||||||
| // the returned error value will always be nil. |  | ||||||
| func EnableTLS(configs []server.Config, loadCertificates bool) error { |  | ||||||
| 	for i := 0; i < len(configs); i++ { |  | ||||||
| 		if !configs[i].TLS.Managed { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		configs[i].TLS.Enabled = true |  | ||||||
| 		if loadCertificates && HostQualifies(configs[i].Host) { |  | ||||||
| 			_, err := cacheManagedCertificate(configs[i].Host, false) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		setDefaultTLSParams(&configs[i]) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // hostHasOtherPort returns true if there is another config in the list with the same |  | ||||||
| // hostname that has port otherPort, or false otherwise. All the configs are checked |  | ||||||
| // against the hostname of allConfigs[thisConfigIdx]. |  | ||||||
| func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool { |  | ||||||
| 	for i, otherCfg := range allConfigs { |  | ||||||
| 		if i == thisConfigIdx { |  | ||||||
| 			continue // has to be a config OTHER than the one we're comparing against |  | ||||||
| 		} |  | ||||||
| 		if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort { |  | ||||||
| 			return true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS |  | ||||||
| // hosts. You must pass in all configs, not just configs that qualify, since |  | ||||||
| // we must know whether the same host already exists on port 80, and those would |  | ||||||
| // not be in a list of configs that qualify for automatic HTTPS. This function will |  | ||||||
| // only set up redirects for configs that qualify. It returns the updated list of |  | ||||||
| // all configs. |  | ||||||
| func MakePlaintextRedirects(allConfigs []server.Config) []server.Config { |  | ||||||
| 	for i, cfg := range allConfigs { |  | ||||||
| 		if cfg.TLS.Managed && |  | ||||||
| 			!hostHasOtherPort(allConfigs, i, "80") && |  | ||||||
| 			(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) { |  | ||||||
| 			allConfigs = append(allConfigs, redirPlaintextHost(cfg)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return allConfigs |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ConfigQualifies returns true if cfg qualifies for |  | ||||||
| // fully managed TLS (but not on-demand TLS, which is |  | ||||||
| // not considered here). It does NOT check to see if a |  | ||||||
| // cert and key already exist for the config. If the |  | ||||||
| // config does qualify, you should set cfg.TLS.Managed |  | ||||||
| // to true and check that instead, because the process of |  | ||||||
| // setting up the config may make it look like it |  | ||||||
| // doesn't qualify even though it originally did. |  | ||||||
| func ConfigQualifies(cfg server.Config) bool { |  | ||||||
| 	return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key |  | ||||||
| 
 |  | ||||||
| 		// user can force-disable automatic HTTPS for this host |  | ||||||
| 		cfg.Scheme != "http" && |  | ||||||
| 		cfg.Port != "80" && |  | ||||||
| 		cfg.TLS.LetsEncryptEmail != "off" && |  | ||||||
| 
 |  | ||||||
| 		// we get can't certs for some kinds of hostnames, but |  | ||||||
| 		// on-demand TLS allows empty hostnames at startup |  | ||||||
| 		(HostQualifies(cfg.Host) || cfg.TLS.OnDemand) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // HostQualifies returns true if the hostname alone |  | ||||||
| // appears eligible for automatic HTTPS. For example, |  | ||||||
| // localhost, empty hostname, and IP addresses are |  | ||||||
| // not eligible because we cannot obtain certificates |  | ||||||
| // for those names. |  | ||||||
| func HostQualifies(hostname string) bool { |  | ||||||
| 	return hostname != "localhost" && // localhost is ineligible |  | ||||||
| 
 |  | ||||||
| 		// hostname must not be empty |  | ||||||
| 		strings.TrimSpace(hostname) != "" && |  | ||||||
| 
 |  | ||||||
| 		// cannot be an IP address, see |  | ||||||
| 		// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt |  | ||||||
| 		// (also trim [] from either end, since that special case can sneak through |  | ||||||
| 		// for IPv6 addresses using the -host flag and with empty/no Caddyfile) |  | ||||||
| 		net.ParseIP(strings.Trim(hostname, "[]")) == nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // existingCertAndKey returns true if the host has a certificate |  | ||||||
| // and private key in storage already, false otherwise. |  | ||||||
| func existingCertAndKey(host string) bool { |  | ||||||
| 	_, err := os.Stat(storage.SiteCertFile(host)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	_, err = os.Stat(storage.SiteKeyFile(host)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // saveCertResource saves the certificate resource to disk. This |  | ||||||
| // includes the certificate file itself, the private key, and the |  | ||||||
| // metadata file. |  | ||||||
| func saveCertResource(cert acme.CertificateResource) error { |  | ||||||
| 	err := os.MkdirAll(storage.Site(cert.Domain), 0700) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Save cert |  | ||||||
| 	err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Save private key |  | ||||||
| 	err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Save cert metadata |  | ||||||
| 	jsonBytes, err := json.MarshalIndent(&cert, "", "\t") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // redirPlaintextHost returns a new plaintext HTTP configuration for |  | ||||||
| // a virtualHost that simply redirects to cfg, which is assumed to |  | ||||||
| // be the HTTPS configuration. The returned configuration is set |  | ||||||
| // to listen on port 80. |  | ||||||
| func redirPlaintextHost(cfg server.Config) server.Config { |  | ||||||
| 	toURL := "https://{host}" // serve any host, since cfg.Host could be empty |  | ||||||
| 	if cfg.Port != "443" && cfg.Port != "80" { |  | ||||||
| 		toURL += ":" + cfg.Port |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	redirMidware := func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		return redirect.Redirect{Next: next, Rules: []redirect.Rule{ |  | ||||||
| 			{ |  | ||||||
| 				FromScheme: "http", |  | ||||||
| 				FromPath:   "/", |  | ||||||
| 				To:         toURL + "{uri}", |  | ||||||
| 				Code:       http.StatusMovedPermanently, |  | ||||||
| 			}, |  | ||||||
| 		}} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return server.Config{ |  | ||||||
| 		Host:       cfg.Host, |  | ||||||
| 		BindHost:   cfg.BindHost, |  | ||||||
| 		Port:       "80", |  | ||||||
| 		Middleware: []middleware.Middleware{redirMidware}, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Revoke revokes the certificate for host via ACME protocol. |  | ||||||
| func Revoke(host string) error { |  | ||||||
| 	if !existingCertAndKey(host) { |  | ||||||
| 		return errors.New("no certificate and key for " + host) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	email := getEmail(server.Config{Host: host}, true) |  | ||||||
| 	if email == "" { |  | ||||||
| 		return errors.New("email is required to revoke") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	client, err := NewACMEClient(email, true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	certFile := storage.SiteCertFile(host) |  | ||||||
| 	certBytes, err := ioutil.ReadFile(certFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = client.RevokeCertificate(certBytes) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = os.Remove(certFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// DefaultEmail represents the Let's Encrypt account email to use if none provided |  | ||||||
| 	DefaultEmail string |  | ||||||
| 
 |  | ||||||
| 	// Agreed indicates whether user has agreed to the Let's Encrypt SA |  | ||||||
| 	Agreed bool |  | ||||||
| 
 |  | ||||||
| 	// CAUrl represents the base URL to the CA's ACME endpoint |  | ||||||
| 	CAUrl string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // AlternatePort is the port on which the acme client will open a |  | ||||||
| // listener and solve the CA's challenges. If this alternate port |  | ||||||
| // is used instead of the default port (80 or 443), then the |  | ||||||
| // default port for the challenge must be forwarded to this one. |  | ||||||
| const AlternatePort = "5033" |  | ||||||
| 
 |  | ||||||
| // KeyType is the type to use for new keys. |  | ||||||
| // This shouldn't need to change except for in tests; |  | ||||||
| // the size can be drastically reduced for speed. |  | ||||||
| var KeyType = acme.RSA2048 |  | ||||||
| 
 |  | ||||||
| // stopChan is used to signal the maintenance goroutine |  | ||||||
| // to terminate. |  | ||||||
| var stopChan chan struct{} |  | ||||||
| @ -1,332 +0,0 @@ | |||||||
| package https |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" |  | ||||||
| 	"os" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/middleware/redirect" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| 	"github.com/xenolf/lego/acme" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestHostQualifies(t *testing.T) { |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		host   string |  | ||||||
| 		expect bool |  | ||||||
| 	}{ |  | ||||||
| 		{"localhost", false}, |  | ||||||
| 		{"127.0.0.1", false}, |  | ||||||
| 		{"127.0.1.5", false}, |  | ||||||
| 		{"::1", false}, |  | ||||||
| 		{"[::1]", false}, |  | ||||||
| 		{"[::]", false}, |  | ||||||
| 		{"::", false}, |  | ||||||
| 		{"", false}, |  | ||||||
| 		{" ", false}, |  | ||||||
| 		{"0.0.0.0", false}, |  | ||||||
| 		{"192.168.1.3", false}, |  | ||||||
| 		{"10.0.2.1", false}, |  | ||||||
| 		{"169.112.53.4", false}, |  | ||||||
| 		{"foobar.com", true}, |  | ||||||
| 		{"sub.foobar.com", true}, |  | ||||||
| 	} { |  | ||||||
| 		if HostQualifies(test.host) && !test.expect { |  | ||||||
| 			t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host) |  | ||||||
| 		} |  | ||||||
| 		if !HostQualifies(test.host) && test.expect { |  | ||||||
| 			t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestConfigQualifies(t *testing.T) { |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		cfg    server.Config |  | ||||||
| 		expect bool |  | ||||||
| 	}{ |  | ||||||
| 		{server.Config{Host: ""}, false}, |  | ||||||
| 		{server.Config{Host: "localhost"}, false}, |  | ||||||
| 		{server.Config{Host: "123.44.3.21"}, false}, |  | ||||||
| 		{server.Config{Host: "example.com"}, true}, |  | ||||||
| 		{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false}, |  | ||||||
| 		{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false}, |  | ||||||
| 		{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true}, |  | ||||||
| 		{server.Config{Host: "example.com", Scheme: "http"}, false}, |  | ||||||
| 		{server.Config{Host: "example.com", Port: "80"}, false}, |  | ||||||
| 		{server.Config{Host: "example.com", Port: "1234"}, true}, |  | ||||||
| 		{server.Config{Host: "example.com", Scheme: "https"}, true}, |  | ||||||
| 		{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false}, |  | ||||||
| 	} { |  | ||||||
| 		if test.expect && !ConfigQualifies(test.cfg) { |  | ||||||
| 			t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg) |  | ||||||
| 		} |  | ||||||
| 		if !test.expect && ConfigQualifies(test.cfg) { |  | ||||||
| 			t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestRedirPlaintextHost(t *testing.T) { |  | ||||||
| 	cfg := redirPlaintextHost(server.Config{ |  | ||||||
| 		Host:     "example.com", |  | ||||||
| 		BindHost: "93.184.216.34", |  | ||||||
| 		Port:     "1234", |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// Check host and port |  | ||||||
| 	if actual, expected := cfg.Host, "example.com"; actual != expected { |  | ||||||
| 		t.Errorf("Expected redir config to have host %s but got %s", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected { |  | ||||||
| 		t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := cfg.Port, "80"; actual != expected { |  | ||||||
| 		t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Make sure redirect handler is set up properly |  | ||||||
| 	if cfg.Middleware == nil || len(cfg.Middleware) != 1 { |  | ||||||
| 		t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	handler, ok := cfg.Middleware[0](nil).(redirect.Redirect) |  | ||||||
| 	if !ok { |  | ||||||
| 		t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler) |  | ||||||
| 	} |  | ||||||
| 	if len(handler.Rules) != 1 { |  | ||||||
| 		t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Check redirect rule for correctness |  | ||||||
| 	if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected { |  | ||||||
| 		t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected { |  | ||||||
| 		t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected { |  | ||||||
| 		t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected { |  | ||||||
| 		t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// browsers can infer a default port from scheme, so make sure the port |  | ||||||
| 	// doesn't get added in explicitly for default ports like 443 for https. |  | ||||||
| 	cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"}) |  | ||||||
| 	handler, _ = cfg.Middleware[0](nil).(redirect.Redirect) |  | ||||||
| 	if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected { |  | ||||||
| 		t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestSaveCertResource(t *testing.T) { |  | ||||||
| 	storage = Storage("./le_test_save") |  | ||||||
| 	defer func() { |  | ||||||
| 		err := os.RemoveAll(string(storage)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err) |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	domain := "example.com" |  | ||||||
| 	certContents := "certificate" |  | ||||||
| 	keyContents := "private key" |  | ||||||
| 	metaContents := `{ |  | ||||||
| 	"domain": "example.com", |  | ||||||
| 	"certUrl": "https://example.com/cert", |  | ||||||
| 	"certStableUrl": "https://example.com/cert/stable" |  | ||||||
| }` |  | ||||||
| 
 |  | ||||||
| 	cert := acme.CertificateResource{ |  | ||||||
| 		Domain:        domain, |  | ||||||
| 		CertURL:       "https://example.com/cert", |  | ||||||
| 		CertStableURL: "https://example.com/cert/stable", |  | ||||||
| 		PrivateKey:    []byte(keyContents), |  | ||||||
| 		Certificate:   []byte(certContents), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := saveCertResource(cert) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("Expected no error, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Errorf("Expected no error reading certificate file, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 	if string(certFile) != certContents { |  | ||||||
| 		t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Errorf("Expected no error reading private key file, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 	if string(keyFile) != keyContents { |  | ||||||
| 		t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Errorf("Expected no error reading meta file, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 	if string(metaFile) != metaContents { |  | ||||||
| 		t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestExistingCertAndKey(t *testing.T) { |  | ||||||
| 	storage = Storage("./le_test_existing") |  | ||||||
| 	defer func() { |  | ||||||
| 		err := os.RemoveAll(string(storage)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err) |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	domain := "example.com" |  | ||||||
| 
 |  | ||||||
| 	if existingCertAndKey(domain) { |  | ||||||
| 		t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err := saveCertResource(acme.CertificateResource{ |  | ||||||
| 		Domain:      domain, |  | ||||||
| 		PrivateKey:  []byte("key"), |  | ||||||
| 		Certificate: []byte("cert"), |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("Expected no error, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if !existingCertAndKey(domain) { |  | ||||||
| 		t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestHostHasOtherPort(t *testing.T) { |  | ||||||
| 	configs := []server.Config{ |  | ||||||
| 		{Host: "example.com", Port: "80"}, |  | ||||||
| 		{Host: "sub1.example.com", Port: "80"}, |  | ||||||
| 		{Host: "sub1.example.com", Port: "443"}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if hostHasOtherPort(configs, 0, "80") { |  | ||||||
| 		t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`) |  | ||||||
| 	} |  | ||||||
| 	if hostHasOtherPort(configs, 0, "443") { |  | ||||||
| 		t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`) |  | ||||||
| 	} |  | ||||||
| 	if !hostHasOtherPort(configs, 1, "443") { |  | ||||||
| 		t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMakePlaintextRedirects(t *testing.T) { |  | ||||||
| 	configs := []server.Config{ |  | ||||||
| 		// Happy path = standard redirect from 80 to 443 |  | ||||||
| 		{Host: "example.com", TLS: server.TLSConfig{Managed: true}}, |  | ||||||
| 
 |  | ||||||
| 		// Host on port 80 already defined; don't change it (no redirect) |  | ||||||
| 		{Host: "sub1.example.com", Port: "80", Scheme: "http"}, |  | ||||||
| 		{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}}, |  | ||||||
| 
 |  | ||||||
| 		// Redirect from port 80 to port 5000 in this case |  | ||||||
| 		{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}}, |  | ||||||
| 
 |  | ||||||
| 		// Can redirect from 80 to either 443 or 5001, but choose 443 |  | ||||||
| 		{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}}, |  | ||||||
| 		{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	result := MakePlaintextRedirects(configs) |  | ||||||
| 	expectedRedirCount := 3 |  | ||||||
| 
 |  | ||||||
| 	if len(result) != len(configs)+expectedRedirCount { |  | ||||||
| 		t.Errorf("Expected %d redirect(s) to be added, but got %d", |  | ||||||
| 			expectedRedirCount, len(result)-len(configs)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestEnableTLS(t *testing.T) { |  | ||||||
| 	configs := []server.Config{ |  | ||||||
| 		{Host: "example.com", TLS: server.TLSConfig{Managed: true}}, |  | ||||||
| 		{}, // not managed - no changes! |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	EnableTLS(configs, false) |  | ||||||
| 
 |  | ||||||
| 	if !configs[0].TLS.Enabled { |  | ||||||
| 		t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false") |  | ||||||
| 	} |  | ||||||
| 	if configs[1].TLS.Enabled { |  | ||||||
| 		t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGroupConfigsByEmail(t *testing.T) { |  | ||||||
| 	if groupConfigsByEmail([]server.Config{}, false) == nil { |  | ||||||
| 		t.Errorf("With empty input, returned map was nil, but expected non-nil map") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	configs := []server.Config{ |  | ||||||
| 		{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}}, |  | ||||||
| 		{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}}, |  | ||||||
| 		{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}}, |  | ||||||
| 		{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}}, |  | ||||||
| 		{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}}, |  | ||||||
| 		{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed |  | ||||||
| 	} |  | ||||||
| 	DefaultEmail = "test@example.com" |  | ||||||
| 
 |  | ||||||
| 	groups := groupConfigsByEmail(configs, true) |  | ||||||
| 
 |  | ||||||
| 	if groups == nil { |  | ||||||
| 		t.Fatalf("Returned map was nil, but expected values") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(groups) != 2 { |  | ||||||
| 		t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups) |  | ||||||
| 	} |  | ||||||
| 	if len(groups["foo@bar"]) != 2 { |  | ||||||
| 		t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"]) |  | ||||||
| 	} |  | ||||||
| 	if len(groups[DefaultEmail]) != 3 { |  | ||||||
| 		t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"]) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMarkQualified(t *testing.T) { |  | ||||||
| 	// TODO: TestConfigQualifies and this test share the same config list... |  | ||||||
| 	configs := []server.Config{ |  | ||||||
| 		{Host: ""}, |  | ||||||
| 		{Host: "localhost"}, |  | ||||||
| 		{Host: "123.44.3.21"}, |  | ||||||
| 		{Host: "example.com"}, |  | ||||||
| 		{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, |  | ||||||
| 		{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, |  | ||||||
| 		{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, |  | ||||||
| 		{Host: "example.com", Scheme: "http"}, |  | ||||||
| 		{Host: "example.com", Port: "80"}, |  | ||||||
| 		{Host: "example.com", Port: "1234"}, |  | ||||||
| 		{Host: "example.com", Scheme: "https"}, |  | ||||||
| 		{Host: "example.com", Port: "80", Scheme: "https"}, |  | ||||||
| 	} |  | ||||||
| 	expectedManagedCount := 4 |  | ||||||
| 
 |  | ||||||
| 	MarkQualified(configs) |  | ||||||
| 
 |  | ||||||
| 	count := 0 |  | ||||||
| 	for _, cfg := range configs { |  | ||||||
| 		if cfg.TLS.Managed { |  | ||||||
| 			count++ |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if count != expectedManagedCount { |  | ||||||
| 		t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,355 +0,0 @@ | |||||||
| package https |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"crypto/tls" |  | ||||||
| 	"encoding/pem" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/caddy/setup" |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| 	"github.com/xenolf/lego/acme" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Setup sets up the TLS configuration and installs certificates that |  | ||||||
| // are specified by the user in the config file. All the automatic HTTPS |  | ||||||
| // stuff comes later outside of this function. |  | ||||||
| func Setup(c *setup.Controller) (middleware.Middleware, error) { |  | ||||||
| 	if c.Port == "80" || c.Scheme == "http" { |  | ||||||
| 		c.TLS.Enabled = false |  | ||||||
| 		log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address()) |  | ||||||
| 		return nil, nil |  | ||||||
| 	} |  | ||||||
| 	c.TLS.Enabled = true |  | ||||||
| 
 |  | ||||||
| 	for c.Next() { |  | ||||||
| 		var certificateFile, keyFile, loadDir, maxCerts string |  | ||||||
| 
 |  | ||||||
| 		args := c.RemainingArgs() |  | ||||||
| 		switch len(args) { |  | ||||||
| 		case 1: |  | ||||||
| 			c.TLS.LetsEncryptEmail = args[0] |  | ||||||
| 
 |  | ||||||
| 			// user can force-disable managed TLS this way |  | ||||||
| 			if c.TLS.LetsEncryptEmail == "off" { |  | ||||||
| 				c.TLS.Enabled = false |  | ||||||
| 				return nil, nil |  | ||||||
| 			} |  | ||||||
| 		case 2: |  | ||||||
| 			certificateFile = args[0] |  | ||||||
| 			keyFile = args[1] |  | ||||||
| 			c.TLS.Manual = true |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Optional block with extra parameters |  | ||||||
| 		var hadBlock bool |  | ||||||
| 		for c.NextBlock() { |  | ||||||
| 			hadBlock = true |  | ||||||
| 			switch c.Val() { |  | ||||||
| 			case "key_type": |  | ||||||
| 				arg := c.RemainingArgs() |  | ||||||
| 				value, ok := supportedKeyTypes[strings.ToUpper(arg[0])] |  | ||||||
| 				if !ok { |  | ||||||
| 					return nil, c.Errf("Wrong KeyType name or KeyType not supported '%s'", c.Val()) |  | ||||||
| 				} |  | ||||||
| 				KeyType = value |  | ||||||
| 			case "protocols": |  | ||||||
| 				args := c.RemainingArgs() |  | ||||||
| 				if len(args) != 2 { |  | ||||||
| 					return nil, c.ArgErr() |  | ||||||
| 				} |  | ||||||
| 				value, ok := supportedProtocols[strings.ToLower(args[0])] |  | ||||||
| 				if !ok { |  | ||||||
| 					return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val()) |  | ||||||
| 				} |  | ||||||
| 				c.TLS.ProtocolMinVersion = value |  | ||||||
| 				value, ok = supportedProtocols[strings.ToLower(args[1])] |  | ||||||
| 				if !ok { |  | ||||||
| 					return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val()) |  | ||||||
| 				} |  | ||||||
| 				c.TLS.ProtocolMaxVersion = value |  | ||||||
| 			case "ciphers": |  | ||||||
| 				for c.NextArg() { |  | ||||||
| 					value, ok := supportedCiphersMap[strings.ToUpper(c.Val())] |  | ||||||
| 					if !ok { |  | ||||||
| 						return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val()) |  | ||||||
| 					} |  | ||||||
| 					c.TLS.Ciphers = append(c.TLS.Ciphers, value) |  | ||||||
| 				} |  | ||||||
| 			case "clients": |  | ||||||
| 				clientCertList := c.RemainingArgs() |  | ||||||
| 				if len(clientCertList) == 0 { |  | ||||||
| 					return nil, c.ArgErr() |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				listStart, mustProvideCA := 1, true |  | ||||||
| 				switch clientCertList[0] { |  | ||||||
| 				case "request": |  | ||||||
| 					c.TLS.ClientAuth = tls.RequestClientCert |  | ||||||
| 					mustProvideCA = false |  | ||||||
| 				case "require": |  | ||||||
| 					c.TLS.ClientAuth = tls.RequireAnyClientCert |  | ||||||
| 					mustProvideCA = false |  | ||||||
| 				case "verify_if_given": |  | ||||||
| 					c.TLS.ClientAuth = tls.VerifyClientCertIfGiven |  | ||||||
| 				default: |  | ||||||
| 					c.TLS.ClientAuth = tls.RequireAndVerifyClientCert |  | ||||||
| 					listStart = 0 |  | ||||||
| 				} |  | ||||||
| 				if mustProvideCA && len(clientCertList) <= listStart { |  | ||||||
| 					return nil, c.ArgErr() |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				c.TLS.ClientCerts = clientCertList[listStart:] |  | ||||||
| 			case "load": |  | ||||||
| 				c.Args(&loadDir) |  | ||||||
| 				c.TLS.Manual = true |  | ||||||
| 			case "max_certs": |  | ||||||
| 				c.Args(&maxCerts) |  | ||||||
| 				c.TLS.OnDemand = true |  | ||||||
| 			default: |  | ||||||
| 				return nil, c.Errf("Unknown keyword '%s'", c.Val()) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// tls requires at least one argument if a block is not opened |  | ||||||
| 		if len(args) == 0 && !hadBlock { |  | ||||||
| 			return nil, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// set certificate limit if on-demand TLS is enabled |  | ||||||
| 		if maxCerts != "" { |  | ||||||
| 			maxCertsNum, err := strconv.Atoi(maxCerts) |  | ||||||
| 			if err != nil || maxCertsNum < 1 { |  | ||||||
| 				return nil, c.Err("max_certs must be a positive integer") |  | ||||||
| 			} |  | ||||||
| 			if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost... |  | ||||||
| 				onDemandMaxIssue = int32(maxCertsNum) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// don't try to load certificates unless we're supposed to |  | ||||||
| 		if !c.TLS.Enabled || !c.TLS.Manual { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// load a single certificate and key, if specified |  | ||||||
| 		if certificateFile != "" && keyFile != "" { |  | ||||||
| 			err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err) |  | ||||||
| 			} |  | ||||||
| 			log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// load a directory of certificates, if specified |  | ||||||
| 		if loadDir != "" { |  | ||||||
| 			err := loadCertsInDir(c, loadDir) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	setDefaultTLSParams(c.Config) |  | ||||||
| 
 |  | ||||||
| 	return nil, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // loadCertsInDir loads all the certificates/keys in dir, as long as |  | ||||||
| // the file ends with .pem. This method of loading certificates is |  | ||||||
| // modeled after haproxy, which expects the certificate and key to |  | ||||||
| // be bundled into the same file: |  | ||||||
| // https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt |  | ||||||
| // |  | ||||||
| // This function may write to the log as it walks the directory tree. |  | ||||||
| func loadCertsInDir(c *setup.Controller, dir string) error { |  | ||||||
| 	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Printf("[WARNING] Unable to traverse into %s; skipping", path) |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
| 		if info.IsDir() { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
| 		if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { |  | ||||||
| 			certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer) |  | ||||||
| 			var foundKey bool // use only the first key in the file |  | ||||||
| 
 |  | ||||||
| 			bundle, err := ioutil.ReadFile(path) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			for { |  | ||||||
| 				// Decode next block so we can see what type it is |  | ||||||
| 				var derBlock *pem.Block |  | ||||||
| 				derBlock, bundle = pem.Decode(bundle) |  | ||||||
| 				if derBlock == nil { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				if derBlock.Type == "CERTIFICATE" { |  | ||||||
| 					// Re-encode certificate as PEM, appending to certificate chain |  | ||||||
| 					pem.Encode(certBuilder, derBlock) |  | ||||||
| 				} else if derBlock.Type == "EC PARAMETERS" { |  | ||||||
| 					// EC keys generated from openssl can be composed of two blocks: |  | ||||||
| 					// parameters and key (parameter block should come first) |  | ||||||
| 					if !foundKey { |  | ||||||
| 						// Encode parameters |  | ||||||
| 						pem.Encode(keyBuilder, derBlock) |  | ||||||
| 
 |  | ||||||
| 						// Key must immediately follow |  | ||||||
| 						derBlock, bundle = pem.Decode(bundle) |  | ||||||
| 						if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" { |  | ||||||
| 							return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path) |  | ||||||
| 						} |  | ||||||
| 						pem.Encode(keyBuilder, derBlock) |  | ||||||
| 						foundKey = true |  | ||||||
| 					} |  | ||||||
| 				} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { |  | ||||||
| 					// RSA key |  | ||||||
| 					if !foundKey { |  | ||||||
| 						pem.Encode(keyBuilder, derBlock) |  | ||||||
| 						foundKey = true |  | ||||||
| 					} |  | ||||||
| 				} else { |  | ||||||
| 					return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes() |  | ||||||
| 			if len(certPEMBytes) == 0 { |  | ||||||
| 				return c.Errf("%s: failed to parse PEM data", path) |  | ||||||
| 			} |  | ||||||
| 			if len(keyPEMBytes) == 0 { |  | ||||||
| 				return c.Errf("%s: no private key block found", path) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err) |  | ||||||
| 			} |  | ||||||
| 			log.Printf("[INFO] Successfully loaded TLS assets from %s", path) |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // setDefaultTLSParams sets the default TLS cipher suites, protocol versions, |  | ||||||
| // and server preferences of a server.Config if they were not previously set |  | ||||||
| // (it does not overwrite; only fills in missing values). It will also set the |  | ||||||
| // port to 443 if not already set, TLS is enabled, TLS is manual, and the host |  | ||||||
| // does not equal localhost. |  | ||||||
| func setDefaultTLSParams(c *server.Config) { |  | ||||||
| 	// If no ciphers provided, use default list |  | ||||||
| 	if len(c.TLS.Ciphers) == 0 { |  | ||||||
| 		c.TLS.Ciphers = defaultCiphers |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Not a cipher suite, but still important for mitigating protocol downgrade attacks |  | ||||||
| 	// (prepend since having it at end breaks http2 due to non-h2-approved suites before it) |  | ||||||
| 	c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...) |  | ||||||
| 
 |  | ||||||
| 	// Set default protocol min and max versions - must balance compatibility and security |  | ||||||
| 	if c.TLS.ProtocolMinVersion == 0 { |  | ||||||
| 		c.TLS.ProtocolMinVersion = tls.VersionTLS10 |  | ||||||
| 	} |  | ||||||
| 	if c.TLS.ProtocolMaxVersion == 0 { |  | ||||||
| 		c.TLS.ProtocolMaxVersion = tls.VersionTLS12 |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Prefer server cipher suites |  | ||||||
| 	c.TLS.PreferServerCipherSuites = true |  | ||||||
| 
 |  | ||||||
| 	// Default TLS port is 443; only use if port is not manually specified, |  | ||||||
| 	// TLS is enabled, and the host is not localhost |  | ||||||
| 	if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" { |  | ||||||
| 		c.Port = "443" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Map of supported key types |  | ||||||
| var supportedKeyTypes = map[string]acme.KeyType{ |  | ||||||
| 	"P384":    acme.EC384, |  | ||||||
| 	"P256":    acme.EC256, |  | ||||||
| 	"RSA8192": acme.RSA8192, |  | ||||||
| 	"RSA4096": acme.RSA4096, |  | ||||||
| 	"RSA2048": acme.RSA2048, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Map of supported protocols. |  | ||||||
| // SSLv3 will be not supported in future release. |  | ||||||
| // HTTP/2 only supports TLS 1.2 and higher. |  | ||||||
| var supportedProtocols = map[string]uint16{ |  | ||||||
| 	"ssl3.0": tls.VersionSSL30, |  | ||||||
| 	"tls1.0": tls.VersionTLS10, |  | ||||||
| 	"tls1.1": tls.VersionTLS11, |  | ||||||
| 	"tls1.2": tls.VersionTLS12, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Map of supported ciphers, used only for parsing config. |  | ||||||
| // |  | ||||||
| // Note that, at time of writing, HTTP/2 blacklists 276 cipher suites, |  | ||||||
| // including all but two of the suites below (the two GCM suites). |  | ||||||
| // See https://http2.github.io/http2-spec/#BadCipherSuites |  | ||||||
| // |  | ||||||
| // TLS_FALLBACK_SCSV is not in this list because we manually ensure |  | ||||||
| // it is always added (even though it is not technically a cipher suite). |  | ||||||
| // |  | ||||||
| // This map, like any map, is NOT ORDERED. Do not range over this map. |  | ||||||
| var supportedCiphersMap = map[string]uint16{ |  | ||||||
| 	"ECDHE-RSA-AES256-GCM-SHA384":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |  | ||||||
| 	"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, |  | ||||||
| 	"ECDHE-RSA-AES128-GCM-SHA256":   tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |  | ||||||
| 	"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |  | ||||||
| 	"ECDHE-RSA-AES128-CBC-SHA":      tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	"ECDHE-RSA-AES256-CBC-SHA":      tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	"ECDHE-ECDSA-AES256-CBC-SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	"ECDHE-ECDSA-AES128-CBC-SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	"RSA-AES128-CBC-SHA":            tls.TLS_RSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	"RSA-AES256-CBC-SHA":            tls.TLS_RSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	"ECDHE-RSA-3DES-EDE-CBC-SHA":    tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, |  | ||||||
| 	"RSA-3DES-EDE-CBC-SHA":          tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // List of supported cipher suites in descending order of preference. |  | ||||||
| // Ordering is very important! Getting the wrong order will break |  | ||||||
| // mainstream clients, especially with HTTP/2. |  | ||||||
| // |  | ||||||
| // Note that TLS_FALLBACK_SCSV is not in this list since it is always |  | ||||||
| // added manually. |  | ||||||
| var supportedCiphers = []uint16{ |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	tls.TLS_RSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	tls.TLS_RSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, |  | ||||||
| 	tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // List of all the ciphers we want to use by default |  | ||||||
| var defaultCiphers = []uint16{ |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, |  | ||||||
| 	tls.TLS_RSA_WITH_AES_256_CBC_SHA, |  | ||||||
| 	tls.TLS_RSA_WITH_AES_128_CBC_SHA, |  | ||||||
| } |  | ||||||
							
								
								
									
										7
									
								
								caddy/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								caddy/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import "github.com/mholt/caddy/caddy/caddymain" | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	caddymain.Run() | ||||||
|  | } | ||||||
| @ -1,165 +0,0 @@ | |||||||
| package parse |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type lexerTestCase struct { |  | ||||||
| 	input    string |  | ||||||
| 	expected []token |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestLexer(t *testing.T) { |  | ||||||
| 	testCases := []lexerTestCase{ |  | ||||||
| 		{ |  | ||||||
| 			input: `host:123`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "host:123"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `host:123 |  | ||||||
| 
 |  | ||||||
| 					directive`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "host:123"}, |  | ||||||
| 				{line: 3, text: "directive"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `host:123 { |  | ||||||
| 						directive |  | ||||||
| 					}`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "host:123"}, |  | ||||||
| 				{line: 1, text: "{"}, |  | ||||||
| 				{line: 2, text: "directive"}, |  | ||||||
| 				{line: 3, text: "}"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `host:123 { directive }`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "host:123"}, |  | ||||||
| 				{line: 1, text: "{"}, |  | ||||||
| 				{line: 1, text: "directive"}, |  | ||||||
| 				{line: 1, text: "}"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `host:123 { |  | ||||||
| 						#comment |  | ||||||
| 						directive |  | ||||||
| 						# comment |  | ||||||
| 						foobar # another comment |  | ||||||
| 					}`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "host:123"}, |  | ||||||
| 				{line: 1, text: "{"}, |  | ||||||
| 				{line: 3, text: "directive"}, |  | ||||||
| 				{line: 5, text: "foobar"}, |  | ||||||
| 				{line: 6, text: "}"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `a "quoted value" b |  | ||||||
| 					foobar`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "a"}, |  | ||||||
| 				{line: 1, text: "quoted value"}, |  | ||||||
| 				{line: 1, text: "b"}, |  | ||||||
| 				{line: 2, text: "foobar"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `A "quoted \"value\" inside" B`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "A"}, |  | ||||||
| 				{line: 1, text: `quoted "value" inside`}, |  | ||||||
| 				{line: 1, text: "B"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `"don't\escape"`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: `don't\escape`}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `"don't\\escape"`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: `don't\\escape`}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `A "quoted value with line |  | ||||||
| 					break inside" { |  | ||||||
| 						foobar |  | ||||||
| 					}`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "A"}, |  | ||||||
| 				{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"}, |  | ||||||
| 				{line: 2, text: "{"}, |  | ||||||
| 				{line: 3, text: "foobar"}, |  | ||||||
| 				{line: 4, text: "}"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `"C:\php\php-cgi.exe"`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: `C:\php\php-cgi.exe`}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: `empty "" string`, |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: `empty`}, |  | ||||||
| 				{line: 1, text: ``}, |  | ||||||
| 				{line: 1, text: `string`}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			input: "skip those\r\nCR characters", |  | ||||||
| 			expected: []token{ |  | ||||||
| 				{line: 1, text: "skip"}, |  | ||||||
| 				{line: 1, text: "those"}, |  | ||||||
| 				{line: 2, text: "CR"}, |  | ||||||
| 				{line: 2, text: "characters"}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for i, testCase := range testCases { |  | ||||||
| 		actual := tokenize(testCase.input) |  | ||||||
| 		lexerCompare(t, i, testCase.expected, actual) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func tokenize(input string) (tokens []token) { |  | ||||||
| 	l := lexer{} |  | ||||||
| 	l.load(strings.NewReader(input)) |  | ||||||
| 	for l.next() { |  | ||||||
| 		tokens = append(tokens, l.token) |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func lexerCompare(t *testing.T, n int, expected, actual []token) { |  | ||||||
| 	if len(expected) != len(actual) { |  | ||||||
| 		t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for i := 0; i < len(actual) && i < len(expected); i++ { |  | ||||||
| 		if actual[i].line != expected[i].line { |  | ||||||
| 			t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", |  | ||||||
| 				n, i, expected[i].text, expected[i].line, actual[i].line) |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 		if actual[i].text != expected[i].text { |  | ||||||
| 			t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", |  | ||||||
| 				n, i, expected[i].text, actual[i].text) |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,32 +0,0 @@ | |||||||
| // Package parse provides facilities for parsing configuration files. |  | ||||||
| package parse |  | ||||||
| 
 |  | ||||||
| import "io" |  | ||||||
| 
 |  | ||||||
| // ServerBlocks parses the input just enough to organize tokens, |  | ||||||
| // in order, by server block. No further parsing is performed. |  | ||||||
| // If checkDirectives is true, only valid directives will be allowed |  | ||||||
| // otherwise we consider it a parse error. Server blocks are returned |  | ||||||
| // in the order in which they appear. |  | ||||||
| func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) { |  | ||||||
| 	p := parser{Dispenser: NewDispenser(filename, input)} |  | ||||||
| 	p.checkDirectives = checkDirectives |  | ||||||
| 	blocks, err := p.parseAll() |  | ||||||
| 	return blocks, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // allTokens lexes the entire input, but does not parse it. |  | ||||||
| // It returns all the tokens from the input, unstructured |  | ||||||
| // and in order. |  | ||||||
| func allTokens(input io.Reader) (tokens []token) { |  | ||||||
| 	l := new(lexer) |  | ||||||
| 	l.load(input) |  | ||||||
| 	for l.next() { |  | ||||||
| 		tokens = append(tokens, l.token) |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ValidDirectives is a set of directives that are valid (unordered). Populated |  | ||||||
| // by config package's init function. |  | ||||||
| var ValidDirectives = make(map[string]struct{}) |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| package parse |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestAllTokens(t *testing.T) { |  | ||||||
| 	input := strings.NewReader("a b c\nd e") |  | ||||||
| 	expected := []string{"a", "b", "c", "d", "e"} |  | ||||||
| 	tokens := allTokens(input) |  | ||||||
| 
 |  | ||||||
| 	if len(tokens) != len(expected) { |  | ||||||
| 		t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for i, val := range expected { |  | ||||||
| 		if tokens[i].text != val { |  | ||||||
| 			t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,480 +0,0 @@ | |||||||
| package parse |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestStandardAddress(t *testing.T) { |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		input              string |  | ||||||
| 		scheme, host, port string |  | ||||||
| 		shouldErr          bool |  | ||||||
| 	}{ |  | ||||||
| 		{`localhost`, "", "localhost", "", false}, |  | ||||||
| 		{`LOCALHOST`, "", "localhost", "", false}, |  | ||||||
| 		{`localhost:1234`, "", "localhost", "1234", false}, |  | ||||||
| 		{`LOCALHOST:1234`, "", "localhost", "1234", false}, |  | ||||||
| 		{`localhost:`, "", "localhost", "", false}, |  | ||||||
| 		{`0.0.0.0`, "", "0.0.0.0", "", false}, |  | ||||||
| 		{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false}, |  | ||||||
| 		{`:1234`, "", "", "1234", false}, |  | ||||||
| 		{`[::1]`, "", "::1", "", false}, |  | ||||||
| 		{`[::1]:1234`, "", "::1", "1234", false}, |  | ||||||
| 		{`:`, "", "", "", false}, |  | ||||||
| 		{`localhost:http`, "http", "localhost", "80", false}, |  | ||||||
| 		{`localhost:https`, "https", "localhost", "443", false}, |  | ||||||
| 		{`:http`, "http", "", "80", false}, |  | ||||||
| 		{`:https`, "https", "", "443", false}, |  | ||||||
| 		{`http://localhost:https`, "", "", "", true}, // conflict |  | ||||||
| 		{`http://localhost:http`, "", "", "", true},  // repeated scheme |  | ||||||
| 		{`http://localhost:443`, "", "", "", true},   // not conventional |  | ||||||
| 		{`https://localhost:80`, "", "", "", true},   // not conventional |  | ||||||
| 		{`http://localhost`, "http", "localhost", "80", false}, |  | ||||||
| 		{`https://localhost`, "https", "localhost", "443", false}, |  | ||||||
| 		{`http://127.0.0.1`, "http", "127.0.0.1", "80", false}, |  | ||||||
| 		{`https://127.0.0.1`, "https", "127.0.0.1", "443", false}, |  | ||||||
| 		{`http://[::1]`, "http", "::1", "80", false}, |  | ||||||
| 		{`http://localhost:1234`, "http", "localhost", "1234", false}, |  | ||||||
| 		{`http://LOCALHOST:1234`, "http", "localhost", "1234", false}, |  | ||||||
| 		{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false}, |  | ||||||
| 		{`http://[::1]:1234`, "http", "::1", "1234", false}, |  | ||||||
| 		{``, "", "", "", false}, |  | ||||||
| 		{`::1`, "", "::1", "", true}, |  | ||||||
| 		{`localhost::`, "", "localhost::", "", true}, |  | ||||||
| 		{`#$%@`, "", "#$%@", "", true}, |  | ||||||
| 	} { |  | ||||||
| 		actual, err := standardAddress(test.input) |  | ||||||
| 
 |  | ||||||
| 		if err != nil && !test.shouldErr { |  | ||||||
| 			t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) |  | ||||||
| 		} |  | ||||||
| 		if err == nil && test.shouldErr { |  | ||||||
| 			t.Errorf("Test %d (%s): Expected error, but had none", i, test.input) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if actual.Scheme != test.scheme { |  | ||||||
| 			t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme) |  | ||||||
| 		} |  | ||||||
| 		if actual.Host != test.host { |  | ||||||
| 			t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host) |  | ||||||
| 		} |  | ||||||
| 		if actual.Port != test.port { |  | ||||||
| 			t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestParseOneAndImport(t *testing.T) { |  | ||||||
| 	setupParseTests() |  | ||||||
| 
 |  | ||||||
| 	testParseOne := func(input string) (ServerBlock, error) { |  | ||||||
| 		p := testParser(input) |  | ||||||
| 		p.Next() // parseOne doesn't call Next() to start, so we must |  | ||||||
| 		err := p.parseOne() |  | ||||||
| 		return p.block, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		input     string |  | ||||||
| 		shouldErr bool |  | ||||||
| 		addresses []address |  | ||||||
| 		tokens    map[string]int // map of directive name to number of tokens expected |  | ||||||
| 	}{ |  | ||||||
| 		{`localhost`, false, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost |  | ||||||
| 		  dir1`, false, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 1, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost:1234 |  | ||||||
| 		  dir1 foo bar`, false, []address{ |  | ||||||
| 			{"localhost:1234", "", "localhost", "1234"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost { |  | ||||||
| 		    dir1 |  | ||||||
| 		  }`, false, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 1, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost:1234 { |  | ||||||
| 		    dir1 foo bar |  | ||||||
| 		    dir2 |  | ||||||
| 		  }`, false, []address{ |  | ||||||
| 			{"localhost:1234", "", "localhost", "1234"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 			"dir2": 1, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://localhost https://localhost |  | ||||||
| 		  dir1 foo bar`, false, []address{ |  | ||||||
| 			{"http://localhost", "http", "localhost", "80"}, |  | ||||||
| 			{"https://localhost", "https", "localhost", "443"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://localhost https://localhost { |  | ||||||
| 		    dir1 foo bar |  | ||||||
| 		  }`, false, []address{ |  | ||||||
| 			{"http://localhost", "http", "localhost", "80"}, |  | ||||||
| 			{"https://localhost", "https", "localhost", "443"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://localhost, https://localhost { |  | ||||||
| 		    dir1 foo bar |  | ||||||
| 		  }`, false, []address{ |  | ||||||
| 			{"http://localhost", "http", "localhost", "80"}, |  | ||||||
| 			{"https://localhost", "https", "localhost", "443"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://localhost, { |  | ||||||
| 		  }`, true, []address{ |  | ||||||
| 			{"http://localhost", "http", "localhost", "80"}, |  | ||||||
| 		}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`host1:80, http://host2.com |  | ||||||
| 		  dir1 foo bar |  | ||||||
| 		  dir2 baz`, false, []address{ |  | ||||||
| 			{"host1:80", "", "host1", "80"}, |  | ||||||
| 			{"http://host2.com", "http", "host2.com", "80"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 			"dir2": 2, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://host1.com, |  | ||||||
| 		  http://host2.com, |  | ||||||
| 		  https://host3.com`, false, []address{ |  | ||||||
| 			{"http://host1.com", "http", "host1.com", "80"}, |  | ||||||
| 			{"http://host2.com", "http", "host2.com", "80"}, |  | ||||||
| 			{"https://host3.com", "https", "host3.com", "443"}, |  | ||||||
| 		}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://host1.com:1234, https://host2.com |  | ||||||
| 		  dir1 foo { |  | ||||||
| 		    bar baz |  | ||||||
| 		  } |  | ||||||
| 		  dir2`, false, []address{ |  | ||||||
| 			{"http://host1.com:1234", "http", "host1.com", "1234"}, |  | ||||||
| 			{"https://host2.com", "https", "host2.com", "443"}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 6, |  | ||||||
| 			"dir2": 1, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`127.0.0.1 |  | ||||||
| 		  dir1 { |  | ||||||
| 		    bar baz |  | ||||||
| 		  } |  | ||||||
| 		  dir2 { |  | ||||||
| 		    foo bar |  | ||||||
| 		  }`, false, []address{ |  | ||||||
| 			{"127.0.0.1", "", "127.0.0.1", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 5, |  | ||||||
| 			"dir2": 5, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`127.0.0.1 |  | ||||||
| 		  unknown_directive`, true, []address{ |  | ||||||
| 			{"127.0.0.1", "", "127.0.0.1", ""}, |  | ||||||
| 		}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost |  | ||||||
| 		  dir1 { |  | ||||||
| 		    foo`, true, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost |  | ||||||
| 		  dir1 { |  | ||||||
| 		  }`, false, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost |  | ||||||
| 		  dir1 { |  | ||||||
| 		  } }`, true, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost |  | ||||||
| 		  dir1 { |  | ||||||
| 		    nested { |  | ||||||
| 		      foo |  | ||||||
| 		    } |  | ||||||
| 		  } |  | ||||||
| 		  dir2 foo bar`, false, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 7, |  | ||||||
| 			"dir2": 3, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{``, false, []address{}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost |  | ||||||
| 		  dir1 arg1 |  | ||||||
| 		  import import_test1.txt`, false, []address{ |  | ||||||
| 			{"localhost", "", "localhost", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 2, |  | ||||||
| 			"dir2": 3, |  | ||||||
| 			"dir3": 1, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`import import_test2.txt`, false, []address{ |  | ||||||
| 			{"host1", "", "host1", ""}, |  | ||||||
| 		}, map[string]int{ |  | ||||||
| 			"dir1": 1, |  | ||||||
| 			"dir2": 2, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`import not_found.txt`, true, []address{}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{`""`, false, []address{}, map[string]int{}}, |  | ||||||
| 
 |  | ||||||
| 		{``, false, []address{}, map[string]int{}}, |  | ||||||
| 	} { |  | ||||||
| 		result, err := testParseOne(test.input) |  | ||||||
| 
 |  | ||||||
| 		if test.shouldErr && err == nil { |  | ||||||
| 			t.Errorf("Test %d: Expected an error, but didn't get one", i) |  | ||||||
| 		} |  | ||||||
| 		if !test.shouldErr && err != nil { |  | ||||||
| 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if len(result.Addresses) != len(test.addresses) { |  | ||||||
| 			t.Errorf("Test %d: Expected %d addresses, got %d", |  | ||||||
| 				i, len(test.addresses), len(result.Addresses)) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		for j, addr := range result.Addresses { |  | ||||||
| 			if addr.Host != test.addresses[j].Host { |  | ||||||
| 				t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'", |  | ||||||
| 					i, j, test.addresses[j].Host, addr.Host) |  | ||||||
| 			} |  | ||||||
| 			if addr.Port != test.addresses[j].Port { |  | ||||||
| 				t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'", |  | ||||||
| 					i, j, test.addresses[j].Port, addr.Port) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if len(result.Tokens) != len(test.tokens) { |  | ||||||
| 			t.Errorf("Test %d: Expected %d directives, had %d", |  | ||||||
| 				i, len(test.tokens), len(result.Tokens)) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		for directive, tokens := range result.Tokens { |  | ||||||
| 			if len(tokens) != test.tokens[directive] { |  | ||||||
| 				t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d", |  | ||||||
| 					i, directive, test.tokens[directive], len(tokens)) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestParseAll(t *testing.T) { |  | ||||||
| 	setupParseTests() |  | ||||||
| 
 |  | ||||||
| 	for i, test := range []struct { |  | ||||||
| 		input     string |  | ||||||
| 		shouldErr bool |  | ||||||
| 		addresses [][]address // addresses per server block, in order |  | ||||||
| 	}{ |  | ||||||
| 		{`localhost`, false, [][]address{ |  | ||||||
| 			{{"localhost", "", "localhost", ""}}, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost:1234`, false, [][]address{ |  | ||||||
| 			{{"localhost:1234", "", "localhost", "1234"}}, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost:1234 { |  | ||||||
| 		  } |  | ||||||
| 		  localhost:2015 { |  | ||||||
| 		  }`, false, [][]address{ |  | ||||||
| 			{{"localhost:1234", "", "localhost", "1234"}}, |  | ||||||
| 			{{"localhost:2015", "", "localhost", "2015"}}, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost:1234, http://host2`, false, [][]address{ |  | ||||||
| 			{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}}, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`localhost:1234, http://host2,`, true, [][]address{}}, |  | ||||||
| 
 |  | ||||||
| 		{`http://host1.com, http://host2.com { |  | ||||||
| 		  } |  | ||||||
| 		  https://host3.com, https://host4.com { |  | ||||||
| 		  }`, false, [][]address{ |  | ||||||
| 			{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}}, |  | ||||||
| 			{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}}, |  | ||||||
| 		}}, |  | ||||||
| 
 |  | ||||||
| 		{`import import_glob*.txt`, false, [][]address{ |  | ||||||
| 			{{"glob0.host0", "", "glob0.host0", ""}}, |  | ||||||
| 			{{"glob0.host1", "", "glob0.host1", ""}}, |  | ||||||
| 			{{"glob1.host0", "", "glob1.host0", ""}}, |  | ||||||
| 			{{"glob2.host0", "", "glob2.host0", ""}}, |  | ||||||
| 		}}, |  | ||||||
| 	} { |  | ||||||
| 		p := testParser(test.input) |  | ||||||
| 		blocks, err := p.parseAll() |  | ||||||
| 
 |  | ||||||
| 		if test.shouldErr && err == nil { |  | ||||||
| 			t.Errorf("Test %d: Expected an error, but didn't get one", i) |  | ||||||
| 		} |  | ||||||
| 		if !test.shouldErr && err != nil { |  | ||||||
| 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if len(blocks) != len(test.addresses) { |  | ||||||
| 			t.Errorf("Test %d: Expected %d server blocks, got %d", |  | ||||||
| 				i, len(test.addresses), len(blocks)) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		for j, block := range blocks { |  | ||||||
| 			if len(block.Addresses) != len(test.addresses[j]) { |  | ||||||
| 				t.Errorf("Test %d: Expected %d addresses in block %d, got %d", |  | ||||||
| 					i, len(test.addresses[j]), j, len(block.Addresses)) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			for k, addr := range block.Addresses { |  | ||||||
| 				if addr.Host != test.addresses[j][k].Host { |  | ||||||
| 					t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'", |  | ||||||
| 						i, j, k, test.addresses[j][k].Host, addr.Host) |  | ||||||
| 				} |  | ||||||
| 				if addr.Port != test.addresses[j][k].Port { |  | ||||||
| 					t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'", |  | ||||||
| 						i, j, k, test.addresses[j][k].Port, addr.Port) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestEnvironmentReplacement(t *testing.T) { |  | ||||||
| 	setupParseTests() |  | ||||||
| 
 |  | ||||||
| 	os.Setenv("PORT", "8080") |  | ||||||
| 	os.Setenv("ADDRESS", "servername.com") |  | ||||||
| 	os.Setenv("FOOBAR", "foobar") |  | ||||||
| 
 |  | ||||||
| 	// basic test; unix-style env vars |  | ||||||
| 	p := testParser(`{$ADDRESS}`) |  | ||||||
| 	blocks, _ := p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { |  | ||||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// multiple vars per token |  | ||||||
| 	p = testParser(`{$ADDRESS}:{$PORT}`) |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { |  | ||||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { |  | ||||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// windows-style var and unix style in same token |  | ||||||
| 	p = testParser(`{%ADDRESS%}:{$PORT}`) |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { |  | ||||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { |  | ||||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// reverse order |  | ||||||
| 	p = testParser(`{$ADDRESS}:{%PORT%}`) |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { |  | ||||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { |  | ||||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// env var in server block body as argument |  | ||||||
| 	p = testParser(":{%PORT%}\ndir1 {$FOOBAR}") |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { |  | ||||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual { |  | ||||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// combined windows env vars in argument |  | ||||||
| 	p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}") |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual { |  | ||||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// malformed env var (windows) |  | ||||||
| 	p = testParser(":1234\ndir1 {%ADDRESS}") |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual { |  | ||||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// malformed (non-existent) env var (unix) |  | ||||||
| 	p = testParser(`:{$PORT$}`) |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual { |  | ||||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// in quoted field |  | ||||||
| 	p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") |  | ||||||
| 	blocks, _ = p.parseAll() |  | ||||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual { |  | ||||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func setupParseTests() { |  | ||||||
| 	// Set up some bogus directives for testing |  | ||||||
| 	ValidDirectives = map[string]struct{}{ |  | ||||||
| 		"dir1": {}, |  | ||||||
| 		"dir2": {}, |  | ||||||
| 		"dir3": {}, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func testParser(input string) parser { |  | ||||||
| 	buf := strings.NewReader(input) |  | ||||||
| 	p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true} |  | ||||||
| 	return p |  | ||||||
| } |  | ||||||
| @ -1,92 +0,0 @@ | |||||||
| // +build !windows |  | ||||||
| 
 |  | ||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"errors" |  | ||||||
| 	"log" |  | ||||||
| 	"net" |  | ||||||
| 	"path/filepath" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/caddy/https" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Restart restarts the entire application; gracefully with zero |  | ||||||
| // downtime if on a POSIX-compatible system, or forcefully if on |  | ||||||
| // Windows but with imperceptibly-short downtime. |  | ||||||
| // |  | ||||||
| // The behavior can be controlled by the RestartMode variable, |  | ||||||
| // where "inproc" will restart forcefully in process same as |  | ||||||
| // Windows on a POSIX-compatible system. |  | ||||||
| // |  | ||||||
| // The restarted application will use newCaddyfile as its input |  | ||||||
| // configuration. If newCaddyfile is nil, the current (existing) |  | ||||||
| // Caddyfile configuration will be used. |  | ||||||
| // |  | ||||||
| // Note: The process must exist in the same place on the disk in |  | ||||||
| // order for this to work. Thus, multiple graceful restarts don't |  | ||||||
| // work if executing with `go run`, since the binary is cleaned up |  | ||||||
| // when `go run` sees the initial parent process exit. |  | ||||||
| func Restart(newCaddyfile Input) error { |  | ||||||
| 	log.Println("[INFO] Restarting") |  | ||||||
| 
 |  | ||||||
| 	if newCaddyfile == nil { |  | ||||||
| 		caddyfileMu.Lock() |  | ||||||
| 		newCaddyfile = caddyfile |  | ||||||
| 		caddyfileMu.Unlock() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Get certificates for any new hosts in the new Caddyfile without causing downtime |  | ||||||
| 	err := getCertsForNewCaddyfile(newCaddyfile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.New("TLS preload: " + err.Error()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Add file descriptors of all the sockets for new instance |  | ||||||
| 	serversMu.Lock() |  | ||||||
| 	for _, s := range servers { |  | ||||||
| 		restartFds[s.Addr] = s.ListenerFd() |  | ||||||
| 	} |  | ||||||
| 	serversMu.Unlock() |  | ||||||
| 
 |  | ||||||
| 	return restartInProc(newCaddyfile) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getCertsForNewCaddyfile(newCaddyfile Input) error { |  | ||||||
| 	// parse the new caddyfile only up to (and including) TLS |  | ||||||
| 	// so we can know what we need to get certs for. |  | ||||||
| 	configs, _, _, err := loadConfigsUpToIncludingTLS(filepath.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body())) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.New("loading Caddyfile: " + err.Error()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// first mark the configs that are qualified for managed TLS |  | ||||||
| 	https.MarkQualified(configs) |  | ||||||
| 
 |  | ||||||
| 	// since we group by bind address to obtain certs, we must call |  | ||||||
| 	// EnableTLS to make sure the port is set properly first |  | ||||||
| 	// (can ignore error since we aren't actually using the certs) |  | ||||||
| 	https.EnableTLS(configs, false) |  | ||||||
| 
 |  | ||||||
| 	// find out if we can let the acme package start its own challenge listener |  | ||||||
| 	// on port 80 |  | ||||||
| 	var proxyACME bool |  | ||||||
| 	serversMu.Lock() |  | ||||||
| 	for _, s := range servers { |  | ||||||
| 		_, port, _ := net.SplitHostPort(s.Addr) |  | ||||||
| 		if port == "80" { |  | ||||||
| 			proxyACME = true |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	serversMu.Unlock() |  | ||||||
| 
 |  | ||||||
| 	// place certs on the disk |  | ||||||
| 	err = https.ObtainCerts(configs, false, proxyACME) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errors.New("obtaining certs: " + err.Error()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import "log" |  | ||||||
| 
 |  | ||||||
| // Restart restarts Caddy forcefully using newCaddyfile, |  | ||||||
| // or, if nil, the current/existing Caddyfile is reused. |  | ||||||
| func Restart(newCaddyfile Input) error { |  | ||||||
| 	log.Println("[INFO] Restarting") |  | ||||||
| 
 |  | ||||||
| 	if newCaddyfile == nil { |  | ||||||
| 		caddyfileMu.Lock() |  | ||||||
| 		newCaddyfile = caddyfile |  | ||||||
| 		caddyfileMu.Unlock() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return restartInProc(newCaddyfile) |  | ||||||
| } |  | ||||||
| @ -1,28 +0,0 @@ | |||||||
| package caddy |  | ||||||
| 
 |  | ||||||
| import "log" |  | ||||||
| 
 |  | ||||||
| // restartInProc restarts Caddy forcefully in process using newCaddyfile. |  | ||||||
| func restartInProc(newCaddyfile Input) error { |  | ||||||
| 	wg.Add(1) // barrier so Wait() doesn't unblock |  | ||||||
| 	defer wg.Done() |  | ||||||
| 
 |  | ||||||
| 	err := Stop() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	caddyfileMu.Lock() |  | ||||||
| 	oldCaddyfile := caddyfile |  | ||||||
| 	caddyfileMu.Unlock() |  | ||||||
| 
 |  | ||||||
| 	err = Start(newCaddyfile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		// revert to old Caddyfile |  | ||||||
| 		if oldErr := Start(oldCaddyfile); oldErr != nil { |  | ||||||
| 			log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import "github.com/mholt/caddy/middleware" |  | ||||||
| 
 |  | ||||||
| // BindHost sets the host to bind the listener to. |  | ||||||
| func BindHost(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	for c.Next() { |  | ||||||
| 		if !c.Args(&c.BindHost) { |  | ||||||
| 			return nil, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil, nil |  | ||||||
| } |  | ||||||
| @ -1,83 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/caddy/parse" |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Controller is given to the setup function of middlewares which |  | ||||||
| // gives them access to be able to read tokens and set config. Each |  | ||||||
| // virtualhost gets their own server config and dispenser. |  | ||||||
| type Controller struct { |  | ||||||
| 	*server.Config |  | ||||||
| 	parse.Dispenser |  | ||||||
| 
 |  | ||||||
| 	// OncePerServerBlock is a function that executes f |  | ||||||
| 	// exactly once per server block, no matter how many |  | ||||||
| 	// hosts are associated with it. If it is the first |  | ||||||
| 	// time, the function f is executed immediately |  | ||||||
| 	// (not deferred) and may return an error which is |  | ||||||
| 	// returned by OncePerServerBlock. |  | ||||||
| 	OncePerServerBlock func(f func() error) error |  | ||||||
| 
 |  | ||||||
| 	// ServerBlockIndex is the 0-based index of the |  | ||||||
| 	// server block as it appeared in the input. |  | ||||||
| 	ServerBlockIndex int |  | ||||||
| 
 |  | ||||||
| 	// ServerBlockHostIndex is the 0-based index of this |  | ||||||
| 	// host as it appeared in the input at the head of the |  | ||||||
| 	// server block. |  | ||||||
| 	ServerBlockHostIndex int |  | ||||||
| 
 |  | ||||||
| 	// ServerBlockHosts is a list of hosts that are |  | ||||||
| 	// associated with this server block. All these |  | ||||||
| 	// hosts, consequently, share the same tokens. |  | ||||||
| 	ServerBlockHosts []string |  | ||||||
| 
 |  | ||||||
| 	// ServerBlockStorage is used by a directive's |  | ||||||
| 	// setup function to persist state between all |  | ||||||
| 	// the hosts on a server block. |  | ||||||
| 	ServerBlockStorage interface{} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // NewTestController creates a new *Controller for |  | ||||||
| // the input specified, with a filename of "Testfile". |  | ||||||
| // The Config is bare, consisting only of a Root of cwd. |  | ||||||
| // |  | ||||||
| // Used primarily for testing but needs to be exported so |  | ||||||
| // add-ons can use this as a convenience. Does not initialize |  | ||||||
| // the server-block-related fields. |  | ||||||
| func NewTestController(input string) *Controller { |  | ||||||
| 	return &Controller{ |  | ||||||
| 		Config: &server.Config{ |  | ||||||
| 			Root: ".", |  | ||||||
| 		}, |  | ||||||
| 		Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), |  | ||||||
| 		OncePerServerBlock: func(f func() error) error { |  | ||||||
| 			return f() |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // EmptyNext is a no-op function that can be passed into |  | ||||||
| // middleware.Middleware functions so that the assignment |  | ||||||
| // to the Next field of the Handler can be tested. |  | ||||||
| // |  | ||||||
| // Used primarily for testing but needs to be exported so |  | ||||||
| // add-ons can use this as a convenience. |  | ||||||
| var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { |  | ||||||
| 	return 0, nil |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // SameNext does a pointer comparison between next1 and next2. |  | ||||||
| // |  | ||||||
| // Used primarily for testing but needs to be exported so |  | ||||||
| // add-ons can use this as a convenience. |  | ||||||
| func SameNext(next1, next2 middleware.Handler) bool { |  | ||||||
| 	return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2) |  | ||||||
| } |  | ||||||
| @ -1,60 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	stdexpvar "expvar" |  | ||||||
| 	"runtime" |  | ||||||
| 	"sync" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/middleware/expvar" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // ExpVar configures a new ExpVar middleware instance. |  | ||||||
| func ExpVar(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	resource, err := expVarParse(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// publish any extra information/metrics we may want to capture |  | ||||||
| 	publishExtraVars() |  | ||||||
| 
 |  | ||||||
| 	expvar := expvar.ExpVar{Resource: resource} |  | ||||||
| 
 |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		expvar.Next = next |  | ||||||
| 		return expvar |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func expVarParse(c *Controller) (expvar.Resource, error) { |  | ||||||
| 	var resource expvar.Resource |  | ||||||
| 	var err error |  | ||||||
| 
 |  | ||||||
| 	for c.Next() { |  | ||||||
| 		args := c.RemainingArgs() |  | ||||||
| 		switch len(args) { |  | ||||||
| 		case 0: |  | ||||||
| 			resource = expvar.Resource(defaultExpvarPath) |  | ||||||
| 		case 1: |  | ||||||
| 			resource = expvar.Resource(args[0]) |  | ||||||
| 		default: |  | ||||||
| 			return resource, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return resource, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func publishExtraVars() { |  | ||||||
| 	// By using sync.Once instead of an init() function, we don't clutter |  | ||||||
| 	// the app's expvar export unnecessarily, or risk colliding with it. |  | ||||||
| 	publishOnce.Do(func() { |  | ||||||
| 		stdexpvar.Publish("Goroutines", stdexpvar.Func(func() interface{} { |  | ||||||
| 			return runtime.NumGoroutine() |  | ||||||
| 		})) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var publishOnce sync.Once // publishing variables should only be done once |  | ||||||
| var defaultExpvarPath = "/debug/vars" |  | ||||||
| @ -1,39 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/middleware/expvar" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestExpvar(t *testing.T) { |  | ||||||
| 	c := NewTestController(`expvar`) |  | ||||||
| 	mid, err := ExpVar(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Errorf("Expected no errors, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 	if mid == nil { |  | ||||||
| 		t.Fatal("Expected middleware, was nil instead") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	c = NewTestController(`expvar /d/v`) |  | ||||||
| 	mid, err = ExpVar(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Errorf("Expected no errors, got: %v", err) |  | ||||||
| 	} |  | ||||||
| 	if mid == nil { |  | ||||||
| 		t.Fatal("Expected middleware, was nil instead") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	handler := mid(EmptyNext) |  | ||||||
| 	myHandler, ok := handler.(expvar.ExpVar) |  | ||||||
| 	if !ok { |  | ||||||
| 		t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler) |  | ||||||
| 	} |  | ||||||
| 	if myHandler.Resource != "/d/v" { |  | ||||||
| 		t.Errorf("Expected /d/v as expvar resource") |  | ||||||
| 	} |  | ||||||
| 	if !SameNext(myHandler.Next, EmptyNext) { |  | ||||||
| 		t.Error("'Next' field of handler was not set properly") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,55 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/middleware/extensions" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Ext configures a new instance of 'extensions' middleware for clean URLs. |  | ||||||
| func Ext(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	root := c.Root |  | ||||||
| 
 |  | ||||||
| 	exts, err := extParse(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		return extensions.Ext{ |  | ||||||
| 			Next:       next, |  | ||||||
| 			Extensions: exts, |  | ||||||
| 			Root:       root, |  | ||||||
| 		} |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // extParse sets up an instance of extension middleware |  | ||||||
| // from a middleware controller and returns a list of extensions. |  | ||||||
| func extParse(c *Controller) ([]string, error) { |  | ||||||
| 	var exts []string |  | ||||||
| 
 |  | ||||||
| 	for c.Next() { |  | ||||||
| 		// At least one extension is required |  | ||||||
| 		if !c.NextArg() { |  | ||||||
| 			return exts, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 		exts = append(exts, c.Val()) |  | ||||||
| 
 |  | ||||||
| 		// Tack on any other extensions that may have been listed |  | ||||||
| 		exts = append(exts, c.RemainingArgs()...) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return exts, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // resourceExists returns true if the file specified at |  | ||||||
| // root + path exists; false otherwise. |  | ||||||
| func resourceExists(root, path string) bool { |  | ||||||
| 	_, err := os.Stat(filepath.Join(root, path)) |  | ||||||
| 	// technically we should use os.IsNotExist(err) |  | ||||||
| 	// but we don't handle any other kinds of errors anyway |  | ||||||
| 	return err == nil |  | ||||||
| } |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/middleware/inner" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Internal configures a new Internal middleware instance. |  | ||||||
| func Internal(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	paths, err := internalParse(c) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		return inner.Internal{Next: next, Paths: paths} |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func internalParse(c *Controller) ([]string, error) { |  | ||||||
| 	var paths []string |  | ||||||
| 
 |  | ||||||
| 	for c.Next() { |  | ||||||
| 		if !c.NextArg() { |  | ||||||
| 			return paths, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 		paths = append(paths, c.Val()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return paths, nil |  | ||||||
| } |  | ||||||
| @ -1,27 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/middleware/pprof" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| //PProf returns a new instance of a pprof handler. It accepts no arguments or options. |  | ||||||
| func PProf(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	found := false |  | ||||||
| 	for c.Next() { |  | ||||||
| 		if found { |  | ||||||
| 			return nil, c.Err("pprof can only be specified once") |  | ||||||
| 		} |  | ||||||
| 		if len(c.RemainingArgs()) != 0 { |  | ||||||
| 			return nil, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 		if c.NextBlock() { |  | ||||||
| 			return nil, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 		found = true |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		return &pprof.Handler{Next: next, Mux: pprof.NewMux()} |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| 	"github.com/mholt/caddy/middleware/proxy" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Proxy configures a new Proxy middleware instance. |  | ||||||
| func Proxy(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	upstreams, err := proxy.NewStaticUpstreams(c.Dispenser) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		return proxy.Proxy{Next: next, Upstreams: upstreams} |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| @ -1,32 +0,0 @@ | |||||||
| package setup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/middleware" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Root sets up the root file path of the server. |  | ||||||
| func Root(c *Controller) (middleware.Middleware, error) { |  | ||||||
| 	for c.Next() { |  | ||||||
| 		if !c.NextArg() { |  | ||||||
| 			return nil, c.ArgErr() |  | ||||||
| 		} |  | ||||||
| 		c.Root = c.Val() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Check if root path exists |  | ||||||
| 	_, err := os.Stat(c.Root) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if os.IsNotExist(err) { |  | ||||||
| 			// Allow this, because the folder might appear later. |  | ||||||
| 			// But make sure the user knows! |  | ||||||
| 			log.Printf("[WARNING] Root path does not exist: %s", c.Root) |  | ||||||
| 		} else { |  | ||||||
| 			return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil, nil |  | ||||||
| } |  | ||||||
							
								
								
									
										1
									
								
								caddy/setup/testdata/blog/first_post.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								caddy/setup/testdata/blog/first_post.md
									
									
									
									
										vendored
									
									
								
							| @ -1 +0,0 @@ | |||||||
| # Test h1 |  | ||||||
							
								
								
									
										10
									
								
								caddy/setup/testdata/tpl_with_include.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								caddy/setup/testdata/tpl_with_include.html
									
									
									
									
										vendored
									
									
								
							| @ -1,10 +0,0 @@ | |||||||
| <!DOCTYPE html> |  | ||||||
| <html> |  | ||||||
| <head> |  | ||||||
| <title>{{.Doc.title}}</title> |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
| {{.Include "header.html"}} |  | ||||||
| {{.Doc.body}} |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
							
								
								
									
										58
									
								
								caddy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								caddy_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | package caddy | ||||||
|  | 
 | ||||||
|  | import "testing" | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | // TODO | ||||||
|  | func TestCaddyStartStop(t *testing.T) { | ||||||
|  | 	caddyfile := "localhost:1984" | ||||||
|  | 
 | ||||||
|  | 	for i := 0; i < 2; i++ { | ||||||
|  | 		_, err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Error starting, iteration %d: %v", i, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		client := http.Client{ | ||||||
|  | 			Timeout: time.Duration(2 * time.Second), | ||||||
|  | 		} | ||||||
|  | 		resp, err := client.Get("http://localhost:1984") | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) | ||||||
|  | 		} | ||||||
|  | 		resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 		err = Stop() | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Error stopping, iteration %d: %v", i, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | func TestIsLoopback(t *testing.T) { | ||||||
|  | 	for i, test := range []struct { | ||||||
|  | 		input  string | ||||||
|  | 		expect bool | ||||||
|  | 	}{ | ||||||
|  | 		{"example.com", false}, | ||||||
|  | 		{"localhost", true}, | ||||||
|  | 		{"localhost:1234", true}, | ||||||
|  | 		{"localhost:", true}, | ||||||
|  | 		{"127.0.0.1", true}, | ||||||
|  | 		{"127.0.0.1:443", true}, | ||||||
|  | 		{"127.0.1.5", true}, | ||||||
|  | 		{"10.0.0.5", false}, | ||||||
|  | 		{"12.7.0.1", false}, | ||||||
|  | 		{"[::1]", true}, | ||||||
|  | 		{"[::1]:1234", true}, | ||||||
|  | 		{"::1", true}, | ||||||
|  | 		{"::", false}, | ||||||
|  | 		{"[::]", false}, | ||||||
|  | 		{"local", false}, | ||||||
|  | 	} { | ||||||
|  | 		if got, want := IsLoopback(test.input), test.expect; got != want { | ||||||
|  | 			t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package parse | package caddyfile | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| @ -12,7 +12,7 @@ import ( | |||||||
| // some really convenient methods. | // some really convenient methods. | ||||||
| type Dispenser struct { | type Dispenser struct { | ||||||
| 	filename string | 	filename string | ||||||
| 	tokens   []token | 	tokens   []Token | ||||||
| 	cursor   int | 	cursor   int | ||||||
| 	nesting  int | 	nesting  int | ||||||
| } | } | ||||||
| @ -27,7 +27,7 @@ func NewDispenser(filename string, input io.Reader) Dispenser { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewDispenserTokens returns a Dispenser filled with the given tokens. | // 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{ | 	return Dispenser{ | ||||||
| 		filename: filename, | 		filename: filename, | ||||||
| 		tokens:   tokens, | 		tokens:   tokens, | ||||||
| @ -59,8 +59,8 @@ func (d *Dispenser) NextArg() bool { | |||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	if d.cursor < len(d.tokens)-1 && | 	if d.cursor < len(d.tokens)-1 && | ||||||
| 		d.tokens[d.cursor].file == d.tokens[d.cursor+1].file && | 		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].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { | ||||||
| 		d.cursor++ | 		d.cursor++ | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| @ -80,8 +80,8 @@ func (d *Dispenser) NextLine() bool { | |||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	if d.cursor < len(d.tokens)-1 && | 	if d.cursor < len(d.tokens)-1 && | ||||||
| 		(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file || | 		(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].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { | ||||||
| 		d.cursor++ | 		d.cursor++ | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| @ -131,7 +131,7 @@ func (d *Dispenser) Val() string { | |||||||
| 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | ||||||
| 		return "" | 		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 | // 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) { | 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | ||||||
| 		return 0 | 		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, | // 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) { | 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | ||||||
| 		return d.filename | 		return d.filename | ||||||
| 	} | 	} | ||||||
| 	if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" { | 	if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" { | ||||||
| 		return tokenFilename | 		return tokenFilename | ||||||
| 	} | 	} | ||||||
| 	return d.filename | 	return d.filename | ||||||
| @ -233,7 +233,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int { | |||||||
| 	if tknIdx < 0 || tknIdx >= len(d.tokens) { | 	if tknIdx < 0 || tknIdx >= len(d.tokens) { | ||||||
| 		return 0 | 		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 | // 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 { | 	if d.cursor > len(d.tokens)-1 { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file || | 	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 | 		d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line | ||||||
| } | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package parse | package caddyfile | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"reflect" | 	"reflect" | ||||||
| @ -4,31 +4,26 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" |  | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/caddy/parse" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const filename = "Caddyfile" | const filename = "Caddyfile" | ||||||
| 
 | 
 | ||||||
| // ToJSON converts caddyfile to its JSON representation. | // ToJSON converts caddyfile to its JSON representation. | ||||||
| func ToJSON(caddyfile []byte) ([]byte, error) { | 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 { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, sb := range serverBlocks { | 	for _, sb := range serverBlocks { | ||||||
| 		block := ServerBlock{Body: [][]interface{}{}} | 		block := EncodedServerBlock{ | ||||||
| 
 | 			Keys: sb.Keys, | ||||||
| 		// Fill up host list | 			Body: [][]interface{}{}, | ||||||
| 		for _, host := range sb.HostList() { |  | ||||||
| 			block.Hosts = append(block.Hosts, standardizeScheme(host)) |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Extract directives deterministically by sorting them | 		// 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 | 		// Convert each directive's tokens into our JSON structure | ||||||
| 		for _, dir := range directives { | 		for _, dir := range directives { | ||||||
| 			disp := parse.NewDispenserTokens(filename, sb.Tokens[dir]) | 			disp := NewDispenserTokens(filename, sb.Tokens[dir]) | ||||||
| 			for disp.Next() { | 			for disp.Next() { | ||||||
| 				block.Body = append(block.Body, constructLine(&disp)) | 				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 | // 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 | // a server block only (where the first token on each line is a | ||||||
| // directive) - not to be used at any other nesting level. | // directive) - not to be used at any other nesting level. | ||||||
| func constructLine(d *parse.Dispenser) []interface{} { | func constructLine(d *Dispenser) []interface{} { | ||||||
| 	var args []interface{} | 	var args []interface{} | ||||||
| 
 | 
 | ||||||
| 	args = append(args, d.Val()) | 	args = append(args, d.Val()) | ||||||
| @ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} { | |||||||
| // constructBlock recursively processes tokens into a | // constructBlock recursively processes tokens into a | ||||||
| // JSON-encodable structure. To be used in a directive's | // JSON-encodable structure. To be used in a directive's | ||||||
| // block. Goes to end of block. | // block. Goes to end of block. | ||||||
| func constructBlock(d *parse.Dispenser) [][]interface{} { | func constructBlock(d *Dispenser) [][]interface{} { | ||||||
| 	block := [][]interface{}{} | 	block := [][]interface{}{} | ||||||
| 
 | 
 | ||||||
| 	for d.Next() { | 	for d.Next() { | ||||||
| @ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} { | |||||||
| 
 | 
 | ||||||
| // FromJSON converts JSON-encoded jsonBytes to Caddyfile text | // FromJSON converts JSON-encoded jsonBytes to Caddyfile text | ||||||
| func FromJSON(jsonBytes []byte) ([]byte, error) { | func FromJSON(jsonBytes []byte) ([]byte, error) { | ||||||
| 	var j Caddyfile | 	var j EncodedCaddyfile | ||||||
| 	var result string | 	var result string | ||||||
| 
 | 
 | ||||||
| 	err := json.Unmarshal(jsonBytes, &j) | 	err := json.Unmarshal(jsonBytes, &j) | ||||||
| @ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) { | |||||||
| 		if sbPos > 0 { | 		if sbPos > 0 { | ||||||
| 			result += "\n\n" | 			result += "\n\n" | ||||||
| 		} | 		} | ||||||
| 		for i, host := range sb.Hosts { | 		for i, key := range sb.Keys { | ||||||
| 			if i > 0 { | 			if i > 0 { | ||||||
| 				result += ", " | 				result += ", " | ||||||
| 			} | 			} | ||||||
| 			result += standardizeScheme(host) | 			//result += standardizeScheme(key) | ||||||
|  | 			result += key | ||||||
| 		} | 		} | ||||||
| 		result += jsonToText(sb.Body, 1) | 		result += jsonToText(sb.Body, 1) | ||||||
| 	} | 	} | ||||||
| @ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string { | |||||||
| 	return result | 	return result | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // TODO: Will this function come in handy somewhere else? | ||||||
|  | /* | ||||||
| // standardizeScheme turns an address like host:https into https://host, | // standardizeScheme turns an address like host:https into https://host, | ||||||
| // or "host:" into "host". | // or "host:" into "host". | ||||||
| func standardizeScheme(addr string) string { | func standardizeScheme(addr string) string { | ||||||
| @ -174,12 +172,13 @@ func standardizeScheme(addr string) string { | |||||||
| 	} | 	} | ||||||
| 	return strings.TrimSuffix(addr, ":") | 	return strings.TrimSuffix(addr, ":") | ||||||
| } | } | ||||||
|  | */ | ||||||
| 
 | 
 | ||||||
| // Caddyfile encapsulates a slice of ServerBlocks. | // EncodedCaddyfile encapsulates a slice of EncodedServerBlocks. | ||||||
| type Caddyfile []ServerBlock | type EncodedCaddyfile []EncodedServerBlock | ||||||
| 
 | 
 | ||||||
| // ServerBlock represents a server block. | // EncodedServerBlock represents a server block ripe for encoding. | ||||||
| type ServerBlock struct { | type EncodedServerBlock struct { | ||||||
| 	Hosts []string        `json:"hosts"` | 	Keys []string        `json:"keys"` | ||||||
| 	Body [][]interface{} `json:"body"` | 	Body [][]interface{} `json:"body"` | ||||||
| } | } | ||||||
| @ -9,7 +9,7 @@ var tests = []struct { | |||||||
| 		caddyfile: `foo { | 		caddyfile: `foo { | ||||||
| 	root /bar | 	root /bar | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`, | 		json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 1 | 	{ // 1 | ||||||
| 		caddyfile: `host1, host2 { | 		caddyfile: `host1, host2 { | ||||||
| @ -17,7 +17,7 @@ var tests = []struct { | |||||||
| 		def | 		def | ||||||
| 	} | 	} | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`, | 		json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 2 | 	{ // 2 | ||||||
| 		caddyfile: `host1, host2 { | 		caddyfile: `host1, host2 { | ||||||
| @ -26,58 +26,58 @@ var tests = []struct { | |||||||
| 		jkl | 		jkl | ||||||
| 	} | 	} | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`, | 		json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 3 | 	{ // 3 | ||||||
| 		caddyfile: `host1:1234, host2:5678 { | 		caddyfile: `host1:1234, host2:5678 { | ||||||
| 	dir abc { | 	dir abc { | ||||||
| 	} | 	} | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`, | 		json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 4 | 	{ // 4 | ||||||
| 		caddyfile: `host { | 		caddyfile: `host { | ||||||
| 	foo "bar baz" | 	foo "bar baz" | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`, | 		json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 5 | 	{ // 5 | ||||||
| 		caddyfile: `host, host:80 { | 		caddyfile: `host, host:80 { | ||||||
| 	foo "bar \"baz\"" | 	foo "bar \"baz\"" | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`, | 		json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 6 | 	{ // 6 | ||||||
| 		caddyfile: `host { | 		caddyfile: `host { | ||||||
| 	foo "bar | 	foo "bar | ||||||
| baz" | baz" | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`, | 		json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 7 | 	{ // 7 | ||||||
| 		caddyfile: `host { | 		caddyfile: `host { | ||||||
| 	dir 123 4.56 true | 	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 | 	{ // 8 | ||||||
| 		caddyfile: `http://host, https://host { | 		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 | 	{ // 9 | ||||||
| 		caddyfile: `host { | 		caddyfile: `host { | ||||||
| 	dir1 a b | 	dir1 a b | ||||||
| 	dir2 c d | 	dir2 c d | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`, | 		json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 10 | 	{ // 10 | ||||||
| 		caddyfile: `host { | 		caddyfile: `host { | ||||||
| 	dir a b | 	dir a b | ||||||
| 	dir c d | 	dir c d | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`, | 		json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 11 | 	{ // 11 | ||||||
| 		caddyfile: `host { | 		caddyfile: `host { | ||||||
| @ -87,7 +87,7 @@ baz" | |||||||
| 		d | 		d | ||||||
| 	} | 	} | ||||||
| }`, | }`, | ||||||
| 		json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`, | 		json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`, | ||||||
| 	}, | 	}, | ||||||
| 	{ // 12 | 	{ // 12 | ||||||
| 		caddyfile: `host1 { | 		caddyfile: `host1 { | ||||||
| @ -97,7 +97,7 @@ baz" | |||||||
| host2 { | host2 { | ||||||
| 	dir2 | 	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) { | func TestStandardizeAddress(t *testing.T) { | ||||||
| 	// host:https should be converted to https://host | 	// host:https should be converted to https://host | ||||||
| 	output, err := ToJSON([]byte(`host:https`)) | 	output, err := ToJSON([]byte(`host:https`)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		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) | 		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 { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		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) | 		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 { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) { | |||||||
| 		t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual) | 		t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | */ | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package parse | package caddyfile | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| @ -13,15 +13,15 @@ type ( | |||||||
| 	// in quotes if it contains whitespace. | 	// in quotes if it contains whitespace. | ||||||
| 	lexer struct { | 	lexer struct { | ||||||
| 		reader *bufio.Reader | 		reader *bufio.Reader | ||||||
| 		token  token | 		token  Token | ||||||
| 		line   int | 		line   int | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// token represents a single parsable unit. | 	// Token represents a single parsable unit. | ||||||
| 	token struct { | 	Token struct { | ||||||
| 		file string | 		File string | ||||||
| 		line int | 		Line int | ||||||
| 		text string | 		Text string | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -47,7 +47,7 @@ func (l *lexer) next() bool { | |||||||
| 	var comment, quoted, escaped bool | 	var comment, quoted, escaped bool | ||||||
| 
 | 
 | ||||||
| 	makeToken := func() bool { | 	makeToken := func() bool { | ||||||
| 		l.token.text = string(val) | 		l.token.Text = string(val) | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -110,7 +110,7 @@ func (l *lexer) next() bool { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if len(val) == 0 { | 		if len(val) == 0 { | ||||||
| 			l.token = token{line: l.line} | 			l.token = Token{Line: l.line} | ||||||
| 			if ch == '"' { | 			if ch == '"' { | ||||||
| 				quoted = true | 				quoted = true | ||||||
| 				continue | 				continue | ||||||
							
								
								
									
										165
									
								
								caddyfile/lexer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								caddyfile/lexer_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | |||||||
|  | package caddyfile | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type lexerTestCase struct { | ||||||
|  | 	input    string | ||||||
|  | 	expected []Token | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestLexer(t *testing.T) { | ||||||
|  | 	testCases := []lexerTestCase{ | ||||||
|  | 		{ | ||||||
|  | 			input: `host:123`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "host:123"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `host:123 | ||||||
|  | 
 | ||||||
|  | 					directive`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "host:123"}, | ||||||
|  | 				{Line: 3, Text: "directive"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `host:123 { | ||||||
|  | 						directive | ||||||
|  | 					}`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "host:123"}, | ||||||
|  | 				{Line: 1, Text: "{"}, | ||||||
|  | 				{Line: 2, Text: "directive"}, | ||||||
|  | 				{Line: 3, Text: "}"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `host:123 { directive }`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "host:123"}, | ||||||
|  | 				{Line: 1, Text: "{"}, | ||||||
|  | 				{Line: 1, Text: "directive"}, | ||||||
|  | 				{Line: 1, Text: "}"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `host:123 { | ||||||
|  | 						#comment | ||||||
|  | 						directive | ||||||
|  | 						# comment | ||||||
|  | 						foobar # another comment | ||||||
|  | 					}`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "host:123"}, | ||||||
|  | 				{Line: 1, Text: "{"}, | ||||||
|  | 				{Line: 3, Text: "directive"}, | ||||||
|  | 				{Line: 5, Text: "foobar"}, | ||||||
|  | 				{Line: 6, Text: "}"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `a "quoted value" b | ||||||
|  | 					foobar`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "a"}, | ||||||
|  | 				{Line: 1, Text: "quoted value"}, | ||||||
|  | 				{Line: 1, Text: "b"}, | ||||||
|  | 				{Line: 2, Text: "foobar"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `A "quoted \"value\" inside" B`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "A"}, | ||||||
|  | 				{Line: 1, Text: `quoted "value" inside`}, | ||||||
|  | 				{Line: 1, Text: "B"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `"don't\escape"`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: `don't\escape`}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `"don't\\escape"`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: `don't\\escape`}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `A "quoted value with line | ||||||
|  | 					break inside" { | ||||||
|  | 						foobar | ||||||
|  | 					}`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "A"}, | ||||||
|  | 				{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"}, | ||||||
|  | 				{Line: 2, Text: "{"}, | ||||||
|  | 				{Line: 3, Text: "foobar"}, | ||||||
|  | 				{Line: 4, Text: "}"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `"C:\php\php-cgi.exe"`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: `C:\php\php-cgi.exe`}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: `empty "" string`, | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: `empty`}, | ||||||
|  | 				{Line: 1, Text: ``}, | ||||||
|  | 				{Line: 1, Text: `string`}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input: "skip those\r\nCR characters", | ||||||
|  | 			expected: []Token{ | ||||||
|  | 				{Line: 1, Text: "skip"}, | ||||||
|  | 				{Line: 1, Text: "those"}, | ||||||
|  | 				{Line: 2, Text: "CR"}, | ||||||
|  | 				{Line: 2, Text: "characters"}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i, testCase := range testCases { | ||||||
|  | 		actual := tokenize(testCase.input) | ||||||
|  | 		lexerCompare(t, i, testCase.expected, actual) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func tokenize(input string) (tokens []Token) { | ||||||
|  | 	l := lexer{} | ||||||
|  | 	l.load(strings.NewReader(input)) | ||||||
|  | 	for l.next() { | ||||||
|  | 		tokens = append(tokens, l.token) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func lexerCompare(t *testing.T, n int, expected, actual []Token) { | ||||||
|  | 	if len(expected) != len(actual) { | ||||||
|  | 		t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i := 0; i < len(actual) && i < len(expected); i++ { | ||||||
|  | 		if actual[i].Line != expected[i].Line { | ||||||
|  | 			t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", | ||||||
|  | 				n, i, expected[i].Text, expected[i].Line, actual[i].Line) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		if actual[i].Text != expected[i].Text { | ||||||
|  | 			t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", | ||||||
|  | 				n, i, expected[i].Text, actual[i].Text) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -1,18 +1,41 @@ | |||||||
| package parse | package caddyfile | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"io" | ||||||
| 	"net" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"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 { | type parser struct { | ||||||
| 	Dispenser | 	Dispenser | ||||||
| 	block           ServerBlock // current server block being parsed | 	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 | 	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) { | func (p *parser) parseAll() ([]ServerBlock, error) { | ||||||
| @ -23,7 +46,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return blocks, err | 			return blocks, err | ||||||
| 		} | 		} | ||||||
| 		if len(p.block.Addresses) > 0 { | 		if len(p.block.Keys) > 0 { | ||||||
| 			blocks = append(blocks, p.block) | 			blocks = append(blocks, p.block) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -32,7 +55,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *parser) parseOne() 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() | 	err := p.begin() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -89,7 +112,7 @@ func (p *parser) addresses() error { | |||||||
| 			break | 			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 | 			// Trailing comma indicates another address will follow, which | ||||||
| 			// may possibly be on the next line | 			// may possibly be on the next line | ||||||
| 			if tkn[len(tkn)-1] == ',' { | 			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 | 				expectingAnother = false // but we may still see another one on this line | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// Parse and save this address | 			p.block.Keys = append(p.block.Keys, tkn) | ||||||
| 			addr, err := standardAddress(tkn) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			p.block.Addresses = append(p.block.Addresses, addr) |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Advance token and possibly break out of loop or return error | 		// 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:] | 	tokensAfter := p.tokens[p.cursor+1:] | ||||||
| 
 | 
 | ||||||
| 	// collect all the imported tokens | 	// collect all the imported tokens | ||||||
| 	var importedTokens []token | 	var importedTokens []Token | ||||||
| 	for _, importFile := range matches { | 	for _, importFile := range matches { | ||||||
| 		newTokens, err := p.doSingleImport(importFile) | 		newTokens, err := p.doSingleImport(importFile) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -226,7 +243,7 @@ func (p *parser) doImport() error { | |||||||
| 
 | 
 | ||||||
| // doSingleImport lexes the individual file at importFile and returns | // doSingleImport lexes the individual file at importFile and returns | ||||||
| // its tokens or an error, if any. | // 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) | 	file, err := os.Open(importFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, p.Errf("Could not import %s: %v", importFile, err) | 		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 | 	// Tack the filename onto these tokens so errors show the imported file's name | ||||||
| 	filename := filepath.Base(importFile) | 	filename := filepath.Base(importFile) | ||||||
| 	for i := 0; i < len(importedTokens); i++ { | 	for i := 0; i < len(importedTokens); i++ { | ||||||
| 		importedTokens[i].file = filename | 		importedTokens[i].File = filename | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return importedTokens, nil | 	return importedTokens, nil | ||||||
| @ -253,11 +270,10 @@ func (p *parser) directive() error { | |||||||
| 	dir := p.Val() | 	dir := p.Val() | ||||||
| 	nesting := 0 | 	nesting := 0 | ||||||
| 
 | 
 | ||||||
| 	if p.checkDirectives { | 	// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type") | ||||||
| 		if _, ok := ValidDirectives[dir]; !ok { | 	if !p.validDirective(dir) { | ||||||
| 		return p.Errf("Unknown directive '%s'", dir) | 		return p.Errf("Unknown directive '%s'", dir) | ||||||
| 	} | 	} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// The directive itself is appended as a relevant token | 	// The directive itself is appended as a relevant token | ||||||
| 	p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) | 	p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) | ||||||
| @ -273,7 +289,7 @@ func (p *parser) directive() error { | |||||||
| 		} else if p.Val() == "}" && nesting == 0 { | 		} else if p.Val() == "}" && nesting == 0 { | ||||||
| 			return p.Err("Unexpected '}' because no matching opening brace") | 			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]) | 		p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -305,63 +321,17 @@ func (p *parser) closeCurlyBrace() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // standardAddress parses an address string into a structured format with separate | // validDirective returns true if dir is in p.validDirectives. | ||||||
| // scheme, host, and port portions, as well as the original input string. | func (p *parser) validDirective(dir string) bool { | ||||||
| func standardAddress(str string) (address, error) { | 	if p.validDirectives == nil { | ||||||
| 	var scheme string | 		return true | ||||||
| 	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:] |  | ||||||
| 	} | 	} | ||||||
| 
 | 	for _, d := range p.validDirectives { | ||||||
| 	// separate host and port | 		if d == dir { | ||||||
| 	host, port, err := net.SplitHostPort(str) | 			return true | ||||||
| 	if err != nil { |  | ||||||
| 		host, port, err = net.SplitHostPort(str + ":") |  | ||||||
| 		if err != nil { |  | ||||||
| 			host = str |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 	return false | ||||||
| 	// "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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // replaceEnvVars replaces environment variables that appear in the token | // replaceEnvVars replaces environment variables that appear in the token | ||||||
| @ -389,27 +359,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string { | |||||||
| 	return s | 	return s | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ( | // ServerBlock associates any number of keys (usually addresses | ||||||
| 	// ServerBlock associates tokens with a list of addresses | // of some sort) with tokens (grouped by directive name). | ||||||
| 	// and groups tokens by directive name. | type ServerBlock struct { | ||||||
| 	ServerBlock struct { | 	Keys   []string | ||||||
| 		Addresses []address | 	Tokens map[string][]Token | ||||||
| 		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 |  | ||||||
| } | } | ||||||
							
								
								
									
										399
									
								
								caddyfile/parse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								caddyfile/parse_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,399 @@ | |||||||
|  | package caddyfile | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestAllTokens(t *testing.T) { | ||||||
|  | 	input := strings.NewReader("a b c\nd e") | ||||||
|  | 	expected := []string{"a", "b", "c", "d", "e"} | ||||||
|  | 	tokens := allTokens(input) | ||||||
|  | 
 | ||||||
|  | 	if len(tokens) != len(expected) { | ||||||
|  | 		t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i, val := range expected { | ||||||
|  | 		if tokens[i].Text != val { | ||||||
|  | 			t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestParseOneAndImport(t *testing.T) { | ||||||
|  | 	testParseOne := func(input string) (ServerBlock, error) { | ||||||
|  | 		p := testParser(input) | ||||||
|  | 		p.Next() // parseOne doesn't call Next() to start, so we must | ||||||
|  | 		err := p.parseOne() | ||||||
|  | 		return p.block, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for i, test := range []struct { | ||||||
|  | 		input     string | ||||||
|  | 		shouldErr bool | ||||||
|  | 		keys      []string | ||||||
|  | 		tokens    map[string]int // map of directive name to number of tokens expected | ||||||
|  | 	}{ | ||||||
|  | 		{`localhost`, false, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost | ||||||
|  | 		  dir1`, false, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 1, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost:1234 | ||||||
|  | 		  dir1 foo bar`, false, []string{ | ||||||
|  | 			"localhost:1234", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost { | ||||||
|  | 		    dir1 | ||||||
|  | 		  }`, false, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 1, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost:1234 { | ||||||
|  | 		    dir1 foo bar | ||||||
|  | 		    dir2 | ||||||
|  | 		  }`, false, []string{ | ||||||
|  | 			"localhost:1234", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 			"dir2": 1, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://localhost https://localhost | ||||||
|  | 		  dir1 foo bar`, false, []string{ | ||||||
|  | 			"http://localhost", | ||||||
|  | 			"https://localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://localhost https://localhost { | ||||||
|  | 		    dir1 foo bar | ||||||
|  | 		  }`, false, []string{ | ||||||
|  | 			"http://localhost", | ||||||
|  | 			"https://localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://localhost, https://localhost { | ||||||
|  | 		    dir1 foo bar | ||||||
|  | 		  }`, false, []string{ | ||||||
|  | 			"http://localhost", | ||||||
|  | 			"https://localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://localhost, { | ||||||
|  | 		  }`, true, []string{ | ||||||
|  | 			"http://localhost", | ||||||
|  | 		}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{`host1:80, http://host2.com | ||||||
|  | 		  dir1 foo bar | ||||||
|  | 		  dir2 baz`, false, []string{ | ||||||
|  | 			"host1:80", | ||||||
|  | 			"http://host2.com", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 			"dir2": 2, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://host1.com, | ||||||
|  | 		  http://host2.com, | ||||||
|  | 		  https://host3.com`, false, []string{ | ||||||
|  | 			"http://host1.com", | ||||||
|  | 			"http://host2.com", | ||||||
|  | 			"https://host3.com", | ||||||
|  | 		}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://host1.com:1234, https://host2.com | ||||||
|  | 		  dir1 foo { | ||||||
|  | 		    bar baz | ||||||
|  | 		  } | ||||||
|  | 		  dir2`, false, []string{ | ||||||
|  | 			"http://host1.com:1234", | ||||||
|  | 			"https://host2.com", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 6, | ||||||
|  | 			"dir2": 1, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`127.0.0.1 | ||||||
|  | 		  dir1 { | ||||||
|  | 		    bar baz | ||||||
|  | 		  } | ||||||
|  | 		  dir2 { | ||||||
|  | 		    foo bar | ||||||
|  | 		  }`, false, []string{ | ||||||
|  | 			"127.0.0.1", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 5, | ||||||
|  | 			"dir2": 5, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost | ||||||
|  | 		  dir1 { | ||||||
|  | 		    foo`, true, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost | ||||||
|  | 		  dir1 { | ||||||
|  | 		  }`, false, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost | ||||||
|  | 		  dir1 { | ||||||
|  | 		  } }`, true, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost | ||||||
|  | 		  dir1 { | ||||||
|  | 		    nested { | ||||||
|  | 		      foo | ||||||
|  | 		    } | ||||||
|  | 		  } | ||||||
|  | 		  dir2 foo bar`, false, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 7, | ||||||
|  | 			"dir2": 3, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{``, false, []string{}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost | ||||||
|  | 		  dir1 arg1 | ||||||
|  | 		  import testdata/import_test1.txt`, false, []string{ | ||||||
|  | 			"localhost", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 2, | ||||||
|  | 			"dir2": 3, | ||||||
|  | 			"dir3": 1, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`import testdata/import_test2.txt`, false, []string{ | ||||||
|  | 			"host1", | ||||||
|  | 		}, map[string]int{ | ||||||
|  | 			"dir1": 1, | ||||||
|  | 			"dir2": 2, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{`import testdata/not_found.txt`, true, []string{}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{`""`, false, []string{}, map[string]int{}}, | ||||||
|  | 
 | ||||||
|  | 		{``, false, []string{}, map[string]int{}}, | ||||||
|  | 	} { | ||||||
|  | 		result, err := testParseOne(test.input) | ||||||
|  | 
 | ||||||
|  | 		if test.shouldErr && err == nil { | ||||||
|  | 			t.Errorf("Test %d: Expected an error, but didn't get one", i) | ||||||
|  | 		} | ||||||
|  | 		if !test.shouldErr && err != nil { | ||||||
|  | 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(result.Keys) != len(test.keys) { | ||||||
|  | 			t.Errorf("Test %d: Expected %d keys, got %d", | ||||||
|  | 				i, len(test.keys), len(result.Keys)) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for j, addr := range result.Keys { | ||||||
|  | 			if addr != test.keys[j] { | ||||||
|  | 				t.Errorf("Test %d, key %d: Expected '%s', but was '%s'", | ||||||
|  | 					i, j, test.keys[j], addr) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(result.Tokens) != len(test.tokens) { | ||||||
|  | 			t.Errorf("Test %d: Expected %d directives, had %d", | ||||||
|  | 				i, len(test.tokens), len(result.Tokens)) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for directive, tokens := range result.Tokens { | ||||||
|  | 			if len(tokens) != test.tokens[directive] { | ||||||
|  | 				t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d", | ||||||
|  | 					i, directive, test.tokens[directive], len(tokens)) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestParseAll(t *testing.T) { | ||||||
|  | 	for i, test := range []struct { | ||||||
|  | 		input     string | ||||||
|  | 		shouldErr bool | ||||||
|  | 		keys      [][]string // keys per server block, in order | ||||||
|  | 	}{ | ||||||
|  | 		{`localhost`, false, [][]string{ | ||||||
|  | 			{"localhost"}, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost:1234`, false, [][]string{ | ||||||
|  | 			{"localhost:1234"}, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost:1234 { | ||||||
|  | 		  } | ||||||
|  | 		  localhost:2015 { | ||||||
|  | 		  }`, false, [][]string{ | ||||||
|  | 			{"localhost:1234"}, | ||||||
|  | 			{"localhost:2015"}, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost:1234, http://host2`, false, [][]string{ | ||||||
|  | 			{"localhost:1234", "http://host2"}, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`localhost:1234, http://host2,`, true, [][]string{}}, | ||||||
|  | 
 | ||||||
|  | 		{`http://host1.com, http://host2.com { | ||||||
|  | 		  } | ||||||
|  | 		  https://host3.com, https://host4.com { | ||||||
|  | 		  }`, false, [][]string{ | ||||||
|  | 			{"http://host1.com", "http://host2.com"}, | ||||||
|  | 			{"https://host3.com", "https://host4.com"}, | ||||||
|  | 		}}, | ||||||
|  | 
 | ||||||
|  | 		{`import testdata/import_glob*.txt`, false, [][]string{ | ||||||
|  | 			{"glob0.host0"}, | ||||||
|  | 			{"glob0.host1"}, | ||||||
|  | 			{"glob1.host0"}, | ||||||
|  | 			{"glob2.host0"}, | ||||||
|  | 		}}, | ||||||
|  | 	} { | ||||||
|  | 		p := testParser(test.input) | ||||||
|  | 		blocks, err := p.parseAll() | ||||||
|  | 
 | ||||||
|  | 		if test.shouldErr && err == nil { | ||||||
|  | 			t.Errorf("Test %d: Expected an error, but didn't get one", i) | ||||||
|  | 		} | ||||||
|  | 		if !test.shouldErr && err != nil { | ||||||
|  | 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(blocks) != len(test.keys) { | ||||||
|  | 			t.Errorf("Test %d: Expected %d server blocks, got %d", | ||||||
|  | 				i, len(test.keys), len(blocks)) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for j, block := range blocks { | ||||||
|  | 			if len(block.Keys) != len(test.keys[j]) { | ||||||
|  | 				t.Errorf("Test %d: Expected %d keys in block %d, got %d", | ||||||
|  | 					i, len(test.keys[j]), j, len(block.Keys)) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			for k, addr := range block.Keys { | ||||||
|  | 				if addr != test.keys[j][k] { | ||||||
|  | 					t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'", | ||||||
|  | 						i, j, k, test.keys[j][k], addr) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestEnvironmentReplacement(t *testing.T) { | ||||||
|  | 	os.Setenv("PORT", "8080") | ||||||
|  | 	os.Setenv("ADDRESS", "servername.com") | ||||||
|  | 	os.Setenv("FOOBAR", "foobar") | ||||||
|  | 
 | ||||||
|  | 	// basic test; unix-style env vars | ||||||
|  | 	p := testParser(`{$ADDRESS}`) | ||||||
|  | 	blocks, _ := p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual { | ||||||
|  | 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// multiple vars per token | ||||||
|  | 	p = testParser(`{$ADDRESS}:{$PORT}`) | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { | ||||||
|  | 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// windows-style var and unix style in same token | ||||||
|  | 	p = testParser(`{%ADDRESS%}:{$PORT}`) | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { | ||||||
|  | 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// reverse order | ||||||
|  | 	p = testParser(`{$ADDRESS}:{%PORT%}`) | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { | ||||||
|  | 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// env var in server block body as argument | ||||||
|  | 	p = testParser(":{%PORT%}\ndir1 {$FOOBAR}") | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual { | ||||||
|  | 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual { | ||||||
|  | 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// combined windows env vars in argument | ||||||
|  | 	p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}") | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual { | ||||||
|  | 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// malformed env var (windows) | ||||||
|  | 	p = testParser(":1234\ndir1 {%ADDRESS}") | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual { | ||||||
|  | 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// malformed (non-existent) env var (unix) | ||||||
|  | 	p = testParser(`:{$PORT$}`) | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Keys[0], ":"; expected != actual { | ||||||
|  | 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// in quoted field | ||||||
|  | 	p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") | ||||||
|  | 	blocks, _ = p.parseAll() | ||||||
|  | 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual { | ||||||
|  | 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func testParser(input string) parser { | ||||||
|  | 	buf := strings.NewReader(input) | ||||||
|  | 	p := parser{Dispenser: NewDispenser("Test", buf)} | ||||||
|  | 	return p | ||||||
|  | } | ||||||
| @ -13,7 +13,7 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
| 	"github.com/jimstudt/http-authentication/basic" | 	"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. | // 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 | // security of HTTP Basic Auth is disputed. Use discretion when deciding | ||||||
| // what to protect with BasicAuth. | // what to protect with BasicAuth. | ||||||
| type BasicAuth struct { | type BasicAuth struct { | ||||||
| 	Next     middleware.Handler | 	Next     httpserver.Handler | ||||||
| 	SiteRoot string | 	SiteRoot string | ||||||
| 	Rules    []Rule | 	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) { | func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||||
| 
 | 
 | ||||||
| 	var hasAuth bool | 	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 _, rule := range a.Rules { | ||||||
| 		for _, res := range rule.Resources { | 		for _, res := range rule.Resources { | ||||||
| 			if !middleware.Path(r.URL.Path).Matches(res) { | 			if !httpserver.Path(r.URL.Path).Matches(res) { | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| @ -10,13 +10,12 @@ import ( | |||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestBasicAuth(t *testing.T) { | func TestBasicAuth(t *testing.T) { | ||||||
| 
 |  | ||||||
| 	rw := BasicAuth{ | 	rw := BasicAuth{ | ||||||
| 		Next: middleware.HandlerFunc(contentHandler), | 		Next: httpserver.HandlerFunc(contentHandler), | ||||||
| 		Rules: []Rule{ | 		Rules: []Rule{ | ||||||
| 			{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}}, | 			{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}}, | ||||||
| 		}, | 		}, | ||||||
| @ -67,7 +66,7 @@ func TestBasicAuth(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| func TestMultipleOverlappingRules(t *testing.T) { | func TestMultipleOverlappingRules(t *testing.T) { | ||||||
| 	rw := BasicAuth{ | 	rw := BasicAuth{ | ||||||
| 		Next: middleware.HandlerFunc(contentHandler), | 		Next: httpserver.HandlerFunc(contentHandler), | ||||||
| 		Rules: []Rule{ | 		Rules: []Rule{ | ||||||
| 			{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}}, | 			{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}}, | ||||||
| 			{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}}, | 			{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}}, | ||||||
| @ -1,43 +1,55 @@ | |||||||
| package setup | package basicauth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/basicauth" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // BasicAuth configures a new BasicAuth middleware instance. | func init() { | ||||||
| func BasicAuth(c *Controller) (middleware.Middleware, error) { | 	caddy.RegisterPlugin(caddy.Plugin{ | ||||||
| 	root := c.Root | 		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) | 	rules, err := basicAuthParse(c) | ||||||
| 	if err != nil { | 	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.Next = next | ||||||
| 		basic.SiteRoot = root | 		basic.SiteRoot = root | ||||||
| 		return basic | 		return basic | ||||||
| 	}, nil | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { | func basicAuthParse(c *caddy.Controller) ([]Rule, error) { | ||||||
| 	var rules []basicauth.Rule | 	var rules []Rule | ||||||
|  | 	cfg := httpserver.GetConfig(c.Key) | ||||||
| 
 | 
 | ||||||
| 	var err error | 	var err error | ||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		var rule basicauth.Rule | 		var rule Rule | ||||||
| 
 | 
 | ||||||
| 		args := c.RemainingArgs() | 		args := c.RemainingArgs() | ||||||
| 
 | 
 | ||||||
| 		switch len(args) { | 		switch len(args) { | ||||||
| 		case 2: | 		case 2: | ||||||
| 			rule.Username = args[0] | 			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) | 				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: | 		case 3: | ||||||
| 			rule.Resources = append(rule.Resources, args[0]) | 			rule.Resources = append(rule.Resources, args[0]) | ||||||
| 			rule.Username = args[1] | 			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) | 				return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) | ||||||
| 			} | 			} | ||||||
| 		default: | 		default: | ||||||
| @ -63,10 +75,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { | |||||||
| 	return rules, nil | 	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=") { | 	if !strings.HasPrefix(passw, "htpasswd=") { | ||||||
| 		return basicauth.PlainMatcher(passw), nil | 		return PlainMatcher(passw), nil | ||||||
| 	} | 	} | ||||||
| 
 | 	return GetHtpasswdMatcher(passw[9:], username, siteRoot) | ||||||
| 	return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot) |  | ||||||
| } | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package setup | package basicauth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @ -7,27 +7,27 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware/basicauth" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestBasicAuth(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 	c := NewTestController(`basicauth user pwd`) | 	err := setup(caddy.NewTestController(`basicauth user pwd`)) | ||||||
| 
 |  | ||||||
| 	mid, err := BasicAuth(c) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Expected no errors, but got: %v", err) | 		t.Errorf("Expected no errors, but got: %v", err) | ||||||
| 	} | 	} | ||||||
| 	if mid == nil { | 	mids := httpserver.GetConfig("").Middleware() | ||||||
| 		t.Fatal("Expected middleware, was nil instead") | 	if len(mids) == 0 { | ||||||
|  | 		t.Fatal("Expected middleware, got 0 instead") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	handler := mid(EmptyNext) | 	handler := mids[0](httpserver.EmptyNext) | ||||||
| 	myHandler, ok := handler.(basicauth.BasicAuth) | 	myHandler, ok := handler.(BasicAuth) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler) | 		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") | 		t.Error("'Next' field of handler was not set properly") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -54,41 +54,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` | |||||||
| 		input     string | 		input     string | ||||||
| 		shouldErr bool | 		shouldErr bool | ||||||
| 		password  string | 		password  string | ||||||
| 		expected  []basicauth.Rule | 		expected  []Rule | ||||||
| 	}{ | 	}{ | ||||||
| 		{`basicauth user pwd`, false, "pwd", []basicauth.Rule{ | 		{`basicauth user pwd`, false, "pwd", []Rule{ | ||||||
| 			{Username: "user"}, | 			{Username: "user"}, | ||||||
| 		}}, | 		}}, | ||||||
| 		{`basicauth user pwd { | 		{`basicauth user pwd { | ||||||
| 		}`, false, "pwd", []basicauth.Rule{ | 		}`, false, "pwd", []Rule{ | ||||||
| 			{Username: "user"}, | 			{Username: "user"}, | ||||||
| 		}}, | 		}}, | ||||||
| 		{`basicauth user pwd { | 		{`basicauth user pwd { | ||||||
| 			/resource1 | 			/resource1 | ||||||
| 			/resource2 | 			/resource2 | ||||||
| 		}`, false, "pwd", []basicauth.Rule{ | 		}`, false, "pwd", []Rule{ | ||||||
| 			{Username: "user", Resources: []string{"/resource1", "/resource2"}}, | 			{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"}}, | 			{Username: "user", Resources: []string{"/resource"}}, | ||||||
| 		}}, | 		}}, | ||||||
| 		{`basicauth /res1 user1 pwd1 | 		{`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: "user1", Resources: []string{"/res1"}}, | ||||||
| 			{Username: "user2", Resources: []string{"/res2"}}, | 			{Username: "user2", Resources: []string{"/res2"}}, | ||||||
| 		}}, | 		}}, | ||||||
| 		{`basicauth user`, true, "", []basicauth.Rule{}}, | 		{`basicauth user`, true, "", []Rule{}}, | ||||||
| 		{`basicauth`, true, "", []basicauth.Rule{}}, | 		{`basicauth`, true, "", []Rule{}}, | ||||||
| 		{`basicauth /resource user pwd asdf`, true, "", []basicauth.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"}, | 			{Username: "sha1"}, | ||||||
| 		}}, | 		}}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		c := NewTestController(test.input) | 		actual, err := basicAuthParse(caddy.NewTestController(test.input)) | ||||||
| 		actual, err := basicAuthParse(c) |  | ||||||
| 
 | 
 | ||||||
| 		if err == nil && test.shouldErr { | 		if err == nil && test.shouldErr { | ||||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | 			t.Errorf("Test %d didn't error, but it should have", i) | ||||||
							
								
								
									
										25
									
								
								caddyhttp/bind/bind.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								caddyhttp/bind/bind.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | package bind | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	caddy.RegisterPlugin(caddy.Plugin{ | ||||||
|  | 		Name:       "bind", | ||||||
|  | 		ServerType: "http", | ||||||
|  | 		Action:     setupBind, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setupBind(c *caddy.Controller) error { | ||||||
|  | 	config := httpserver.GetConfig(c.Key) | ||||||
|  | 	for c.Next() { | ||||||
|  | 		if !c.Args(&config.ListenHost) { | ||||||
|  | 			return c.ArgErr() | ||||||
|  | 		} | ||||||
|  | 		config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309 | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								caddyhttp/bind/bind_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								caddyhttp/bind/bind_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | package bind | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestSetupBind(t *testing.T) { | ||||||
|  | 	err := setupBind(caddy.NewTestController(`bind 1.2.3.4`)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Expected no errors, but got: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	cfg := httpserver.GetConfig("") | ||||||
|  | 	if got, want := cfg.ListenHost, "1.2.3.4"; got != want { | ||||||
|  | 		t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got) | ||||||
|  | 	} | ||||||
|  | 	if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want { | ||||||
|  | 		t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -16,13 +16,14 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/dustin/go-humanize" | 	"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 | // Browse is an http.Handler that can show a file listing when | ||||||
| // directories in the given paths are specified. | // directories in the given paths are specified. | ||||||
| type Browse struct { | type Browse struct { | ||||||
| 	Next          middleware.Handler | 	Next          httpserver.Handler | ||||||
| 	Configs       []Config | 	Configs       []Config | ||||||
| 	IgnoreIndexes bool | 	IgnoreIndexes bool | ||||||
| } | } | ||||||
| @ -67,7 +68,7 @@ type Listing struct { | |||||||
| 	// Optional custom variables for use in browse templates | 	// Optional custom variables for use in browse templates | ||||||
| 	User interface{} | 	User interface{} | ||||||
| 
 | 
 | ||||||
| 	middleware.Context | 	httpserver.Context | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // BreadcrumbMap returns l.Path where every element is a map | // 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 { | 	for _, f := range files { | ||||||
| 		name := f.Name() | 		name := f.Name() | ||||||
| 
 | 
 | ||||||
| 		for _, indexName := range middleware.IndexPages { | 		for _, indexName := range staticfiles.IndexPages { | ||||||
| 			if name == indexName { | 			if name == indexName { | ||||||
| 				hasIndexFile = true | 				hasIndexFile = true | ||||||
| 				break | 				break | ||||||
| @ -237,7 +238,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | |||||||
| 	var bc *Config | 	var bc *Config | ||||||
| 	// See if there's a browse configuration to match the path | 	// See if there's a browse configuration to match the path | ||||||
| 	for i := range b.Configs { | 	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] | 			bc = &b.Configs[i] | ||||||
| 			goto inScope | 			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 | 	if containsIndex && !b.IgnoreIndexes { // directory isn't browsable | ||||||
| 		return b.Next.ServeHTTP(w, r) | 		return b.Next.ServeHTTP(w, r) | ||||||
| 	} | 	} | ||||||
| 	listing.Context = middleware.Context{ | 	listing.Context = httpserver.Context{ | ||||||
| 		Root: bc.Root, | 		Root: bc.Root, | ||||||
| 		Req:  r, | 		Req:  r, | ||||||
| 		URL:  r.URL, | 		URL:  r.URL, | ||||||
| @ -12,20 +12,9 @@ import ( | |||||||
| 	"text/template" | 	"text/template" | ||||||
| 	"time" | 	"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) { | func TestSort(t *testing.T) { | ||||||
| 	// making up []fileInfo with bogus values; | 	// making up []fileInfo with bogus values; | ||||||
| 	// to be used to make up our "listing" | 	// to be used to make up our "listing" | ||||||
| @ -111,7 +100,7 @@ func TestBrowseHTTPMethods(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b := Browse{ | 	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 | 			return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield | ||||||
| 		}), | 		}), | ||||||
| 		Configs: []Config{ | 		Configs: []Config{ | ||||||
| @ -149,7 +138,7 @@ func TestBrowseTemplate(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b := Browse{ | 	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") | 			t.Fatalf("Next shouldn't be called") | ||||||
| 			return 0, nil | 			return 0, nil | ||||||
| 		}), | 		}), | ||||||
| @ -202,9 +191,8 @@ func TestBrowseTemplate(t *testing.T) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestBrowseJson(t *testing.T) { | func TestBrowseJson(t *testing.T) { | ||||||
| 
 |  | ||||||
| 	b := Browse{ | 	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") | 			t.Fatalf("Next shouldn't be called") | ||||||
| 			return 0, nil | 			return 0, nil | ||||||
| 		}), | 		}), | ||||||
| @ -354,3 +342,14 @@ func TestBrowseJson(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // "sort" package has "IsSorted" function, but no "IsReversed"; | ||||||
|  | func isReversed(data sort.Interface) bool { | ||||||
|  | 	n := data.Len() | ||||||
|  | 	for i := n - 1; i > 0; i-- { | ||||||
|  | 		if !data.Less(i, i-1) { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package setup | package browse | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @ -6,32 +6,44 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"text/template" | 	"text/template" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/browse" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Browse configures a new Browse middleware instance. | func init() { | ||||||
| func Browse(c *Controller) (middleware.Middleware, error) { | 	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) | 	configs, err := browseParse(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	browse := browse.Browse{ | 	b := Browse{ | ||||||
| 		Configs:       configs, | 		Configs:       configs, | ||||||
| 		IgnoreIndexes: false, | 		IgnoreIndexes: false, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return func(next middleware.Handler) middleware.Handler { | 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||||
| 		browse.Next = next | 		b.Next = next | ||||||
| 		return browse | 		return b | ||||||
| 	}, nil | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func browseParse(c *Controller) ([]browse.Config, error) { | func browseParse(c *caddy.Controller) ([]Config, error) { | ||||||
| 	var configs []browse.Config | 	var configs []Config | ||||||
| 
 | 
 | ||||||
| 	appendCfg := func(bc browse.Config) error { | 	cfg := httpserver.GetConfig(c.Key) | ||||||
|  | 
 | ||||||
|  | 	appendCfg := func(bc Config) error { | ||||||
| 		for _, c := range configs { | 		for _, c := range configs { | ||||||
| 			if c.PathScope == bc.PathScope { | 			if c.PathScope == bc.PathScope { | ||||||
| 				return fmt.Errorf("duplicate browsing config for %s", c.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() { | 	for c.Next() { | ||||||
| 		var bc browse.Config | 		var bc Config | ||||||
| 
 | 
 | ||||||
| 		// First argument is directory to allow browsing; default is site root | 		// First argument is directory to allow browsing; default is site root | ||||||
| 		if c.NextArg() { | 		if c.NextArg() { | ||||||
| @ -50,7 +62,7 @@ func browseParse(c *Controller) ([]browse.Config, error) { | |||||||
| 		} else { | 		} else { | ||||||
| 			bc.PathScope = "/" | 			bc.PathScope = "/" | ||||||
| 		} | 		} | ||||||
| 		bc.Root = http.Dir(c.Root) | 		bc.Root = http.Dir(cfg.Root) | ||||||
| 		theRoot, err := bc.Root.Open("/") // catch a missing path early | 		theRoot, err := bc.Root.Open("/") // catch a missing path early | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return configs, err | 			return configs, err | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package setup | package browse | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| @ -8,12 +8,13 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware/browse" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestBrowse(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 
 | 	tempDirPath := os.TempDir() | ||||||
| 	tempDirPath, err := getTempDirPath() | 	_, err := os.Stat(tempDirPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) | 		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 | 		// test case #0 tests handling of multiple pathscopes | ||||||
| 		{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false}, | 		{"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}, | 		{"browse /", []string{"/"}, false}, | ||||||
| 
 | 
 | ||||||
| 		// test case #2 tests detectaction of custom template | 		// test case #2 tests detectaction of custom template | ||||||
| @ -48,14 +49,16 @@ func TestBrowse(t *testing.T) { | |||||||
| 		{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true}, | 		{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true}, | ||||||
| 	} { | 	} { | ||||||
| 
 | 
 | ||||||
| 		recievedFunc, err := Browse(NewTestController(test.input)) | 		err := setup(caddy.NewTestController(test.input)) | ||||||
| 		if err != nil && !test.shouldErr { | 		if err != nil && !test.shouldErr { | ||||||
| 			t.Errorf("Test case #%d recieved an error of %v", i, err) | 			t.Errorf("Test case #%d recieved an error of %v", i, err) | ||||||
| 		} | 		} | ||||||
| 		if test.expectedPathScope == nil { | 		if test.expectedPathScope == nil { | ||||||
| 			continue | 			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 { | 		for j, config := range recievedConfigs { | ||||||
| 			if config.PathScope != test.expectedPathScope[j] { | 			if config.PathScope != test.expectedPathScope[j] { | ||||||
| 				t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope) | 				t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope) | ||||||
							
								
								
									
										29
									
								
								caddyhttp/caddyhttp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								caddyhttp/caddyhttp.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | package caddyhttp | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	// plug in the server | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
|  | 
 | ||||||
|  | 	// plug in the standard directives | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/basicauth" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/bind" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/browse" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/errors" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/expvar" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/extensions" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/fastcgi" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/gzip" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/header" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/internalsrv" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/log" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/markdown" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/mime" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/pprof" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/proxy" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/redirect" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/rewrite" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/root" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/templates" | ||||||
|  | 	_ "github.com/mholt/caddy/caddyhttp/websocket" | ||||||
|  | 	_ "github.com/mholt/caddy/startupshutdown" | ||||||
|  | ) | ||||||
| @ -11,16 +11,25 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"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). | // ErrorHandler handles HTTP errors (and errors from other middleware). | ||||||
| type ErrorHandler struct { | type ErrorHandler struct { | ||||||
| 	Next       middleware.Handler | 	Next       httpserver.Handler | ||||||
| 	ErrorPages map[int]string // map of status code to filename | 	ErrorPages map[int]string // map of status code to filename | ||||||
| 	LogFile    string | 	LogFile    string | ||||||
| 	Log        *log.Logger | 	Log        *log.Logger | ||||||
| 	LogRoller  *middleware.LogRoller | 	LogRoller  *httpserver.LogRoller | ||||||
| 	Debug      bool // if true, errors are written out to client rather than to a log | 	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 { | 	if err != nil { | ||||||
| 		errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err) | 		errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err) | ||||||
| 
 |  | ||||||
| 		if h.Debug { | 		if h.Debug { | ||||||
| 			// Write error to response instead of to log | 			// Write error to response instead of to log | ||||||
| 			w.Header().Set("Content-Type", "text/plain; charset=utf-8") | 			w.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||||||
| 			w.WriteHeader(status) | 			w.WriteHeader(status) | ||||||
| 			fmt.Fprintln(w, errMsg) | 			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) | 		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 | // code. If there is an error serving the error page, a plaintext error | ||||||
| // message is written instead, and the extra error is logged. | // message is written instead, and the extra error is logged. | ||||||
| func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) { | 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 | 	// See if an error page for this status code was specified | ||||||
| 	if pagePath, ok := h.ErrorPages[code]; ok { | 	if pagePath, ok := h.ErrorPages[code]; ok { | ||||||
| 
 |  | ||||||
| 		// Try to open it | 		// Try to open it | ||||||
| 		errorPage, err := os.Open(pagePath) | 		errorPage, err := os.Open(pagePath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			// An additional error handling an error... <insert grumpy cat here> | 			// An additional error handling an error... <insert grumpy cat here> | ||||||
| 			h.Log.Printf("%s [NOTICE %d %s] could not load error page: %v", | 			h.Log.Printf("%s [NOTICE %d %s] could not load error page: %v", | ||||||
| 				time.Now().Format(timeFormat), code, r.URL.String(), err) | 				time.Now().Format(timeFormat), code, r.URL.String(), err) | ||||||
| 			http.Error(w, defaultBody, code) | 			httpserver.DefaultErrorFunc(w, r, code) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		defer errorPage.Close() | 		defer errorPage.Close() | ||||||
| @ -79,14 +84,14 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int | |||||||
| 			// Epic fail... sigh. | 			// Epic fail... sigh. | ||||||
| 			h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v", | 			h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v", | ||||||
| 				time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err) | 				time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err) | ||||||
| 			http.Error(w, defaultBody, code) | 			httpserver.DefaultErrorFunc(w, r, code) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Default error response | 	// Default error response | ||||||
| 	http.Error(w, defaultBody, code) | 	httpserver.DefaultErrorFunc(w, r, code) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) { | 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 | 		// Write error and stack trace to the response rather than to a log | ||||||
| 		var stackBuf [4096]byte | 		var stackBuf [4096]byte | ||||||
| 		stack := stackBuf[:runtime.Stack(stackBuf[:], false)] | 		stack := stackBuf[:runtime.Stack(stackBuf[:], false)] | ||||||
| 		w.Header().Set("Content-Type", "text/plain; charset=utf-8") | 		httpserver.WriteTextResponse(w, http.StatusInternalServerError, fmt.Sprintf("%s\n\n%s", panicMsg, stack)) | ||||||
| 		w.WriteHeader(http.StatusInternalServerError) |  | ||||||
| 		fmt.Fprintf(w, "%s\n\n%s", panicMsg, stack) |  | ||||||
| 	} else { | 	} else { | ||||||
| 		// Currently we don't use the function name, since file:line is more conventional | 		// Currently we don't use the function name, since file:line is more conventional | ||||||
| 		h.Log.Printf(panicMsg) | 		h.Log.Printf(panicMsg) | ||||||
| @ -13,7 +13,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestErrors(t *testing.T) { | func TestErrors(t *testing.T) { | ||||||
| @ -44,7 +44,7 @@ func TestErrors(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	testErr := errors.New("test error") | 	testErr := errors.New("test error") | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		next         middleware.Handler | 		next         httpserver.Handler | ||||||
| 		expectedCode int | 		expectedCode int | ||||||
| 		expectedBody string | 		expectedBody string | ||||||
| 		expectedLog  string | 		expectedLog  string | ||||||
| @ -124,7 +124,7 @@ func TestVisibleErrorWithPanic(t *testing.T) { | |||||||
| 	eh := ErrorHandler{ | 	eh := ErrorHandler{ | ||||||
| 		ErrorPages: make(map[int]string), | 		ErrorPages: make(map[int]string), | ||||||
| 		Debug:      true, | 		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) | 			panic(panicMsg) | ||||||
| 		}), | 		}), | ||||||
| 	} | 	} | ||||||
| @ -146,7 +146,7 @@ func TestVisibleErrorWithPanic(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	body := rec.Body.String() | 	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) | 		t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body) | ||||||
| 	} | 	} | ||||||
| 	if !strings.Contains(body, panicMsg) { | 	if !strings.Contains(body, panicMsg) { | ||||||
| @ -157,8 +157,8 @@ func TestVisibleErrorWithPanic(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func genErrorHandler(status int, err error, body string) middleware.Handler { | func genErrorHandler(status int, err error, body string) httpserver.Handler { | ||||||
| 	return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | 	return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||||
| 		if len(body) > 0 { | 		if len(body) > 0 { | ||||||
| 			w.Header().Set("Content-Length", strconv.Itoa(len(body))) | 			w.Header().Set("Content-Length", strconv.Itoa(len(body))) | ||||||
| 			fmt.Fprint(w, body) | 			fmt.Fprint(w, body) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package setup | package errors | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io" | 	"io" | ||||||
| @ -8,19 +8,19 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/hashicorp/go-syslog" | 	"github.com/hashicorp/go-syslog" | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/errors" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Errors configures a new errors middleware instance. | // setup configures a new errors middleware instance. | ||||||
| func Errors(c *Controller) (middleware.Middleware, error) { | func setup(c *caddy.Controller) error { | ||||||
| 	handler, err := errorsParse(c) | 	handler, err := errorsParse(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Open the log file for writing when the server starts | 	// 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 err error | ||||||
| 		var writer io.Writer | 		var writer io.Writer | ||||||
| 
 | 
 | ||||||
| @ -62,17 +62,21 @@ func Errors(c *Controller) (middleware.Middleware, error) { | |||||||
| 		return nil | 		return nil | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	return func(next middleware.Handler) middleware.Handler { | 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||||
| 		handler.Next = next | 		handler.Next = next | ||||||
| 		return handler | 		return handler | ||||||
| 	}, nil | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func errorsParse(c *Controller) (*errors.ErrorHandler, error) { | func errorsParse(c *caddy.Controller) (*ErrorHandler, error) { | ||||||
| 	// Very important that we make a pointer because the Startup | 	// Very important that we make a pointer because the startup | ||||||
| 	// function that opens the log file must have access to the | 	// function that opens the log file must have access to the | ||||||
| 	// same instance of the handler, not a copy. | 	// 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) { | 	optionalBlock := func() (bool, error) { | ||||||
| 		var hadBlock bool | 		var hadBlock bool | ||||||
| @ -94,7 +98,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { | |||||||
| 					if c.NextArg() { | 					if c.NextArg() { | ||||||
| 						if c.Val() == "{" { | 						if c.Val() == "{" { | ||||||
| 							c.IncrNest() | 							c.IncrNest() | ||||||
| 							logRoller, err := parseRoller(c) | 							logRoller, err := httpserver.ParseRoller(c) | ||||||
| 							if err != nil { | 							if err != nil { | ||||||
| 								return hadBlock, err | 								return hadBlock, err | ||||||
| 							} | 							} | ||||||
| @ -104,7 +108,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { | |||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				// Error page; ensure it exists | 				// Error page; ensure it exists | ||||||
| 				where = filepath.Join(c.Root, where) | 				where = filepath.Join(cfg.Root, where) | ||||||
| 				f, err := os.Open(where) | 				f, err := os.Open(where) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					log.Printf("[WARNING] Unable to open error page '%s': %v", where, err) | 					log.Printf("[WARNING] Unable to open error page '%s': %v", where, err) | ||||||
| @ -1,27 +1,24 @@ | |||||||
| package setup | package errors | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/errors" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestErrors(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 	c := NewTestController(`errors`) | 	err := setup(caddy.NewTestController(`errors`)) | ||||||
| 	mid, err := Errors(c) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Expected no errors, got: %v", err) | 		t.Errorf("Expected no errors, got: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 	mids := httpserver.GetConfig("").Middleware() | ||||||
| 	if mid == nil { | 	if len(mids) == 0 { | ||||||
| 		t.Fatal("Expected middleware, was nil instead") | 		t.Fatal("Expected middlewares, was nil instead") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	handler := mid(EmptyNext) | 	handler := mids[0](httpserver.EmptyNext) | ||||||
| 	myHandler, ok := handler.(*errors.ErrorHandler) | 	myHandler, ok := handler.(*ErrorHandler) | ||||||
| 
 |  | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler) | 		t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler) | ||||||
| 	} | 	} | ||||||
| @ -32,53 +29,53 @@ func TestErrors(t *testing.T) { | |||||||
| 	if myHandler.LogRoller != nil { | 	if myHandler.LogRoller != nil { | ||||||
| 		t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller) | 		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") | 		t.Error("'Next' field of handler was not set properly") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Test Startup function | 	// Test Startup function -- TODO | ||||||
| 	if len(c.Startup) == 0 { | 	// if len(c.Startup) == 0 { | ||||||
| 		t.Fatal("Expected 1 startup function, had 0") | 	// 	t.Fatal("Expected 1 startup function, had 0") | ||||||
| 	} | 	// } | ||||||
| 	c.Startup[0]() | 	// c.Startup[0]() | ||||||
| 	if myHandler.Log == nil { | 	// if myHandler.Log == nil { | ||||||
| 		t.Error("Expected Log to be non-nil after startup because Debug is not enabled") | 	// 	t.Error("Expected Log to be non-nil after startup because Debug is not enabled") | ||||||
| 	} | 	// } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestErrorsParse(t *testing.T) { | func TestErrorsParse(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		inputErrorsRules     string | 		inputErrorsRules     string | ||||||
| 		shouldErr            bool | 		shouldErr            bool | ||||||
| 		expectedErrorHandler errors.ErrorHandler | 		expectedErrorHandler ErrorHandler | ||||||
| 	}{ | 	}{ | ||||||
| 		{`errors`, false, errors.ErrorHandler{ | 		{`errors`, false, ErrorHandler{ | ||||||
| 			LogFile: "", | 			LogFile: "", | ||||||
| 		}}, | 		}}, | ||||||
| 		{`errors errors.txt`, false, errors.ErrorHandler{ | 		{`errors errors.txt`, false, ErrorHandler{ | ||||||
| 			LogFile: "errors.txt", | 			LogFile: "errors.txt", | ||||||
| 		}}, | 		}}, | ||||||
| 		{`errors visible`, false, errors.ErrorHandler{ | 		{`errors visible`, false, ErrorHandler{ | ||||||
| 			LogFile: "", | 			LogFile: "", | ||||||
| 			Debug:   true, | 			Debug:   true, | ||||||
| 		}}, | 		}}, | ||||||
| 		{`errors { log visible }`, false, errors.ErrorHandler{ | 		{`errors { log visible }`, false, ErrorHandler{ | ||||||
| 			LogFile: "", | 			LogFile: "", | ||||||
| 			Debug:   true, | 			Debug:   true, | ||||||
| 		}}, | 		}}, | ||||||
| 		{`errors { log errors.txt | 		{`errors { log errors.txt | ||||||
|         404 404.html |         404 404.html | ||||||
|         500 500.html |         500 500.html | ||||||
| }`, false, errors.ErrorHandler{ | }`, false, ErrorHandler{ | ||||||
| 			LogFile: "errors.txt", | 			LogFile: "errors.txt", | ||||||
| 			ErrorPages: map[int]string{ | 			ErrorPages: map[int]string{ | ||||||
| 				404: "404.html", | 				404: "404.html", | ||||||
| 				500: "500.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", | 			LogFile: "errors.txt", | ||||||
| 			LogRoller: &middleware.LogRoller{ | 			LogRoller: &httpserver.LogRoller{ | ||||||
| 				MaxSize:    2, | 				MaxSize:    2, | ||||||
| 				MaxAge:     10, | 				MaxAge:     10, | ||||||
| 				MaxBackups: 3, | 				MaxBackups: 3, | ||||||
| @ -92,13 +89,13 @@ func TestErrorsParse(t *testing.T) { | |||||||
|         } |         } | ||||||
|         404 404.html |         404 404.html | ||||||
|         503 503.html |         503 503.html | ||||||
| }`, false, errors.ErrorHandler{ | }`, false, ErrorHandler{ | ||||||
| 			LogFile: "errors.txt", | 			LogFile: "errors.txt", | ||||||
| 			ErrorPages: map[int]string{ | 			ErrorPages: map[int]string{ | ||||||
| 				404: "404.html", | 				404: "404.html", | ||||||
| 				503: "503.html", | 				503: "503.html", | ||||||
| 			}, | 			}, | ||||||
| 			LogRoller: &middleware.LogRoller{ | 			LogRoller: &httpserver.LogRoller{ | ||||||
| 				MaxSize:    3, | 				MaxSize:    3, | ||||||
| 				MaxAge:     11, | 				MaxAge:     11, | ||||||
| 				MaxBackups: 5, | 				MaxBackups: 5, | ||||||
| @ -107,8 +104,7 @@ func TestErrorsParse(t *testing.T) { | |||||||
| 		}}, | 		}}, | ||||||
| 	} | 	} | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		c := NewTestController(test.inputErrorsRules) | 		actualErrorsRule, err := errorsParse(caddy.NewTestController(test.inputErrorsRules)) | ||||||
| 		actualErrorsRule, err := errorsParse(c) |  | ||||||
| 
 | 
 | ||||||
| 		if err == nil && test.shouldErr { | 		if err == nil && test.shouldErr { | ||||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | 			t.Errorf("Test %d didn't error, but it should have", i) | ||||||
| @ -5,23 +5,22 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ExpVar is a simple struct to hold expvar's configuration | // ExpVar is a simple struct to hold expvar's configuration | ||||||
| type ExpVar struct { | type ExpVar struct { | ||||||
| 	Next     middleware.Handler | 	Next     httpserver.Handler | ||||||
| 	Resource Resource | 	Resource Resource | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ServeHTTP handles requests to expvar's configured entry point with | // ServeHTTP handles requests to expvar's configured entry point with | ||||||
| // expvar, or passes all other requests up the chain. | // expvar, or passes all other requests up the chain. | ||||||
| func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | 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) | 		expvarHandler(w, r) | ||||||
| 		return 0, nil | 		return 0, nil | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	return e.Next.ServeHTTP(w, r) | 	return e.Next.ServeHTTP(w, r) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -6,12 +6,12 @@ import ( | |||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestExpVar(t *testing.T) { | func TestExpVar(t *testing.T) { | ||||||
| 	rw := ExpVar{ | 	rw := ExpVar{ | ||||||
| 		Next:     middleware.HandlerFunc(contentHandler), | 		Next:     httpserver.HandlerFunc(contentHandler), | ||||||
| 		Resource: "/d/v", | 		Resource: "/d/v", | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
							
								
								
									
										70
									
								
								caddyhttp/expvar/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								caddyhttp/expvar/setup.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | package expvar | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"expvar" | ||||||
|  | 	"runtime" | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	caddy.RegisterPlugin(caddy.Plugin{ | ||||||
|  | 		Name:       "expvar", | ||||||
|  | 		ServerType: "http", | ||||||
|  | 		Action:     setup, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // setup configures a new ExpVar middleware instance. | ||||||
|  | func setup(c *caddy.Controller) error { | ||||||
|  | 	resource, err := expVarParse(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// publish any extra information/metrics we may want to capture | ||||||
|  | 	publishExtraVars() | ||||||
|  | 
 | ||||||
|  | 	ev := ExpVar{Resource: resource} | ||||||
|  | 
 | ||||||
|  | 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||||
|  | 		ev.Next = next | ||||||
|  | 		return ev | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func expVarParse(c *caddy.Controller) (Resource, error) { | ||||||
|  | 	var resource Resource | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	for c.Next() { | ||||||
|  | 		args := c.RemainingArgs() | ||||||
|  | 		switch len(args) { | ||||||
|  | 		case 0: | ||||||
|  | 			resource = Resource(defaultExpvarPath) | ||||||
|  | 		case 1: | ||||||
|  | 			resource = Resource(args[0]) | ||||||
|  | 		default: | ||||||
|  | 			return resource, c.ArgErr() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return resource, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func publishExtraVars() { | ||||||
|  | 	// By using sync.Once instead of an init() function, we don't clutter | ||||||
|  | 	// the app's expvar export unnecessarily, or risk colliding with it. | ||||||
|  | 	publishOnce.Do(func() { | ||||||
|  | 		expvar.Publish("Goroutines", expvar.Func(func() interface{} { | ||||||
|  | 			return runtime.NumGoroutine() | ||||||
|  | 		})) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var publishOnce sync.Once // publishing variables should only be done once | ||||||
|  | var defaultExpvarPath = "/debug/vars" | ||||||
							
								
								
									
										40
									
								
								caddyhttp/expvar/setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								caddyhttp/expvar/setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | package expvar | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestSetup(t *testing.T) { | ||||||
|  | 	err := setup(caddy.NewTestController(`expvar`)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Expected no errors, got: %v", err) | ||||||
|  | 	} | ||||||
|  | 	mids := httpserver.GetConfig("").Middleware() | ||||||
|  | 	if len(mids) == 0 { | ||||||
|  | 		t.Fatal("Expected middleware, got 0 instead") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = setup(caddy.NewTestController(`expvar /d/v`)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Expected no errors, got: %v", err) | ||||||
|  | 	} | ||||||
|  | 	mids = httpserver.GetConfig("").Middleware() | ||||||
|  | 	if len(mids) == 0 { | ||||||
|  | 		t.Fatal("Expected middleware, got 0 instead") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	handler := mids[1](httpserver.EmptyNext) | ||||||
|  | 	myHandler, ok := handler.(ExpVar) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler) | ||||||
|  | 	} | ||||||
|  | 	if myHandler.Resource != "/d/v" { | ||||||
|  | 		t.Errorf("Expected /d/v as expvar resource") | ||||||
|  | 	} | ||||||
|  | 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||||
|  | 		t.Error("'Next' field of handler was not set properly") | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -12,14 +12,14 @@ import ( | |||||||
| 	"path" | 	"path" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Ext can assume an extension from clean URLs. | // Ext can assume an extension from clean URLs. | ||||||
| // It tries extensions in the order listed in Extensions. | // It tries extensions in the order listed in Extensions. | ||||||
| type Ext struct { | type Ext struct { | ||||||
| 	// Next handler in the chain | 	// Next handler in the chain | ||||||
| 	Next middleware.Handler | 	Next httpserver.Handler | ||||||
| 
 | 
 | ||||||
| 	// Path to ther root of the site | 	// Path to ther root of the site | ||||||
| 	Root string | 	Root string | ||||||
| @ -28,7 +28,7 @@ type Ext struct { | |||||||
| 	Extensions []string | 	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) { | func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||||
| 	urlpath := strings.TrimSuffix(r.URL.Path, "/") | 	urlpath := strings.TrimSuffix(r.URL.Path, "/") | ||||||
| 	if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' { | 	if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' { | ||||||
							
								
								
									
										54
									
								
								caddyhttp/extensions/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								caddyhttp/extensions/setup.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | package extensions | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	caddy.RegisterPlugin(caddy.Plugin{ | ||||||
|  | 		Name:       "ext", | ||||||
|  | 		ServerType: "http", | ||||||
|  | 		Action:     setup, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // setup configures a new instance of 'extensions' middleware for clean URLs. | ||||||
|  | func setup(c *caddy.Controller) error { | ||||||
|  | 	cfg := httpserver.GetConfig(c.Key) | ||||||
|  | 	root := cfg.Root | ||||||
|  | 
 | ||||||
|  | 	exts, err := extParse(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||||
|  | 		return Ext{ | ||||||
|  | 			Next:       next, | ||||||
|  | 			Extensions: exts, | ||||||
|  | 			Root:       root, | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // extParse sets up an instance of extension middleware | ||||||
|  | // from a middleware controller and returns a list of extensions. | ||||||
|  | func extParse(c *caddy.Controller) ([]string, error) { | ||||||
|  | 	var exts []string | ||||||
|  | 
 | ||||||
|  | 	for c.Next() { | ||||||
|  | 		// At least one extension is required | ||||||
|  | 		if !c.NextArg() { | ||||||
|  | 			return exts, c.ArgErr() | ||||||
|  | 		} | ||||||
|  | 		exts = append(exts, c.Val()) | ||||||
|  | 
 | ||||||
|  | 		// Tack on any other extensions that may have been listed | ||||||
|  | 		exts = append(exts, c.RemainingArgs()...) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return exts, nil | ||||||
|  | } | ||||||
| @ -1,26 +1,25 @@ | |||||||
| package setup | package extensions | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware/extensions" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestExt(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 	c := NewTestController(`ext .html .htm .php`) | 	err := setup(caddy.NewTestController(`ext .html .htm .php`)) | ||||||
| 
 |  | ||||||
| 	mid, err := Ext(c) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Expected no errors, got: %v", err) | 		t.Fatalf("Expected no errors, got: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if mid == nil { | 	mids := httpserver.GetConfig("").Middleware() | ||||||
| 		t.Fatal("Expected middleware, was nil instead") | 	if len(mids) == 0 { | ||||||
|  | 		t.Fatal("Expected middleware, had 0 instead") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	handler := mid(EmptyNext) | 	handler := mids[0](httpserver.EmptyNext) | ||||||
| 	myHandler, ok := handler.(extensions.Ext) | 	myHandler, ok := handler.(Ext) | ||||||
| 
 | 
 | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		t.Fatalf("Expected handler to be type Ext, got: %#v", handler) | 		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" { | 	if myHandler.Extensions[2] != ".php" { | ||||||
| 		t.Errorf("Expected .php in the list of Extensions") | 		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") | 		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"}}, | 		{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}}, | ||||||
| 	} | 	} | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		c := NewTestController(test.inputExts) | 		actualExts, err := extParse(caddy.NewTestController(test.inputExts)) | ||||||
| 		actualExts, err := extParse(c) |  | ||||||
| 
 | 
 | ||||||
| 		if err == nil && test.shouldErr { | 		if err == nil && test.shouldErr { | ||||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | 			t.Errorf("Test %d didn't error, but it should have", i) | ||||||
							
								
								
									
										14
									
								
								middleware/fastcgi/fastcgi.go → caddyhttp/fastcgi/fastcgi.go
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										14
									
								
								middleware/fastcgi/fastcgi.go → caddyhttp/fastcgi/fastcgi.go
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -13,12 +13,12 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"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. | // Handler is a middleware type that can handle requests as a FastCGI client. | ||||||
| type Handler struct { | type Handler struct { | ||||||
| 	Next    middleware.Handler | 	Next    httpserver.Handler | ||||||
| 	Rules   []Rule | 	Rules   []Rule | ||||||
| 	Root    string | 	Root    string | ||||||
| 	AbsRoot string // same as root, but absolute path | 	AbsRoot string // same as root, but absolute path | ||||||
| @ -31,12 +31,12 @@ type Handler struct { | |||||||
| 	ServerPort      string | 	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) { | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||||
| 	for _, rule := range h.Rules { | 	for _, rule := range h.Rules { | ||||||
| 
 | 
 | ||||||
| 		// First requirement: Base path must match and the path must be allowed. | 		// 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 | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @ -47,7 +47,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) | |||||||
| 
 | 
 | ||||||
| 		fpath := r.URL.Path | 		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 | 			fpath = idx | ||||||
| 			// Index file present. | 			// Index file present. | ||||||
| 			// If request path cannot be split, return error. | 			// 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 | // splitPos returns the index where path should be split | ||||||
| // based on rule.SplitPath. | // based on rule.SplitPath. | ||||||
| func (r Rule) splitPos(path string) int { | func (r Rule) splitPos(path string) int { | ||||||
| 	if middleware.CaseSensitivePath { | 	if httpserver.CaseSensitivePath { | ||||||
| 		return strings.Index(path, r.SplitPath) | 		return strings.Index(path, r.SplitPath) | ||||||
| 	} | 	} | ||||||
| 	return strings.Index(strings.ToLower(path), strings.ToLower(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. | // AllowedPath checks if requestPath is not an ignored path. | ||||||
| func (r Rule) AllowedPath(requestPath string) bool { | func (r Rule) AllowedPath(requestPath string) bool { | ||||||
| 	for _, ignoredSubPath := range r.IgnoredSubPaths { | 	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 | 			return false | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -1,46 +1,57 @@ | |||||||
| package setup | package fastcgi | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/fastcgi" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // FastCGI configures a new FastCGI middleware instance. | func init() { | ||||||
| func FastCGI(c *Controller) (middleware.Middleware, error) { | 	caddy.RegisterPlugin(caddy.Plugin{ | ||||||
| 	absRoot, err := filepath.Abs(c.Root) | 		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 { | 	if err != nil { | ||||||
| 		return nil, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rules, err := fastcgiParse(c) | 	rules, err := fastcgiParse(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return func(next middleware.Handler) middleware.Handler { | 	cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||||
| 		return fastcgi.Handler{ | 		return Handler{ | ||||||
| 			Next:            next, | 			Next:            next, | ||||||
| 			Rules:           rules, | 			Rules:           rules, | ||||||
| 			Root:            c.Root, | 			Root:            cfg.Root, | ||||||
| 			AbsRoot:         absRoot, | 			AbsRoot:         absRoot, | ||||||
| 			FileSys:         http.Dir(c.Root), | 			FileSys:         http.Dir(cfg.Root), | ||||||
| 			SoftwareName:    c.AppName, | 			SoftwareName:    caddy.AppName, | ||||||
| 			SoftwareVersion: c.AppVersion, | 			SoftwareVersion: caddy.AppVersion, | ||||||
| 			ServerName:      c.Host, | 			ServerName:      cfg.Addr.Host, | ||||||
| 			ServerPort:      c.Port, | 			ServerPort:      cfg.Addr.Port, | ||||||
| 		} | 		} | ||||||
| 	}, nil | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) { | func fastcgiParse(c *caddy.Controller) ([]Rule, error) { | ||||||
| 	var rules []fastcgi.Rule | 	var rules []Rule | ||||||
| 
 | 
 | ||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		var rule fastcgi.Rule | 		var rule Rule | ||||||
| 
 | 
 | ||||||
| 		args := c.RemainingArgs() | 		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 | // fastcgiPreset configures rule according to name. It returns an error if | ||||||
| // name is not a recognized preset name. | // name is not a recognized preset name. | ||||||
| func fastcgiPreset(name string, rule *fastcgi.Rule) error { | func fastcgiPreset(name string, rule *Rule) error { | ||||||
| 	switch name { | 	switch name { | ||||||
| 	case "php": | 	case "php": | ||||||
| 		rule.Ext = ".php" | 		rule.Ext = ".php" | ||||||
| @ -1,28 +1,25 @@ | |||||||
| package setup | package fastcgi | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware/fastcgi" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestFastCGI(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 
 | 	err := setup(caddy.NewTestController(`fastcgi / 127.0.0.1:9000`)) | ||||||
| 	c := NewTestController(`fastcgi / 127.0.0.1:9000`) |  | ||||||
| 
 |  | ||||||
| 	mid, err := FastCGI(c) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Expected no errors, got: %v", err) | 		t.Errorf("Expected no errors, got: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 	mids := httpserver.GetConfig("").Middleware() | ||||||
| 	if mid == nil { | 	if len(mids) == 0 { | ||||||
| 		t.Fatal("Expected middleware, was nil instead") | 		t.Fatal("Expected middleware, got 0 instead") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	handler := mid(EmptyNext) | 	handler := mids[0](httpserver.EmptyNext) | ||||||
| 	myHandler, ok := handler.(fastcgi.Handler) | 	myHandler, ok := handler.(Handler) | ||||||
| 
 | 
 | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		t.Fatalf("Expected handler to be type , got: %#v", handler) | 		t.Fatalf("Expected handler to be type , got: %#v", handler) | ||||||
| @ -41,11 +38,11 @@ func TestFastcgiParse(t *testing.T) { | |||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		inputFastcgiConfig    string | 		inputFastcgiConfig    string | ||||||
| 		shouldErr             bool | 		shouldErr             bool | ||||||
| 		expectedFastcgiConfig []fastcgi.Rule | 		expectedFastcgiConfig []Rule | ||||||
| 	}{ | 	}{ | ||||||
| 
 | 
 | ||||||
| 		{`fastcgi /blog 127.0.0.1:9000 php`, | 		{`fastcgi /blog 127.0.0.1:9000 php`, | ||||||
| 			false, []fastcgi.Rule{{ | 			false, []Rule{{ | ||||||
| 				Path:       "/blog", | 				Path:       "/blog", | ||||||
| 				Address:    "127.0.0.1:9000", | 				Address:    "127.0.0.1:9000", | ||||||
| 				Ext:        ".php", | 				Ext:        ".php", | ||||||
| @ -55,7 +52,7 @@ func TestFastcgiParse(t *testing.T) { | |||||||
| 		{`fastcgi / 127.0.0.1:9001 { | 		{`fastcgi / 127.0.0.1:9001 { | ||||||
| 	              split .html | 	              split .html | ||||||
| 	              }`, | 	              }`, | ||||||
| 			false, []fastcgi.Rule{{ | 			false, []Rule{{ | ||||||
| 				Path:       "/", | 				Path:       "/", | ||||||
| 				Address:    "127.0.0.1:9001", | 				Address:    "127.0.0.1:9001", | ||||||
| 				Ext:        "", | 				Ext:        "", | ||||||
| @ -66,7 +63,7 @@ func TestFastcgiParse(t *testing.T) { | |||||||
| 	              split .html | 	              split .html | ||||||
| 	              except /admin /user | 	              except /admin /user | ||||||
| 	              }`, | 	              }`, | ||||||
| 			false, []fastcgi.Rule{{ | 			false, []Rule{{ | ||||||
| 				Path:            "/", | 				Path:            "/", | ||||||
| 				Address:         "127.0.0.1:9001", | 				Address:         "127.0.0.1:9001", | ||||||
| 				Ext:             "", | 				Ext:             "", | ||||||
| @ -76,8 +73,7 @@ func TestFastcgiParse(t *testing.T) { | |||||||
| 			}}}, | 			}}}, | ||||||
| 	} | 	} | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		c := NewTestController(test.inputFastcgiConfig) | 		actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController(test.inputFastcgiConfig)) | ||||||
| 		actualFastcgiConfigs, err := fastcgiParse(c) |  | ||||||
| 
 | 
 | ||||||
| 		if err == nil && test.shouldErr { | 		if err == nil && test.shouldErr { | ||||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | 			t.Errorf("Test %d didn't error, but it should have", i) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| // Package gzip provides a simple middleware layer that performs | // Package gzip provides a middleware layer that performs | ||||||
| // gzip compression on the response. | // gzip compression on the response. | ||||||
| package gzip | package gzip | ||||||
| 
 | 
 | ||||||
| @ -12,15 +12,24 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"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 | // Gzip is a middleware type which gzips HTTP responses. It is | ||||||
| // imperative that any handler which writes to a gzipped response | // imperative that any handler which writes to a gzipped response | ||||||
| // specifies the Content-Type, otherwise some clients will assume | // specifies the Content-Type, otherwise some clients will assume | ||||||
| // application/x-gzip and try to download a file. | // application/x-gzip and try to download a file. | ||||||
| type Gzip struct { | type Gzip struct { | ||||||
| 	Next    middleware.Handler | 	Next    httpserver.Handler | ||||||
| 	Configs []Config | 	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") { | 	if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { | ||||||
| 		return g.Next.ServeHTTP(w, r) | 		return g.Next.ServeHTTP(w, r) | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| outer: | outer: | ||||||
| 	for _, c := range g.Configs { | 	for _, c := range g.Configs { | ||||||
| 
 | 
 | ||||||
| @ -79,9 +87,7 @@ outer: | |||||||
| 		// to send something back before gzipWriter gets closed at | 		// to send something back before gzipWriter gets closed at | ||||||
| 		// the return of this method! | 		// the return of this method! | ||||||
| 		if status >= 400 { | 		if status >= 400 { | ||||||
| 			gz.Header().Set("Content-Type", "text/plain") // very necessary | 			httpserver.DefaultErrorFunc(w, r, status) | ||||||
| 			gz.WriteHeader(status) |  | ||||||
| 			fmt.Fprintf(gz, "%d %s", status, http.StatusText(status)) |  | ||||||
| 			return 0, err | 			return 0, err | ||||||
| 		} | 		} | ||||||
| 		return status, err | 		return status, err | ||||||
| @ -8,11 +8,10 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestGzipHandler(t *testing.T) { | func TestGzipHandler(t *testing.T) { | ||||||
| 
 |  | ||||||
| 	pathFilter := PathFilter{make(Set)} | 	pathFilter := PathFilter{make(Set)} | ||||||
| 	badPaths := []string{"/bad", "/nogzip", "/nongzip"} | 	badPaths := []string{"/bad", "/nogzip", "/nongzip"} | ||||||
| 	for _, p := range badPaths { | 	for _, p := range badPaths { | ||||||
| @ -80,9 +79,8 @@ func TestGzipHandler(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func nextFunc(shouldGzip bool) middleware.Handler { | func nextFunc(shouldGzip bool) httpserver.Handler { | ||||||
| 	return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | 	return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||||
| 
 |  | ||||||
| 		// write a relatively large text file | 		// write a relatively large text file | ||||||
| 		b, err := ioutil.ReadFile("testdata/test.txt") | 		b, err := ioutil.ReadFile("testdata/test.txt") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -4,7 +4,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path" | 	"path" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // RequestFilter determines if a request should be gzipped. | // 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. | // 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. | // DefaultExtFilter creates an ExtFilter with default extensions. | ||||||
| func DefaultExtFilter() ExtFilter { | func DefaultExtFilter() ExtFilter { | ||||||
| @ -54,7 +55,7 @@ type PathFilter struct { | |||||||
| // is found and true otherwise. | // is found and true otherwise. | ||||||
| func (p PathFilter) ShouldCompress(r *http.Request) bool { | func (p PathFilter) ShouldCompress(r *http.Request) bool { | ||||||
| 	return !p.IgnoredPaths.ContainsFunc(func(value string) bool { | 	return !p.IgnoredPaths.ContainsFunc(func(value string) bool { | ||||||
| 		return middleware.Path(r.URL.Path).Matches(value) | 		return httpserver.Path(r.URL.Path).Matches(value) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -7,7 +7,7 @@ import ( | |||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestLengthFilter(t *testing.T) { | func TestLengthFilter(t *testing.T) { | ||||||
| @ -61,7 +61,7 @@ func TestResponseFilterWriter(t *testing.T) { | |||||||
| 	}} | 	}} | ||||||
| 
 | 
 | ||||||
| 	for i, ts := range tests { | 	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.Header().Set("Content-Length", fmt.Sprint(len(ts.body))) | ||||||
| 			w.Write([]byte(ts.body)) | 			w.Write([]byte(ts.body)) | ||||||
| 			return 200, nil | 			return 200, nil | ||||||
| @ -1,38 +1,40 @@ | |||||||
| package setup | package gzip | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/gzip" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Gzip configures a new gzip middleware instance. | // setup configures a new gzip middleware instance. | ||||||
| func Gzip(c *Controller) (middleware.Middleware, error) { | func setup(c *caddy.Controller) error { | ||||||
| 	configs, err := gzipParse(c) | 	configs, err := gzipParse(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return func(next middleware.Handler) middleware.Handler { | 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||||
| 		return gzip.Gzip{Next: next, Configs: configs} | 		return Gzip{Next: next, Configs: configs} | ||||||
| 	}, nil | 	}) | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func gzipParse(c *Controller) ([]gzip.Config, error) { | func gzipParse(c *caddy.Controller) ([]Config, error) { | ||||||
| 	var configs []gzip.Config | 	var configs []Config | ||||||
| 
 | 
 | ||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		config := gzip.Config{} | 		config := Config{} | ||||||
| 
 | 
 | ||||||
| 		// Request Filters | 		// Request Filters | ||||||
| 		pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)} | 		pathFilter := PathFilter{IgnoredPaths: make(Set)} | ||||||
| 		extFilter := gzip.ExtFilter{Exts: make(gzip.Set)} | 		extFilter := ExtFilter{Exts: make(Set)} | ||||||
| 
 | 
 | ||||||
| 		// Response Filters | 		// Response Filters | ||||||
| 		lengthFilter := gzip.LengthFilter(0) | 		lengthFilter := LengthFilter(0) | ||||||
| 
 | 
 | ||||||
| 		// No extra args expected | 		// No extra args expected | ||||||
| 		if len(c.RemainingArgs()) > 0 { | 		if len(c.RemainingArgs()) > 0 { | ||||||
| @ -47,7 +49,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) { | |||||||
| 					return configs, c.ArgErr() | 					return configs, c.ArgErr() | ||||||
| 				} | 				} | ||||||
| 				for _, e := range exts { | 				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) | 						return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e) | ||||||
| 					} | 					} | ||||||
| 					extFilter.Exts.Add(e) | 					extFilter.Exts.Add(e) | ||||||
| @ -82,18 +84,18 @@ func gzipParse(c *Controller) ([]gzip.Config, error) { | |||||||
| 				} else if length == 0 { | 				} else if length == 0 { | ||||||
| 					return configs, fmt.Errorf(`gzip: min_length must be greater than 0`) | 					return configs, fmt.Errorf(`gzip: min_length must be greater than 0`) | ||||||
| 				} | 				} | ||||||
| 				lengthFilter = gzip.LengthFilter(length) | 				lengthFilter = LengthFilter(length) | ||||||
| 			default: | 			default: | ||||||
| 				return configs, c.ArgErr() | 				return configs, c.ArgErr() | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Request Filters | 		// Request Filters | ||||||
| 		config.RequestFilters = []gzip.RequestFilter{} | 		config.RequestFilters = []RequestFilter{} | ||||||
| 
 | 
 | ||||||
| 		// If ignored paths are specified, put in front to filter with path first | 		// If ignored paths are specified, put in front to filter with path first | ||||||
| 		if len(pathFilter.IgnoredPaths) > 0 { | 		if len(pathFilter.IgnoredPaths) > 0 { | ||||||
| 			config.RequestFilters = []gzip.RequestFilter{pathFilter} | 			config.RequestFilters = []RequestFilter{pathFilter} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Then, if extensions are specified, use those to filter. | 		// 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 { | 		if len(extFilter.Exts) > 0 { | ||||||
| 			config.RequestFilters = append(config.RequestFilters, extFilter) | 			config.RequestFilters = append(config.RequestFilters, extFilter) | ||||||
| 		} else { | 		} else { | ||||||
| 			config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter()) | 			config.RequestFilters = append(config.RequestFilters, DefaultExtFilter()) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Response Filters | 		// Response Filters | ||||||
| @ -1,29 +1,29 @@ | |||||||
| package setup | package gzip | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware/gzip" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestGzip(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 	c := NewTestController(`gzip`) | 	err := setup(caddy.NewTestController(`gzip`)) | ||||||
| 
 |  | ||||||
| 	mid, err := Gzip(c) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Expected no errors, but got: %v", err) | 		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") | 		t.Fatal("Expected middleware, was nil instead") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	handler := mid(EmptyNext) | 	handler := mids[0](httpserver.EmptyNext) | ||||||
| 	myHandler, ok := handler.(gzip.Gzip) | 	myHandler, ok := handler.(Gzip) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		t.Fatalf("Expected handler to be type Gzip, got: %#v", handler) | 		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") | 		t.Error("'Next' field of handler was not set properly") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -90,8 +90,7 @@ func TestGzip(t *testing.T) { | |||||||
| 		`, false}, | 		`, false}, | ||||||
| 	} | 	} | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		c := NewTestController(test.input) | 		_, err := gzipParse(caddy.NewTestController(test.input)) | ||||||
| 		_, err := gzipParse(c) |  | ||||||
| 		if test.shouldErr && err == nil { | 		if test.shouldErr && err == nil { | ||||||
| 			t.Errorf("Test %v: Expected error but found nil", i) | 			t.Errorf("Test %v: Expected error but found nil", i) | ||||||
| 		} else if !test.shouldErr && err != nil { | 		} else if !test.shouldErr && err != nil { | ||||||
| @ -1,28 +1,28 @@ | |||||||
| // Package headers provides middleware that appends headers to | // Package header provides middleware that appends headers to | ||||||
| // requests based on a set of configuration rules that define | // requests based on a set of configuration rules that define | ||||||
| // which routes receive which headers. | // which routes receive which headers. | ||||||
| package headers | package header | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Headers is middleware that adds headers to the responses | // Headers is middleware that adds headers to the responses | ||||||
| // for requests matching a certain path. | // for requests matching a certain path. | ||||||
| type Headers struct { | type Headers struct { | ||||||
| 	Next  middleware.Handler | 	Next  httpserver.Handler | ||||||
| 	Rules []Rule | 	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. | // setting headers on the response according to the configured rules. | ||||||
| func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | 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 { | 	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 { | 			for _, header := range rule.Headers { | ||||||
| 				if strings.HasPrefix(header.Name, "-") { | 				if strings.HasPrefix(header.Name, "-") { | ||||||
| 					w.Header().Del(strings.TrimLeft(header.Name, "-")) | 					w.Header().Del(strings.TrimLeft(header.Name, "-")) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package headers | package header | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @ -6,10 +6,10 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"testing" | 	"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() | 	hostname, err := os.Hostname() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Could not determine hostname: %v", err) | 		t.Fatalf("Could not determine hostname: %v", err) | ||||||
| @ -27,7 +27,7 @@ func TestHeaders(t *testing.T) { | |||||||
| 		{"/b", "Bar", "Removed in /a"}, | 		{"/b", "Bar", "Removed in /a"}, | ||||||
| 	} { | 	} { | ||||||
| 		he := Headers{ | 		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 | 				return 0, nil | ||||||
| 			}), | 			}), | ||||||
| 			Rules: []Rule{ | 			Rules: []Rule{ | ||||||
| @ -1,27 +1,37 @@ | |||||||
| package setup | package header | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy" | ||||||
| 	"github.com/mholt/caddy/middleware/headers" | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Headers configures a new Headers middleware instance. | func init() { | ||||||
| func Headers(c *Controller) (middleware.Middleware, error) { | 	caddy.RegisterPlugin(caddy.Plugin{ | ||||||
| 	rules, err := headersParse(c) | 		Name:       "header", | ||||||
| 	if err != nil { | 		ServerType: "http", | ||||||
| 		return nil, err | 		Action:     setup, | ||||||
| 	} | 	}) | ||||||
| 
 |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { |  | ||||||
| 		return headers.Headers{Next: next, Rules: rules} |  | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func headersParse(c *Controller) ([]headers.Rule, error) { | // setup configures a new Headers middleware instance. | ||||||
| 	var rules []headers.Rule | 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() { | 	for c.NextLine() { | ||||||
| 		var head headers.Rule | 		var head Rule | ||||||
| 		var isNewPattern bool | 		var isNewPattern bool | ||||||
| 
 | 
 | ||||||
| 		if !c.NextArg() { | 		if !c.NextArg() { | ||||||
| @ -46,7 +56,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) { | |||||||
| 		for c.NextBlock() { | 		for c.NextBlock() { | ||||||
| 			// A block of headers was opened... | 			// A block of headers was opened... | ||||||
| 
 | 
 | ||||||
| 			h := headers.Header{Name: c.Val()} | 			h := Header{Name: c.Val()} | ||||||
| 
 | 
 | ||||||
| 			if c.NextArg() { | 			if c.NextArg() { | ||||||
| 				h.Value = c.Val() | 				h.Value = c.Val() | ||||||
| @ -57,7 +67,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) { | |||||||
| 		if c.NextArg() { | 		if c.NextArg() { | ||||||
| 			// ... or single header was defined as an argument instead. | 			// ... or single header was defined as an argument instead. | ||||||
| 
 | 
 | ||||||
| 			h := headers.Header{Name: c.Val()} | 			h := Header{Name: c.Val()} | ||||||
| 
 | 
 | ||||||
| 			h.Value = c.Val() | 			h.Value = c.Val() | ||||||
| 
 | 
 | ||||||
| @ -1,30 +1,31 @@ | |||||||
| package setup | package header | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/middleware/headers" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestHeaders(t *testing.T) { | func TestSetup(t *testing.T) { | ||||||
| 	c := NewTestController(`header / Foo Bar`) | 	err := setup(caddy.NewTestController(`header / Foo Bar`)) | ||||||
| 
 |  | ||||||
| 	mid, err := Headers(c) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Expected no errors, but got: %v", err) | 		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) | 	handler := mids[0](httpserver.EmptyNext) | ||||||
| 	myHandler, ok := handler.(headers.Headers) | 	myHandler, ok := handler.(Headers) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		t.Fatalf("Expected handler to be type Headers, got: %#v", handler) | 		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") | 		t.Error("'Next' field of handler was not set properly") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -33,17 +34,17 @@ func TestHeadersParse(t *testing.T) { | |||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		input     string | 		input     string | ||||||
| 		shouldErr bool | 		shouldErr bool | ||||||
| 		expected  []headers.Rule | 		expected  []Rule | ||||||
| 	}{ | 	}{ | ||||||
| 		{`header /foo Foo "Bar Baz"`, | 		{`header /foo Foo "Bar Baz"`, | ||||||
| 			false, []headers.Rule{ | 			false, []Rule{ | ||||||
| 				{Path: "/foo", Headers: []headers.Header{ | 				{Path: "/foo", Headers: []Header{ | ||||||
| 					{Name: "Foo", Value: "Bar Baz"}, | 					{Name: "Foo", Value: "Bar Baz"}, | ||||||
| 				}}, | 				}}, | ||||||
| 			}}, | 			}}, | ||||||
| 		{`header /bar { Foo "Bar Baz" Baz Qux }`, | 		{`header /bar { Foo "Bar Baz" Baz Qux }`, | ||||||
| 			false, []headers.Rule{ | 			false, []Rule{ | ||||||
| 				{Path: "/bar", Headers: []headers.Header{ | 				{Path: "/bar", Headers: []Header{ | ||||||
| 					{Name: "Foo", Value: "Bar Baz"}, | 					{Name: "Foo", Value: "Bar Baz"}, | ||||||
| 					{Name: "Baz", Value: "Qux"}, | 					{Name: "Baz", Value: "Qux"}, | ||||||
| 				}}, | 				}}, | ||||||
| @ -51,8 +52,7 @@ func TestHeadersParse(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for i, test := range tests { | 	for i, test := range tests { | ||||||
| 		c := NewTestController(test.input) | 		actual, err := headersParse(caddy.NewTestController(test.input)) | ||||||
| 		actual, err := headersParse(c) |  | ||||||
| 
 | 
 | ||||||
| 		if err == nil && test.shouldErr { | 		if err == nil && test.shouldErr { | ||||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | 			t.Errorf("Test %d didn't error, but it should have", i) | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package middleware | package httpserver | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user