mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -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,4 +1,4 @@ | ||||
| package assets | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| @ -6,10 +6,15 @@ import ( | ||||
| 	"runtime" | ||||
| ) | ||||
| 
 | ||||
| // Path returns the path to the folder | ||||
| // where the application may store data. This | ||||
| // currently resolves to ~/.caddy | ||||
| func Path() string { | ||||
| // AssetsPath returns the path to the folder | ||||
| // where the application may store data. If | ||||
| // CADDYPATH env variable is set, that value | ||||
| // is used. Otherwise, the path is the result | ||||
| // of evaluating "$HOME/.caddy". | ||||
| func AssetsPath() string { | ||||
| 	if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" { | ||||
| 		return caddyPath | ||||
| 	} | ||||
| 	return filepath.Join(userHomeDir(), ".caddy") | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										19
									
								
								assets_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								assets_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestAssetsPath(t *testing.T) { | ||||
| 	if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") { | ||||
| 		t.Errorf("Expected path to be a .caddy folder, got: %v", actual) | ||||
| 	} | ||||
| 
 | ||||
| 	os.Setenv("CADDYPATH", "testpath") | ||||
| 	if actual, expected := AssetsPath(), "testpath"; actual != expected { | ||||
| 		t.Errorf("Expected path to be %v, got: %v", expected, actual) | ||||
| 	} | ||||
| 	os.Setenv("CADDYPATH", "") | ||||
| } | ||||
							
								
								
									
										745
									
								
								caddy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										745
									
								
								caddy.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,745 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddyfile" | ||||
| ) | ||||
| 
 | ||||
| // Configurable application parameters | ||||
| var ( | ||||
| 	// AppName is the name of the application. | ||||
| 	AppName string | ||||
| 
 | ||||
| 	// AppVersion is the version of the application. | ||||
| 	AppVersion string | ||||
| 
 | ||||
| 	// Quiet mode will not show any informative output on initialization. | ||||
| 	Quiet bool | ||||
| 
 | ||||
| 	// PidFile is the path to the pidfile to create. | ||||
| 	PidFile string | ||||
| 
 | ||||
| 	// GracefulTimeout is the maximum duration of a graceful shutdown. | ||||
| 	GracefulTimeout time.Duration | ||||
| 
 | ||||
| 	// isUpgrade will be set to true if this process | ||||
| 	// was started as part of an upgrade, where a parent | ||||
| 	// Caddy process started this one. | ||||
| 	isUpgrade bool | ||||
| ) | ||||
| 
 | ||||
| // Instance contains the state of servers created as a result of | ||||
| // calling Start and can be used to access or control those servers. | ||||
| type Instance struct { | ||||
| 	// serverType is the name of the instance's server type | ||||
| 	serverType string | ||||
| 
 | ||||
| 	// caddyfileInput is the input configuration text used for this process | ||||
| 	caddyfileInput Input | ||||
| 
 | ||||
| 	// wg is used to wait for all servers to shut down | ||||
| 	wg sync.WaitGroup | ||||
| 
 | ||||
| 	// servers is the list of servers with their listeners... | ||||
| 	servers []serverListener | ||||
| 
 | ||||
| 	// these are callbacks to execute when certain events happen | ||||
| 	onStartup  []func() error | ||||
| 	onRestart  []func() error | ||||
| 	onShutdown []func() error | ||||
| } | ||||
| 
 | ||||
| // Stop stops all servers contained in i. It does NOT | ||||
| // execute shutdown callbacks. | ||||
| func (i *Instance) Stop() error { | ||||
| 	// stop the servers | ||||
| 	for _, s := range i.servers { | ||||
| 		if gs, ok := s.server.(GracefulServer); ok { | ||||
| 			if err := gs.Stop(); err != nil { | ||||
| 				log.Printf("[ERROR] Stopping %s: %v", gs.Address(), err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// splice instance list to delete this one | ||||
| 	for j, other := range instances { | ||||
| 		if other == i { | ||||
| 			instances = append(instances[:j], instances[j+1:]...) | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // shutdownCallbacks executes all the shutdown callbacks of i. | ||||
| // An error returned from one does not stop execution of the rest. | ||||
| // All the errors will be returned. | ||||
| func (i *Instance) shutdownCallbacks() []error { | ||||
| 	var errs []error | ||||
| 	for _, shutdownFunc := range i.onShutdown { | ||||
| 		err := shutdownFunc() | ||||
| 		if err != nil { | ||||
| 			errs = append(errs, err) | ||||
| 		} | ||||
| 	} | ||||
| 	return errs | ||||
| } | ||||
| 
 | ||||
| // Restart replaces the servers in i with new servers created from | ||||
| // executing the newCaddyfile. Upon success, it returns the new | ||||
| // instance to replace i. Upon failure, i will not be replaced. | ||||
| func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) { | ||||
| 	log.Println("[INFO] Reloading") | ||||
| 
 | ||||
| 	// run restart callbacks | ||||
| 	for _, fn := range i.onRestart { | ||||
| 		err := fn() | ||||
| 		if err != nil { | ||||
| 			return i, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if newCaddyfile == nil { | ||||
| 		newCaddyfile = i.caddyfileInput | ||||
| 	} | ||||
| 
 | ||||
| 	// Add file descriptors of all the sockets that are capable of it | ||||
| 	restartFds := make(map[string]restartPair) | ||||
| 	for _, s := range i.servers { | ||||
| 		gs, srvOk := s.server.(GracefulServer) | ||||
| 		ln, lnOk := s.listener.(Listener) | ||||
| 		if srvOk && lnOk { | ||||
| 			restartFds[gs.Address()] = restartPair{server: gs, listener: ln} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// create new instance; if the restart fails, it is simply discarded | ||||
| 	newInst := &Instance{serverType: newCaddyfile.ServerType()} | ||||
| 
 | ||||
| 	// attempt to start new instance | ||||
| 	err := startWithListenerFds(newCaddyfile, newInst, restartFds) | ||||
| 	if err != nil { | ||||
| 		return i, err | ||||
| 	} | ||||
| 
 | ||||
| 	// success! bump the old instance out so it will be garbage-collected | ||||
| 	instancesMu.Lock() | ||||
| 	for j, other := range instances { | ||||
| 		if other == i { | ||||
| 			instances = append(instances[:j], instances[j+1:]...) | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	instancesMu.Unlock() | ||||
| 
 | ||||
| 	log.Println("[INFO] Reloading complete") | ||||
| 
 | ||||
| 	return newInst, nil | ||||
| } | ||||
| 
 | ||||
| // SaveServer adds s and its associated listener ln to the | ||||
| // internally-kept list of servers that is running. For | ||||
| // saved servers, graceful restarts will be provided. | ||||
| func (i *Instance) SaveServer(s Server, ln net.Listener) { | ||||
| 	i.servers = append(i.servers, serverListener{server: s, listener: ln}) | ||||
| } | ||||
| 
 | ||||
| // HasListenerWithAddress returns whether this package is | ||||
| // tracking a server using a listener with the address | ||||
| // addr. | ||||
| func HasListenerWithAddress(addr string) bool { | ||||
| 	instancesMu.Lock() | ||||
| 	defer instancesMu.Unlock() | ||||
| 	for _, inst := range instances { | ||||
| 		for _, sln := range inst.servers { | ||||
| 			if listenerAddrEqual(sln.listener, addr) { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // listenerAddrEqual compares a listener's address with | ||||
| // addr. Extra care is taken to match addresses with an | ||||
| // empty hostname portion, as listeners tend to report | ||||
| // [::]:80, for example, when the matching address that | ||||
| // created the listener might be simply :80. | ||||
| func listenerAddrEqual(ln net.Listener, addr string) bool { | ||||
| 	lnAddr := ln.Addr().String() | ||||
| 	hostname, port, err := net.SplitHostPort(addr) | ||||
| 	if err != nil || hostname != "" { | ||||
| 		return lnAddr == addr | ||||
| 	} | ||||
| 	if lnAddr == net.JoinHostPort("::", port) { | ||||
| 		return true | ||||
| 	} | ||||
| 	if lnAddr == net.JoinHostPort("0.0.0.0", port) { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| // TODO: We should be able to support UDP servers... I'm considering this pattern. | ||||
| 
 | ||||
| type UDPListener struct { | ||||
| 	*net.UDPConn | ||||
| } | ||||
| 
 | ||||
| func (u UDPListener) Accept() (net.Conn, error) { | ||||
| 	return u.UDPConn, nil | ||||
| } | ||||
| 
 | ||||
| func (u UDPListener) Close() error { | ||||
| 	return u.UDPConn.Close() | ||||
| } | ||||
| 
 | ||||
| func (u UDPListener) Addr() net.Addr { | ||||
| 	return u.UDPConn.LocalAddr() | ||||
| } | ||||
| 
 | ||||
| var _ net.Listener = UDPListener{} | ||||
| */ | ||||
| 
 | ||||
| // Server is a type that can listen and serve. A Server | ||||
| // must associate with exactly zero or one listeners. | ||||
| type Server interface { | ||||
| 	// Listen starts listening by creating a new listener | ||||
| 	// and returning it. It does not start accepting | ||||
| 	// connections. | ||||
| 	Listen() (net.Listener, error) | ||||
| 
 | ||||
| 	// Serve starts serving using the provided listener. | ||||
| 	// Serve must start the server loop nearly immediately, | ||||
| 	// or at least not return any errors before the server | ||||
| 	// loop begins. Serve blocks indefinitely, or in other | ||||
| 	// words, until the server is stopped. | ||||
| 	Serve(net.Listener) error | ||||
| } | ||||
| 
 | ||||
| // Stopper is a type that can stop serving. The stop | ||||
| // does not necessarily have to be graceful. | ||||
| type Stopper interface { | ||||
| 	// Stop stops the server. It blocks until the | ||||
| 	// server is completely stopped. | ||||
| 	Stop() error | ||||
| } | ||||
| 
 | ||||
| // GracefulServer is a Server and Stopper, the stopping | ||||
| // of which is graceful (whatever that means for the kind | ||||
| // of server being implemented). It must be able to return | ||||
| // the address it is configured to listen on so that its | ||||
| // listener can be paired with it upon graceful restarts. | ||||
| // The net.Listener that a GracefulServer creates must | ||||
| // implement the Listener interface for restarts to be | ||||
| // graceful (assuming the listener is for TCP). | ||||
| type GracefulServer interface { | ||||
| 	Server | ||||
| 	Stopper | ||||
| 
 | ||||
| 	// Address returns the address the server should | ||||
| 	// listen on; it is used to pair the server to | ||||
| 	// its listener during a graceful/zero-downtime | ||||
| 	// restart. Thus when implementing this method, | ||||
| 	// you must not access a listener to get the | ||||
| 	// address; you must store the address the | ||||
| 	// server is to serve on some other way. | ||||
| 	Address() string | ||||
| } | ||||
| 
 | ||||
| // Listener is a net.Listener with an underlying file descriptor. | ||||
| // A server's listener should implement this interface if it is | ||||
| // to support zero-downtime reloads. | ||||
| type Listener interface { | ||||
| 	net.Listener | ||||
| 	File() (*os.File, error) | ||||
| } | ||||
| 
 | ||||
| // AfterStartup is an interface that can be implemented | ||||
| // by a server type that wants to run some code after all | ||||
| // servers for the same Instance have started. | ||||
| type AfterStartup interface { | ||||
| 	OnStartupComplete() | ||||
| } | ||||
| 
 | ||||
| // LoadCaddyfile loads a Caddyfile by calling the plugged in | ||||
| // Caddyfile loader methods. An error is returned if more than | ||||
| // one loader returns a non-nil Caddyfile input. If no loaders | ||||
| // load a Caddyfile, the default loader is used. If no default | ||||
| // loader is registered or it returns nil, the server type's | ||||
| // default Caddyfile is loaded. If the server type does not | ||||
| // specify any default Caddyfile value, then an empty Caddyfile | ||||
| // is returned. Consequently, this function never returns a nil | ||||
| // value as long as there are no errors. | ||||
| func LoadCaddyfile(serverType string) (Input, error) { | ||||
| 	// Ask plugged-in loaders for a Caddyfile | ||||
| 	cdyfile, err := loadCaddyfileInput(serverType) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Otherwise revert to default | ||||
| 	if cdyfile == nil { | ||||
| 		cdyfile = DefaultInput(serverType) | ||||
| 	} | ||||
| 
 | ||||
| 	// Still nil? Geez. | ||||
| 	if cdyfile == nil { | ||||
| 		cdyfile = CaddyfileInput{ServerTypeName: serverType} | ||||
| 	} | ||||
| 
 | ||||
| 	return cdyfile, nil | ||||
| } | ||||
| 
 | ||||
| // Wait blocks until all of i's servers have stopped. | ||||
| func (i *Instance) Wait() { | ||||
| 	i.wg.Wait() | ||||
| } | ||||
| 
 | ||||
| // CaddyfileFromPipe loads the Caddyfile input from f if f is | ||||
| // not interactive input. f is assumed to be a pipe or stream, | ||||
| // such as os.Stdin. If f is not a pipe, no error is returned | ||||
| // but the Input value will be nil. An error is only returned | ||||
| // if there was an error reading the pipe, even if the length | ||||
| // of what was read is 0. | ||||
| func CaddyfileFromPipe(f *os.File) (Input, error) { | ||||
| 	fi, err := f.Stat() | ||||
| 	if err == nil && fi.Mode()&os.ModeCharDevice == 0 { | ||||
| 		// Note that a non-nil error is not a problem. Windows | ||||
| 		// will not create a stdin if there is no pipe, which | ||||
| 		// produces an error when calling Stat(). But Unix will | ||||
| 		// make one either way, which is why we also check that | ||||
| 		// bitmask. | ||||
| 		// NOTE: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X) | ||||
| 		confBody, err := ioutil.ReadAll(f) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return CaddyfileInput{ | ||||
| 			Contents: confBody, | ||||
| 			Filepath: f.Name(), | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// not having input from the pipe is not itself an error, | ||||
| 	// just means no input to return. | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| // Caddyfile returns the Caddyfile used to create i. | ||||
| func (i *Instance) Caddyfile() Input { | ||||
| 	return i.caddyfileInput | ||||
| } | ||||
| 
 | ||||
| // Start starts Caddy with the given Caddyfile. | ||||
| // | ||||
| // This function blocks until all the servers are listening. | ||||
| func Start(cdyfile Input) (*Instance, error) { | ||||
| 	writePidFile() | ||||
| 	inst := &Instance{serverType: cdyfile.ServerType()} | ||||
| 	return inst, startWithListenerFds(cdyfile, inst, nil) | ||||
| } | ||||
| 
 | ||||
| func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartPair) error { | ||||
| 	if cdyfile == nil { | ||||
| 		cdyfile = CaddyfileInput{} | ||||
| 	} | ||||
| 
 | ||||
| 	stypeName := cdyfile.ServerType() | ||||
| 
 | ||||
| 	stype, err := getServerType(stypeName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	inst.caddyfileInput = cdyfile | ||||
| 
 | ||||
| 	sblocks, err := loadServerBlocks(stypeName, path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	ctx := stype.NewContext() | ||||
| 
 | ||||
| 	sblocks, err = ctx.InspectServerBlocks(cdyfile.Path(), sblocks) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = executeDirectives(inst, cdyfile.Path(), stype.Directives, sblocks) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	slist, err := ctx.MakeServers() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if restartFds == nil { | ||||
| 		// run startup callbacks since this is not a restart | ||||
| 		for _, startupFunc := range inst.onStartup { | ||||
| 			err := startupFunc() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	err = startServers(slist, inst, restartFds) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	instancesMu.Lock() | ||||
| 	instances = append(instances, inst) | ||||
| 	instancesMu.Unlock() | ||||
| 
 | ||||
| 	// run any AfterStartup callbacks if this is not | ||||
| 	// part of a restart; then show file descriptor notice | ||||
| 	if restartFds == nil { | ||||
| 		for _, srvln := range inst.servers { | ||||
| 			if srv, ok := srvln.server.(AfterStartup); ok { | ||||
| 				srv.OnStartupComplete() | ||||
| 			} | ||||
| 		} | ||||
| 		if !Quiet { | ||||
| 			for _, srvln := range inst.servers { | ||||
| 				if !IsLoopback(srvln.listener.Addr().String()) { | ||||
| 					checkFdlimit() | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func executeDirectives(inst *Instance, filename string, | ||||
| 	directives []string, sblocks []caddyfile.ServerBlock) error { | ||||
| 
 | ||||
| 	// map of server block ID to map of directive name to whatever. | ||||
| 	storages := make(map[int]map[string]interface{}) | ||||
| 
 | ||||
| 	// It is crucial that directives are executed in the proper order. | ||||
| 	// We loop with the directives on the outer loop so we execute | ||||
| 	// a directive for all server blocks before going to the next directive. | ||||
| 	// This is important mainly due to the parsing callbacks (below). | ||||
| 	for _, dir := range directives { | ||||
| 		for i, sb := range sblocks { | ||||
| 			var once sync.Once | ||||
| 			if _, ok := storages[i]; !ok { | ||||
| 				storages[i] = make(map[string]interface{}) | ||||
| 			} | ||||
| 
 | ||||
| 			for j, key := range sb.Keys { | ||||
| 				// Execute directive if it is in the server block | ||||
| 				if tokens, ok := sb.Tokens[dir]; ok { | ||||
| 					controller := &Controller{ | ||||
| 						instance:  inst, | ||||
| 						Key:       key, | ||||
| 						Dispenser: caddyfile.NewDispenserTokens(filename, tokens), | ||||
| 						OncePerServerBlock: func(f func() error) error { | ||||
| 							var err error | ||||
| 							once.Do(func() { | ||||
| 								err = f() | ||||
| 							}) | ||||
| 							return err | ||||
| 						}, | ||||
| 						ServerBlockIndex:    i, | ||||
| 						ServerBlockKeyIndex: j, | ||||
| 						ServerBlockKeys:     sb.Keys, | ||||
| 						ServerBlockStorage:  storages[i][dir], | ||||
| 					} | ||||
| 
 | ||||
| 					setup, err := DirectiveAction(inst.serverType, dir) | ||||
| 					if err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 
 | ||||
| 					err = setup(controller) | ||||
| 					if err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 
 | ||||
| 					storages[i][dir] = controller.ServerBlockStorage // persist for this server block | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// See if there are any callbacks to execute after this directive | ||||
| 		if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok { | ||||
| 			callbacks := allCallbacks[dir] | ||||
| 			for _, callback := range callbacks { | ||||
| 				if err := callback(); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func startServers(serverList []Server, inst *Instance, restartFds map[string]restartPair) error { | ||||
| 	errChan := make(chan error, len(serverList)) | ||||
| 
 | ||||
| 	for _, s := range serverList { | ||||
| 		var ln net.Listener | ||||
| 		var err error | ||||
| 
 | ||||
| 		// If this is a reload and s is a GracefulServer, | ||||
| 		// reuse the listener for a graceful restart. | ||||
| 		if gs, ok := s.(GracefulServer); ok && restartFds != nil { | ||||
| 			addr := gs.Address() | ||||
| 			if old, ok := restartFds[addr]; ok { | ||||
| 				file, err := old.listener.File() | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				ln, err = net.FileListener(file) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 				file.Close() | ||||
| 				delete(restartFds, addr) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if ln == nil { | ||||
| 			ln, err = s.Listen() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		inst.wg.Add(1) | ||||
| 		go func(s Server, ln net.Listener, inst *Instance) { | ||||
| 			defer inst.wg.Done() | ||||
| 			errChan <- s.Serve(ln) | ||||
| 		}(s, ln, inst) | ||||
| 
 | ||||
| 		inst.servers = append(inst.servers, serverListener{server: s, listener: ln}) | ||||
| 	} | ||||
| 
 | ||||
| 	// Close the remaining (unused) file descriptors to free up resources | ||||
| 	// and stop old servers that aren't used anymore | ||||
| 	for key, old := range restartFds { | ||||
| 		if err := old.server.Stop(); err != nil { | ||||
| 			log.Printf("[ERROR] Stopping %s: %v", old.server.Address(), err) | ||||
| 		} | ||||
| 		delete(restartFds, key) | ||||
| 	} | ||||
| 
 | ||||
| 	// Log errors that may be returned from Serve() calls, | ||||
| 	// these errors should only be occurring in the server loop. | ||||
| 	go func() { | ||||
| 		for err := range errChan { | ||||
| 			if err == nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			if strings.Contains(err.Error(), "use of closed network connection") { | ||||
| 				// this error is normal when closing the listener | ||||
| 				continue | ||||
| 			} | ||||
| 			log.Println(err) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func getServerType(serverType string) (ServerType, error) { | ||||
| 	stype, ok := serverTypes[serverType] | ||||
| 	if ok { | ||||
| 		return stype, nil | ||||
| 	} | ||||
| 	if len(serverTypes) == 0 { | ||||
| 		return ServerType{}, fmt.Errorf("no server types plugged in") | ||||
| 	} | ||||
| 	if serverType == "" { | ||||
| 		if len(serverTypes) == 1 { | ||||
| 			for _, stype := range serverTypes { | ||||
| 				return stype, nil | ||||
| 			} | ||||
| 		} | ||||
| 		return ServerType{}, fmt.Errorf("multiple server types available; must choose one") | ||||
| 	} | ||||
| 	return ServerType{}, fmt.Errorf("unknown server type '%s'", serverType) | ||||
| } | ||||
| 
 | ||||
| func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) { | ||||
| 	validDirectives := ValidDirectives(serverType) | ||||
| 	serverBlocks, err := caddyfile.ServerBlocks(filename, input, validDirectives) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(serverBlocks) == 0 && serverTypes[serverType].DefaultInput != nil { | ||||
| 		newInput := serverTypes[serverType].DefaultInput() | ||||
| 		serverBlocks, err = caddyfile.ServerBlocks(newInput.Path(), | ||||
| 			bytes.NewReader(newInput.Body()), validDirectives) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	return serverBlocks, nil | ||||
| } | ||||
| 
 | ||||
| // Stop stops ALL servers. It blocks until they are all stopped. | ||||
| // It does NOT execute shutdown callbacks, and it deletes all | ||||
| // instances after stopping is completed. Do not re-use any | ||||
| // references to old instances after calling Stop. | ||||
| func Stop() error { | ||||
| 	instancesMu.Lock() | ||||
| 	for _, inst := range instances { | ||||
| 		if err := inst.Stop(); err != nil { | ||||
| 			log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err) | ||||
| 		} | ||||
| 	} | ||||
| 	instances = []*Instance{} | ||||
| 	instancesMu.Unlock() | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // IsLoopback returns true if the hostname of addr looks | ||||
| // explicitly like a common local hostname. addr must only | ||||
| // be a host or a host:port combination. | ||||
| func IsLoopback(addr string) bool { | ||||
| 	host, _, err := net.SplitHostPort(addr) | ||||
| 	if err != nil { | ||||
| 		host = addr // happens if the addr is just a hostname | ||||
| 	} | ||||
| 	return host == "localhost" || | ||||
| 		strings.Trim(host, "[]") == "::1" || | ||||
| 		strings.HasPrefix(host, "127.") | ||||
| } | ||||
| 
 | ||||
| // checkFdlimit issues a warning if the OS limit for | ||||
| // max file descriptors is below a recommended minimum. | ||||
| func checkFdlimit() { | ||||
| 	const min = 8192 | ||||
| 
 | ||||
| 	// Warn if ulimit is too low for production sites | ||||
| 	if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { | ||||
| 		out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH | ||||
| 		if err == nil { | ||||
| 			lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) | ||||
| 			if err == nil && lim < min { | ||||
| 				fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+ | ||||
| 					"At least %d is recommended. Fix with \"ulimit -n %d\".\n", lim, min, min) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Upgrade re-launches the process, preserving the listeners | ||||
| // for a graceful restart. It does NOT load new configuration; | ||||
| // it only starts the process anew with a fresh binary. | ||||
| // | ||||
| // TODO: This is not yet implemented | ||||
| func Upgrade() error { | ||||
| 	return fmt.Errorf("not implemented") | ||||
| 	// TODO: have child process set isUpgrade = true | ||||
| } | ||||
| 
 | ||||
| // IsUpgrade returns true if this process is part of an upgrade | ||||
| // where a parent caddy process spawned this one to ugprade | ||||
| // the binary. | ||||
| func IsUpgrade() bool { | ||||
| 	return isUpgrade | ||||
| } | ||||
| 
 | ||||
| // CaddyfileInput represents a Caddyfile as input | ||||
| // and is simply a convenient way to implement | ||||
| // the Input interface. | ||||
| type CaddyfileInput struct { | ||||
| 	Filepath       string | ||||
| 	Contents       []byte | ||||
| 	ServerTypeName string | ||||
| } | ||||
| 
 | ||||
| // Body returns c.Contents. | ||||
| func (c CaddyfileInput) Body() []byte { return c.Contents } | ||||
| 
 | ||||
| // Path returns c.Filepath. | ||||
| func (c CaddyfileInput) Path() string { return c.Filepath } | ||||
| 
 | ||||
| // ServerType returns c.ServerType. | ||||
| func (c CaddyfileInput) ServerType() string { return c.ServerTypeName } | ||||
| 
 | ||||
| // Input represents a Caddyfile; its contents and file path | ||||
| // (which should include the file name at the end of the path). | ||||
| // If path does not apply (e.g. piped input) you may use | ||||
| // any understandable value. The path is mainly used for logging, | ||||
| // error messages, and debugging. | ||||
| type Input interface { | ||||
| 	// Gets the Caddyfile contents | ||||
| 	Body() []byte | ||||
| 
 | ||||
| 	// Gets the path to the origin file | ||||
| 	Path() string | ||||
| 
 | ||||
| 	// The type of server this input is intended for | ||||
| 	ServerType() string | ||||
| } | ||||
| 
 | ||||
| // DefaultInput returns the default Caddyfile input | ||||
| // to use when it is otherwise empty or missing. | ||||
| // It uses the default host and port (depends on | ||||
| // host, e.g. localhost is 2015, otherwise 443) and | ||||
| // root. | ||||
| func DefaultInput(serverType string) Input { | ||||
| 	if _, ok := serverTypes[serverType]; !ok { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if serverTypes[serverType].DefaultInput == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return serverTypes[serverType].DefaultInput() | ||||
| } | ||||
| 
 | ||||
| // writePidFile writes the process ID to the file at PidFile. | ||||
| // It does nothing if PidFile is not set. | ||||
| func writePidFile() error { | ||||
| 	if PidFile == "" { | ||||
| 		return nil | ||||
| 	} | ||||
| 	pid := []byte(strconv.Itoa(os.Getpid()) + "\n") | ||||
| 	return ioutil.WriteFile(PidFile, pid, 0644) | ||||
| } | ||||
| 
 | ||||
| type restartPair struct { | ||||
| 	server   GracefulServer | ||||
| 	listener Listener | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	// instances is the list of running Instances. | ||||
| 	instances []*Instance | ||||
| 
 | ||||
| 	// instancesMu protects instances. | ||||
| 	instancesMu sync.Mutex | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// DefaultConfigFile is the name of the configuration file that is loaded | ||||
| 	// by default if no other file is specified. | ||||
| 	DefaultConfigFile = "Caddyfile" | ||||
| ) | ||||
| @ -1,12 +0,0 @@ | ||||
| package assets | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestPath(t *testing.T) { | ||||
| 	if actual := Path(); !strings.HasSuffix(actual, ".caddy") { | ||||
| 		t.Errorf("Expected path to be a .caddy folder, got: %v", actual) | ||||
| 	} | ||||
| } | ||||
| @ -7,19 +7,18 @@ | ||||
| #     $ ./build.bash [output_filename] [git_repo] | ||||
| # | ||||
| # Outputs compiled program in current directory. | ||||
| # Default file name is 'ecaddy'. | ||||
| # Default git repo is current directory. | ||||
| # Builds always take place from current directory. | ||||
| 
 | ||||
| set -euo pipefail | ||||
| 
 | ||||
| : ${output_filename:="${1:-}"} | ||||
| : ${output_filename:="ecaddy"} | ||||
| : ${output_filename:="caddy"} | ||||
| 
 | ||||
| : ${git_repo:="${2:-}"} | ||||
| : ${git_repo:="."} | ||||
| 
 | ||||
| pkg=main | ||||
| pkg=github.com/mholt/caddy/caddy/caddymain | ||||
| ldflags=() | ||||
| 
 | ||||
| # Timestamp of build | ||||
							
								
								
									
										346
									
								
								caddy/caddy.go
									
									
									
									
									
								
							
							
						
						
									
										346
									
								
								caddy/caddy.go
									
									
									
									
									
								
							| @ -1,346 +0,0 @@ | ||||
| // Package caddy implements the Caddy web server as a service | ||||
| // in your own Go programs. | ||||
| // | ||||
| // To use this package, follow a few simple steps: | ||||
| // | ||||
| //   1. Set the AppName and AppVersion variables. | ||||
| //   2. Call LoadCaddyfile() to get the Caddyfile. | ||||
| //      You should pass in your own Caddyfile loader. | ||||
| //   3. Call caddy.Start() to start Caddy, caddy.Stop() | ||||
| //      to stop it, or caddy.Restart() to restart it. | ||||
| // | ||||
| // You should use caddy.Wait() to wait for all Caddy servers | ||||
| // to quit before your process exits. | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy/https" | ||||
| 	"github.com/mholt/caddy/server" | ||||
| ) | ||||
| 
 | ||||
| // Configurable application parameters | ||||
| var ( | ||||
| 	// AppName is the name of the application. | ||||
| 	AppName string | ||||
| 
 | ||||
| 	// AppVersion is the version of the application. | ||||
| 	AppVersion string | ||||
| 
 | ||||
| 	// Quiet when set to true, will not show any informative output on initialization. | ||||
| 	Quiet bool | ||||
| 
 | ||||
| 	// HTTP2 indicates whether HTTP2 is enabled or not. | ||||
| 	HTTP2 bool | ||||
| 
 | ||||
| 	// PidFile is the path to the pidfile to create. | ||||
| 	PidFile string | ||||
| 
 | ||||
| 	// GracefulTimeout is the maximum duration of a graceful shutdown. | ||||
| 	GracefulTimeout time.Duration | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// caddyfile is the input configuration text used for this process | ||||
| 	caddyfile Input | ||||
| 
 | ||||
| 	// caddyfileMu protects caddyfile during changes | ||||
| 	caddyfileMu sync.Mutex | ||||
| 
 | ||||
| 	// servers is a list of all the currently-listening servers | ||||
| 	servers []*server.Server | ||||
| 
 | ||||
| 	// serversMu protects the servers slice during changes | ||||
| 	serversMu sync.Mutex | ||||
| 
 | ||||
| 	// wg is used to wait for all servers to shut down | ||||
| 	wg sync.WaitGroup | ||||
| 
 | ||||
| 	// restartFds keeps the servers' sockets for graceful in-process restart | ||||
| 	restartFds = make(map[string]*os.File) | ||||
| 
 | ||||
| 	// startedBefore should be set to true if caddy has been started | ||||
| 	// at least once (does not indicate whether currently running). | ||||
| 	startedBefore bool | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// DefaultHost is the default host. | ||||
| 	DefaultHost = "" | ||||
| 	// DefaultPort is the default port. | ||||
| 	DefaultPort = "2015" | ||||
| 	// DefaultRoot is the default root folder. | ||||
| 	DefaultRoot = "." | ||||
| ) | ||||
| 
 | ||||
| // Start starts Caddy with the given Caddyfile. If cdyfile | ||||
| // is nil, the LoadCaddyfile function will be called to get | ||||
| // one. | ||||
| // | ||||
| // This function blocks until all the servers are listening. | ||||
| func Start(cdyfile Input) (err error) { | ||||
| 	// Input must never be nil; try to load something | ||||
| 	if cdyfile == nil { | ||||
| 		cdyfile, err = LoadCaddyfile(nil) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	caddyfileMu.Lock() | ||||
| 	caddyfile = cdyfile | ||||
| 	caddyfileMu.Unlock() | ||||
| 
 | ||||
| 	// load the server configs (activates Let's Encrypt) | ||||
| 	configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body())) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// group virtualhosts by address | ||||
| 	groupings, err := arrangeBindings(configs) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Start each server with its one or more configurations | ||||
| 	err = startServers(groupings) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	showInitializationOutput(groupings) | ||||
| 
 | ||||
| 	startedBefore = true | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // showInitializationOutput just outputs some basic information about | ||||
| // what is being served to stdout, as well as any applicable, non-essential | ||||
| // warnings for the user. | ||||
| func showInitializationOutput(groupings bindingGroup) { | ||||
| 	// Show initialization output | ||||
| 	if !Quiet && !IsRestart() { | ||||
| 		var checkedFdLimit bool | ||||
| 		for _, group := range groupings { | ||||
| 			for _, conf := range group.Configs { | ||||
| 				// Print address of site | ||||
| 				fmt.Println(conf.Address()) | ||||
| 
 | ||||
| 				// Note if non-localhost site resolves to loopback interface | ||||
| 				if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { | ||||
| 					fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", | ||||
| 						conf.Host, group.BindAddr.IP.String()) | ||||
| 				} | ||||
| 				if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { | ||||
| 					checkFdlimit() | ||||
| 					checkedFdLimit = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // startServers starts all the servers in groupings, | ||||
| // taking into account whether or not this process is | ||||
| // from a graceful restart or not. It blocks until | ||||
| // the servers are listening. | ||||
| func startServers(groupings bindingGroup) error { | ||||
| 	var startupWg sync.WaitGroup | ||||
| 	errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later | ||||
| 
 | ||||
| 	for _, group := range groupings { | ||||
| 		s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		s.HTTP2 = HTTP2 | ||||
| 		s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running | ||||
| 		if s.OnDemandTLS { | ||||
| 			s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome! | ||||
| 		} else { | ||||
| 			s.TLSConfig.GetCertificate = https.GetCertificate | ||||
| 		} | ||||
| 
 | ||||
| 		var ln server.ListenerFile | ||||
| 		if len(restartFds) > 0 { | ||||
| 			// Reuse the listeners for in-process restart | ||||
| 			if file, ok := restartFds[s.Addr]; ok { | ||||
| 				fln, err := net.FileListener(file) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				ln, ok = fln.(server.ListenerFile) | ||||
| 				if !ok { | ||||
| 					return errors.New("listener for " + s.Addr + " was not a ListenerFile") | ||||
| 				} | ||||
| 
 | ||||
| 				file.Close() | ||||
| 				delete(restartFds, s.Addr) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		wg.Add(1) | ||||
| 		go func(s *server.Server, ln server.ListenerFile) { | ||||
| 			defer wg.Done() | ||||
| 
 | ||||
| 			// run startup functions that should only execute when | ||||
| 			// the original parent process is starting. | ||||
| 			if !startedBefore { | ||||
| 				err := s.RunFirstStartupFuncs() | ||||
| 				if err != nil { | ||||
| 					errChan <- err | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// start the server | ||||
| 			if ln != nil { | ||||
| 				errChan <- s.Serve(ln) | ||||
| 			} else { | ||||
| 				errChan <- s.ListenAndServe() | ||||
| 			} | ||||
| 		}(s, ln) | ||||
| 
 | ||||
| 		startupWg.Add(1) | ||||
| 		go func(s *server.Server) { | ||||
| 			defer startupWg.Done() | ||||
| 			s.WaitUntilStarted() | ||||
| 		}(s) | ||||
| 
 | ||||
| 		serversMu.Lock() | ||||
| 		servers = append(servers, s) | ||||
| 		serversMu.Unlock() | ||||
| 	} | ||||
| 
 | ||||
| 	// Close the remaining (unused) file descriptors to free up resources | ||||
| 	if len(restartFds) > 0 { | ||||
| 		for key, file := range restartFds { | ||||
| 			file.Close() | ||||
| 			delete(restartFds, key) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Wait for all servers to finish starting | ||||
| 	startupWg.Wait() | ||||
| 
 | ||||
| 	// Return the first error, if any | ||||
| 	select { | ||||
| 	case err := <-errChan: | ||||
| 		// "use of closed network connection" is normal if it was a graceful shutdown | ||||
| 		if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { | ||||
| 			return err | ||||
| 		} | ||||
| 	default: | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Stop stops all servers. It blocks until they are all stopped. | ||||
| // It does NOT execute shutdown callbacks that may have been | ||||
| // configured by middleware (they must be executed separately). | ||||
| func Stop() error { | ||||
| 	https.Deactivate() | ||||
| 
 | ||||
| 	serversMu.Lock() | ||||
| 	for _, s := range servers { | ||||
| 		if err := s.Stop(); err != nil { | ||||
| 			log.Printf("[ERROR] Stopping %s: %v", s.Addr, err) | ||||
| 		} | ||||
| 	} | ||||
| 	servers = []*server.Server{} // don't reuse servers | ||||
| 	serversMu.Unlock() | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Wait blocks until all servers are stopped. | ||||
| func Wait() { | ||||
| 	wg.Wait() | ||||
| } | ||||
| 
 | ||||
| // LoadCaddyfile loads a Caddyfile by calling the user's loader function, | ||||
| // and if that returns nil, then this function resorts to the default | ||||
| // configuration. Thus, if there are no other errors, this function | ||||
| // always returns at least the default Caddyfile. | ||||
| func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { | ||||
| 	// Try user's loader | ||||
| 	if cdyfile == nil && loader != nil { | ||||
| 		cdyfile, err = loader() | ||||
| 	} | ||||
| 
 | ||||
| 	// Otherwise revert to default | ||||
| 	if cdyfile == nil { | ||||
| 		cdyfile = DefaultInput() | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // CaddyfileFromPipe loads the Caddyfile input from f if f is | ||||
| // not interactive input. f is assumed to be a pipe or stream, | ||||
| // such as os.Stdin. If f is not a pipe, no error is returned | ||||
| // but the Input value will be nil. An error is only returned | ||||
| // if there was an error reading the pipe, even if the length | ||||
| // of what was read is 0. | ||||
| func CaddyfileFromPipe(f *os.File) (Input, error) { | ||||
| 	fi, err := f.Stat() | ||||
| 	if err == nil && fi.Mode()&os.ModeCharDevice == 0 { | ||||
| 		// Note that a non-nil error is not a problem. Windows | ||||
| 		// will not create a stdin if there is no pipe, which | ||||
| 		// produces an error when calling Stat(). But Unix will | ||||
| 		// make one either way, which is why we also check that | ||||
| 		// bitmask. | ||||
| 		// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X) | ||||
| 		confBody, err := ioutil.ReadAll(f) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return CaddyfileInput{ | ||||
| 			Contents: confBody, | ||||
| 			Filepath: f.Name(), | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// not having input from the pipe is not itself an error, | ||||
| 	// just means no input to return. | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| // Caddyfile returns the current Caddyfile | ||||
| func Caddyfile() Input { | ||||
| 	caddyfileMu.Lock() | ||||
| 	defer caddyfileMu.Unlock() | ||||
| 	return caddyfile | ||||
| } | ||||
| 
 | ||||
| // Input represents a Caddyfile; its contents and file path | ||||
| // (which should include the file name at the end of the path). | ||||
| // If path does not apply (e.g. piped input) you may use | ||||
| // any understandable value. The path is mainly used for logging, | ||||
| // error messages, and debugging. | ||||
| type Input interface { | ||||
| 	// Gets the Caddyfile contents | ||||
| 	Body() []byte | ||||
| 
 | ||||
| 	// Gets the path to the origin file | ||||
| 	Path() string | ||||
| 
 | ||||
| 	// IsFile returns true if the original input was a file on the file system | ||||
| 	// that could be loaded again later if requested. | ||||
| 	IsFile() bool | ||||
| } | ||||
| @ -1,32 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func TestCaddyStartStop(t *testing.T) { | ||||
| 	caddyfile := "localhost:1984" | ||||
| 
 | ||||
| 	for i := 0; i < 2; i++ { | ||||
| 		err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Error starting, iteration %d: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		client := http.Client{ | ||||
| 			Timeout: time.Duration(2 * time.Second), | ||||
| 		} | ||||
| 		resp, err := client.Get("http://localhost:1984") | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) | ||||
| 		} | ||||
| 		resp.Body.Close() | ||||
| 
 | ||||
| 		err = Stop() | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Error stopping, iteration %d: %v", i, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package main | ||||
| package caddymain | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| @ -7,46 +7,55 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy" | ||||
| 	"github.com/mholt/caddy/caddy/https" | ||||
| 	"github.com/xenolf/lego/acme" | ||||
| 	"gopkg.in/natefinch/lumberjack.v2" | ||||
| 
 | ||||
| 	"github.com/xenolf/lego/acme" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy" | ||||
| 	// plug in the HTTP server type | ||||
| 	_ "github.com/mholt/caddy/caddyhttp" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddytls" | ||||
| 	// This is where other plugins get plugged in (imported) | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.TrapSignals() | ||||
| 	setVersion() | ||||
| 	flag.BoolVar(&https.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") | ||||
| 	flag.StringVar(&https.CAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "Certificate authority ACME server") | ||||
| 	flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") | ||||
| 
 | ||||
| 	flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") | ||||
| 	// TODO: Change from staging to v01 | ||||
| 	flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-staging.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory") | ||||
| 	flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")") | ||||
| 	flag.StringVar(&cpu, "cpu", "100%", "CPU cap") | ||||
| 	flag.StringVar(&https.DefaultEmail, "email", "", "Default Let's Encrypt account email address") | ||||
| 	flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") | ||||
| 	flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host") | ||||
| 	flag.BoolVar(&caddy.HTTP2, "http2", true, "Use HTTP/2") | ||||
| 	flag.BoolVar(&plugins, "plugins", false, "List installed plugins") | ||||
| 	flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address") | ||||
| 	flag.StringVar(&logfile, "log", "", "Process log file") | ||||
| 	flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file") | ||||
| 	flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port") | ||||
| 	flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") | ||||
| 	flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") | ||||
| 	flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site") | ||||
| 	flag.StringVar(&serverType, "type", "http", "Type of server to run") | ||||
| 	flag.BoolVar(&version, "version", false, "Show version") | ||||
| 	flag.BoolVar(&directives, "directives", false, "List supported directives") | ||||
| 
 | ||||
| 	caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader)) | ||||
| 	caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader)) | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	flag.Parse() // called here in main() to allow other packages to set flags in their inits | ||||
| // Run is Caddy's main() function. | ||||
| func Run() { | ||||
| 	flag.Parse() | ||||
| 	moveStorage() // TODO: This is temporary for the 0.9 release, or until most users upgrade to 0.9+ | ||||
| 
 | ||||
| 	caddy.AppName = appName | ||||
| 	caddy.AppVersion = appVersion | ||||
| 	acme.UserAgent = appName + "/" + appVersion | ||||
| 
 | ||||
| 	// set up process log before anything bad happens | ||||
| 	// Set up process log before anything bad happens | ||||
| 	switch logfile { | ||||
| 	case "stdout": | ||||
| 		log.SetOutput(os.Stdout) | ||||
| @ -63,8 +72,9 @@ func main() { | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check for one-time actions | ||||
| 	if revoke != "" { | ||||
| 		err := https.Revoke(revoke) | ||||
| 		err := caddytls.Revoke(revoke) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| @ -78,10 +88,8 @@ func main() { | ||||
| 		} | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 	if directives { | ||||
| 		for _, d := range caddy.Directives() { | ||||
| 			fmt.Println(d) | ||||
| 		} | ||||
| 	if plugins { | ||||
| 		fmt.Println(caddy.DescribePlugins()) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 
 | ||||
| @ -92,77 +100,124 @@ func main() { | ||||
| 	} | ||||
| 
 | ||||
| 	// Get Caddyfile input | ||||
| 	caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile) | ||||
| 	caddyfile, err := caddy.LoadCaddyfile(serverType) | ||||
| 	if err != nil { | ||||
| 		mustLogFatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Start your engines | ||||
| 	err = caddy.Start(caddyfile) | ||||
| 	instance, err := caddy.Start(caddyfile) | ||||
| 	if err != nil { | ||||
| 		mustLogFatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Twiddle your thumbs | ||||
| 	caddy.Wait() | ||||
| 	instance.Wait() | ||||
| } | ||||
| 
 | ||||
| // mustLogFatal just wraps log.Fatal() in a way that ensures the | ||||
| // mustLogFatal wraps log.Fatal() in a way that ensures the | ||||
| // output is always printed to stderr so the user can see it | ||||
| // if the user is still there, even if the process log was not | ||||
| // enabled. If this process is a restart, however, and the user | ||||
| // might not be there anymore, this just logs to the process log | ||||
| // and exits. | ||||
| // enabled. If this process is an upgrade, however, and the user | ||||
| // might not be there anymore, this just logs to the process | ||||
| // log and exits. | ||||
| func mustLogFatal(args ...interface{}) { | ||||
| 	if !caddy.IsRestart() { | ||||
| 	if !caddy.IsUpgrade() { | ||||
| 		log.SetOutput(os.Stderr) | ||||
| 	} | ||||
| 	log.Fatal(args...) | ||||
| } | ||||
| 
 | ||||
| func loadCaddyfile() (caddy.Input, error) { | ||||
| 	// Try -conf flag | ||||
| 	if conf != "" { | ||||
| 		if conf == "stdin" { | ||||
| 			return caddy.CaddyfileFromPipe(os.Stdin) | ||||
| 		} | ||||
| 
 | ||||
| 		contents, err := ioutil.ReadFile(conf) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		return caddy.CaddyfileInput{ | ||||
| 			Contents: contents, | ||||
| 			Filepath: conf, | ||||
| 			RealFile: true, | ||||
| 		}, nil | ||||
| // confLoader loads the Caddyfile using the -conf flag. | ||||
| func confLoader(serverType string) (caddy.Input, error) { | ||||
| 	if conf == "" { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// command line args | ||||
| 	if flag.NArg() > 0 { | ||||
| 		confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n") | ||||
| 		return caddy.CaddyfileInput{ | ||||
| 			Contents: []byte(confBody), | ||||
| 			Filepath: "args", | ||||
| 		}, nil | ||||
| 	if conf == "stdin" { | ||||
| 		return caddy.CaddyfileFromPipe(os.Stdin) | ||||
| 	} | ||||
| 
 | ||||
| 	// Caddyfile in cwd | ||||
| 	contents, err := ioutil.ReadFile(conf) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return caddy.CaddyfileInput{ | ||||
| 		Contents:       contents, | ||||
| 		Filepath:       conf, | ||||
| 		ServerTypeName: serverType, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // defaultLoader loads the Caddyfile from the current working directory. | ||||
| func defaultLoader(serverType string) (caddy.Input, error) { | ||||
| 	contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) | ||||
| 	if err != nil { | ||||
| 		if os.IsNotExist(err) { | ||||
| 			return caddy.DefaultInput(), nil | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return caddy.CaddyfileInput{ | ||||
| 		Contents: contents, | ||||
| 		Filepath: caddy.DefaultConfigFile, | ||||
| 		RealFile: true, | ||||
| 		Contents:       contents, | ||||
| 		Filepath:       caddy.DefaultConfigFile, | ||||
| 		ServerTypeName: serverType, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // moveStorage moves the old certificate storage location by | ||||
| // renaming the "letsencrypt" folder to the hostname of the | ||||
| // CA URL. This is TEMPORARY until most users have upgraded to 0.9+. | ||||
| func moveStorage() { | ||||
| 	oldPath := filepath.Join(caddy.AssetsPath(), "letsencrypt") | ||||
| 	_, err := os.Stat(oldPath) | ||||
| 	if os.IsNotExist(err) { | ||||
| 		return | ||||
| 	} | ||||
| 	newPath, err := caddytls.StorageFor(caddytls.DefaultCAUrl) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("[ERROR] Unable to get new path for certificate storage: %v", err) | ||||
| 	} | ||||
| 	err = os.MkdirAll(string(newPath), 0700) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("[ERROR] Unable to make new certificate storage path: %v", err) | ||||
| 	} | ||||
| 	err = os.Rename(oldPath, string(newPath)) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("[ERROR] Unable to migrate certificate storage: %v", err) | ||||
| 	} | ||||
| 	// convert mixed case folder and file names to lowercase | ||||
| 	filepath.Walk(string(newPath), func(path string, info os.FileInfo, err error) error { | ||||
| 		// must be careful to only lowercase the base of the path, not the whole thing!! | ||||
| 		base := filepath.Base(path) | ||||
| 		if lowerBase := strings.ToLower(base); base != lowerBase { | ||||
| 			lowerPath := filepath.Join(filepath.Dir(path), lowerBase) | ||||
| 			err = os.Rename(path, lowerPath) | ||||
| 			if err != nil { | ||||
| 				log.Fatalf("[ERROR] Unable to lower-case: %v", err) | ||||
| 			} | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setVersion figures out the version information | ||||
| // based on variables set by -ldflags. | ||||
| func setVersion() { | ||||
| 	// A development build is one that's not at a tag or has uncommitted changes | ||||
| 	devBuild = gitTag == "" || gitShortStat != "" | ||||
| 
 | ||||
| 	// Only set the appVersion if -ldflags was used | ||||
| 	if gitNearestTag != "" || gitTag != "" { | ||||
| 		if devBuild && gitNearestTag != "" { | ||||
| 			appVersion = fmt.Sprintf("%s (+%s %s)", | ||||
| 				strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate) | ||||
| 		} else if gitTag != "" { | ||||
| 			appVersion = strings.TrimPrefix(gitTag, "v") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // setCPU parses string cpu and sets GOMAXPROCS | ||||
| // according to its value. It accepts either | ||||
| // a number (e.g. 3) or a percent (e.g. 50%). | ||||
| @ -198,33 +253,17 @@ func setCPU(cpu string) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // setVersion figures out the version information based on | ||||
| // variables set by -ldflags. | ||||
| func setVersion() { | ||||
| 	// A development build is one that's not at a tag or has uncommitted changes | ||||
| 	devBuild = gitTag == "" || gitShortStat != "" | ||||
| 
 | ||||
| 	// Only set the appVersion if -ldflags was used | ||||
| 	if gitNearestTag != "" || gitTag != "" { | ||||
| 		if devBuild && gitNearestTag != "" { | ||||
| 			appVersion = fmt.Sprintf("%s (+%s %s)", | ||||
| 				strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate) | ||||
| 		} else if gitTag != "" { | ||||
| 			appVersion = strings.TrimPrefix(gitTag, "v") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const appName = "Caddy" | ||||
| 
 | ||||
| // Flags that control program flow or startup | ||||
| var ( | ||||
| 	serverType string | ||||
| 	conf       string | ||||
| 	cpu        string | ||||
| 	logfile    string | ||||
| 	revoke     string | ||||
| 	version    bool | ||||
| 	directives bool | ||||
| 	plugins    bool | ||||
| ) | ||||
| 
 | ||||
| // Build information obtained with the help of -ldflags | ||||
							
								
								
									
										348
									
								
								caddy/config.go
									
									
									
									
									
								
							
							
						
						
									
										348
									
								
								caddy/config.go
									
									
									
									
									
								
							| @ -1,348 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy/https" | ||||
| 	"github.com/mholt/caddy/caddy/parse" | ||||
| 	"github.com/mholt/caddy/caddy/setup" | ||||
| 	"github.com/mholt/caddy/server" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// DefaultConfigFile is the name of the configuration file that is loaded | ||||
| 	// by default if no other file is specified. | ||||
| 	DefaultConfigFile = "Caddyfile" | ||||
| ) | ||||
| 
 | ||||
| // loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them, | ||||
| // the parsed server blocks, the index of the last directive it processed, and an error (if any). | ||||
| func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) { | ||||
| 	var configs []server.Config | ||||
| 
 | ||||
| 	// Each server block represents similar hosts/addresses, since they | ||||
| 	// were grouped together in the Caddyfile. | ||||
| 	serverBlocks, err := parse.ServerBlocks(filename, input, true) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, 0, err | ||||
| 	} | ||||
| 	if len(serverBlocks) == 0 { | ||||
| 		newInput := DefaultInput() | ||||
| 		serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true) | ||||
| 		if err != nil { | ||||
| 			return nil, nil, 0, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off | ||||
| 
 | ||||
| 	// Iterate each server block and make a config for each one, | ||||
| 	// executing the directives that were parsed in order up to the tls | ||||
| 	// directive; this is because we must activate Let's Encrypt. | ||||
| 	for i, sb := range serverBlocks { | ||||
| 		onces := makeOnces() | ||||
| 		storages := makeStorages() | ||||
| 
 | ||||
| 		for j, addr := range sb.Addresses { | ||||
| 			config := server.Config{ | ||||
| 				Host:       addr.Host, | ||||
| 				Port:       addr.Port, | ||||
| 				Scheme:     addr.Scheme, | ||||
| 				Root:       Root, | ||||
| 				ConfigFile: filename, | ||||
| 				AppName:    AppName, | ||||
| 				AppVersion: AppVersion, | ||||
| 			} | ||||
| 
 | ||||
| 			// It is crucial that directives are executed in the proper order. | ||||
| 			for k, dir := range directiveOrder { | ||||
| 				// Execute directive if it is in the server block | ||||
| 				if tokens, ok := sb.Tokens[dir.name]; ok { | ||||
| 					// Each setup function gets a controller, from which setup functions | ||||
| 					// get access to the config, tokens, and other state information useful | ||||
| 					// to set up its own host only. | ||||
| 					controller := &setup.Controller{ | ||||
| 						Config:    &config, | ||||
| 						Dispenser: parse.NewDispenserTokens(filename, tokens), | ||||
| 						OncePerServerBlock: func(f func() error) error { | ||||
| 							var err error | ||||
| 							onces[dir.name].Do(func() { | ||||
| 								err = f() | ||||
| 							}) | ||||
| 							return err | ||||
| 						}, | ||||
| 						ServerBlockIndex:     i, | ||||
| 						ServerBlockHostIndex: j, | ||||
| 						ServerBlockHosts:     sb.HostList(), | ||||
| 						ServerBlockStorage:   storages[dir.name], | ||||
| 					} | ||||
| 					// execute setup function and append middleware handler, if any | ||||
| 					midware, err := dir.setup(controller) | ||||
| 					if err != nil { | ||||
| 						return nil, nil, lastDirectiveIndex, err | ||||
| 					} | ||||
| 					if midware != nil { | ||||
| 						config.Middleware = append(config.Middleware, midware) | ||||
| 					} | ||||
| 					storages[dir.name] = controller.ServerBlockStorage // persist for this server block | ||||
| 				} | ||||
| 
 | ||||
| 				// Stop after TLS setup, since we need to activate Let's Encrypt before continuing; | ||||
| 				// it makes some changes to the configs that middlewares might want to know about. | ||||
| 				if dir.name == "tls" { | ||||
| 					lastDirectiveIndex = k | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			configs = append(configs, config) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return configs, serverBlocks, lastDirectiveIndex, nil | ||||
| } | ||||
| 
 | ||||
| // loadConfigs reads input (named filename) and parses it, returning the | ||||
| // server configurations in the order they appeared in the input. As part | ||||
| // of this, it activates Let's Encrypt for the configs that are produced. | ||||
| // Thus, the returned configs are already optimally configured for HTTPS. | ||||
| func loadConfigs(filename string, input io.Reader) ([]server.Config, error) { | ||||
| 	configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Now we have all the configs, but they have only been set up to the | ||||
| 	// point of tls. We need to activate Let's Encrypt before setting up | ||||
| 	// the rest of the middlewares so they have correct information regarding | ||||
| 	// TLS configuration, if necessary. (this only appends, so our iterations | ||||
| 	// over server blocks below shouldn't be affected) | ||||
| 	if !IsRestart() && !Quiet { | ||||
| 		fmt.Print("Activating privacy features...") | ||||
| 	} | ||||
| 	configs, err = https.Activate(configs) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !IsRestart() && !Quiet { | ||||
| 		fmt.Println(" done.") | ||||
| 	} | ||||
| 
 | ||||
| 	// Finish setting up the rest of the directives, now that TLS is | ||||
| 	// optimally configured. These loops are similar to above except | ||||
| 	// we don't iterate all the directives from the beginning and we | ||||
| 	// don't create new configs. | ||||
| 	configIndex := -1 | ||||
| 	for i, sb := range serverBlocks { | ||||
| 		onces := makeOnces() | ||||
| 		storages := makeStorages() | ||||
| 
 | ||||
| 		for j := range sb.Addresses { | ||||
| 			configIndex++ | ||||
| 
 | ||||
| 			for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ { | ||||
| 				dir := directiveOrder[k] | ||||
| 
 | ||||
| 				if tokens, ok := sb.Tokens[dir.name]; ok { | ||||
| 					controller := &setup.Controller{ | ||||
| 						Config:    &configs[configIndex], | ||||
| 						Dispenser: parse.NewDispenserTokens(filename, tokens), | ||||
| 						OncePerServerBlock: func(f func() error) error { | ||||
| 							var err error | ||||
| 							onces[dir.name].Do(func() { | ||||
| 								err = f() | ||||
| 							}) | ||||
| 							return err | ||||
| 						}, | ||||
| 						ServerBlockIndex:     i, | ||||
| 						ServerBlockHostIndex: j, | ||||
| 						ServerBlockHosts:     sb.HostList(), | ||||
| 						ServerBlockStorage:   storages[dir.name], | ||||
| 					} | ||||
| 					midware, err := dir.setup(controller) | ||||
| 					if err != nil { | ||||
| 						return nil, err | ||||
| 					} | ||||
| 					if midware != nil { | ||||
| 						configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware) | ||||
| 					} | ||||
| 					storages[dir.name] = controller.ServerBlockStorage // persist for this server block | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return configs, nil | ||||
| } | ||||
| 
 | ||||
| // makeOnces makes a map of directive name to sync.Once | ||||
| // instance. This is intended to be called once per server | ||||
| // block when setting up configs so that Setup functions | ||||
| // for each directive can perform a task just once per | ||||
| // server block, even if there are multiple hosts on the block. | ||||
| // | ||||
| // We need one Once per directive, otherwise the first | ||||
| // directive to use it would exclude other directives from | ||||
| // using it at all, which would be a bug. | ||||
| func makeOnces() map[string]*sync.Once { | ||||
| 	onces := make(map[string]*sync.Once) | ||||
| 	for _, dir := range directiveOrder { | ||||
| 		onces[dir.name] = new(sync.Once) | ||||
| 	} | ||||
| 	return onces | ||||
| } | ||||
| 
 | ||||
| // makeStorages makes a map of directive name to interface{} | ||||
| // so that directives' setup functions can persist state | ||||
| // between different hosts on the same server block during the | ||||
| // setup phase. | ||||
| func makeStorages() map[string]interface{} { | ||||
| 	storages := make(map[string]interface{}) | ||||
| 	for _, dir := range directiveOrder { | ||||
| 		storages[dir.name] = nil | ||||
| 	} | ||||
| 	return storages | ||||
| } | ||||
| 
 | ||||
| // arrangeBindings groups configurations by their bind address. For example, | ||||
| // a server that should listen on localhost and another on 127.0.0.1 will | ||||
| // be grouped into the same address: 127.0.0.1. It will return an error | ||||
| // if an address is malformed or a TLS listener is configured on the | ||||
| // same address as a plaintext HTTP listener. The return value is a map of | ||||
| // bind address to list of configs that would become VirtualHosts on that | ||||
| // server. Use the keys of the returned map to create listeners, and use | ||||
| // the associated values to set up the virtualhosts. | ||||
| func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) { | ||||
| 	var groupings bindingGroup | ||||
| 
 | ||||
| 	// Group configs by bind address | ||||
| 	for _, conf := range allConfigs { | ||||
| 		// use default port if none is specified | ||||
| 		if conf.Port == "" { | ||||
| 			conf.Port = Port | ||||
| 		} | ||||
| 
 | ||||
| 		bindAddr, warnErr, fatalErr := resolveAddr(conf) | ||||
| 		if fatalErr != nil { | ||||
| 			return groupings, fatalErr | ||||
| 		} | ||||
| 		if warnErr != nil { | ||||
| 			log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr) | ||||
| 		} | ||||
| 
 | ||||
| 		// Make sure to compare the string representation of the address, | ||||
| 		// not the pointer, since a new *TCPAddr is created each time. | ||||
| 		var existing bool | ||||
| 		for i := 0; i < len(groupings); i++ { | ||||
| 			if groupings[i].BindAddr.String() == bindAddr.String() { | ||||
| 				groupings[i].Configs = append(groupings[i].Configs, conf) | ||||
| 				existing = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !existing { | ||||
| 			groupings = append(groupings, bindingMapping{ | ||||
| 				BindAddr: bindAddr, | ||||
| 				Configs:  []server.Config{conf}, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't allow HTTP and HTTPS to be served on the same address | ||||
| 	for _, group := range groupings { | ||||
| 		isTLS := group.Configs[0].TLS.Enabled | ||||
| 		for _, config := range group.Configs { | ||||
| 			if config.TLS.Enabled != isTLS { | ||||
| 				thisConfigProto, otherConfigProto := "HTTP", "HTTP" | ||||
| 				if config.TLS.Enabled { | ||||
| 					thisConfigProto = "HTTPS" | ||||
| 				} | ||||
| 				if group.Configs[0].TLS.Enabled { | ||||
| 					otherConfigProto = "HTTPS" | ||||
| 				} | ||||
| 				return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address", | ||||
| 					group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return groupings, nil | ||||
| } | ||||
| 
 | ||||
| // resolveAddr determines the address (host and port) that a config will | ||||
| // bind to. The returned address, resolvAddr, should be used to bind the | ||||
| // listener or group the config with other configs using the same address. | ||||
| // The first error, if not nil, is just a warning and should be reported | ||||
| // but execution may continue. The second error, if not nil, is a real | ||||
| // problem and the server should not be started. | ||||
| // | ||||
| // This function does not handle edge cases like port "http" or "https" if | ||||
| // they are not known to the system. It does, however, serve on the wildcard | ||||
| // host if resolving the address of the specific hostname fails. | ||||
| func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) { | ||||
| 	resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port)) | ||||
| 	if warnErr != nil { | ||||
| 		// the hostname probably couldn't be resolved, just bind to wildcard then | ||||
| 		resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port)) | ||||
| 		if fatalErr != nil { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // validDirective returns true if d is a valid | ||||
| // directive; false otherwise. | ||||
| func validDirective(d string) bool { | ||||
| 	for _, dir := range directiveOrder { | ||||
| 		if dir.name == d { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // DefaultInput returns the default Caddyfile input | ||||
| // to use when it is otherwise empty or missing. | ||||
| // It uses the default host and port (depends on | ||||
| // host, e.g. localhost is 2015, otherwise 443) and | ||||
| // root. | ||||
| func DefaultInput() CaddyfileInput { | ||||
| 	port := Port | ||||
| 	if https.HostQualifies(Host) && port == DefaultPort { | ||||
| 		port = "443" | ||||
| 	} | ||||
| 	return CaddyfileInput{ | ||||
| 		Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // These defaults are configurable through the command line | ||||
| var ( | ||||
| 	// Root is the site root | ||||
| 	Root = DefaultRoot | ||||
| 
 | ||||
| 	// Host is the site host | ||||
| 	Host = DefaultHost | ||||
| 
 | ||||
| 	// Port is the site port | ||||
| 	Port = DefaultPort | ||||
| ) | ||||
| 
 | ||||
| // bindingMapping maps a network address to configurations | ||||
| // that will bind to it. The order of the configs is important. | ||||
| type bindingMapping struct { | ||||
| 	BindAddr *net.TCPAddr | ||||
| 	Configs  []server.Config | ||||
| } | ||||
| 
 | ||||
| // bindingGroup maps network addresses to their configurations. | ||||
| // Preserving the order of the groupings is important | ||||
| // (related to graceful shutdown and restart) | ||||
| // so this is a slice, not a literal map. | ||||
| type bindingGroup []bindingMapping | ||||
| @ -1,159 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/server" | ||||
| ) | ||||
| 
 | ||||
| func TestDefaultInput(t *testing.T) { | ||||
| 	if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected { | ||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// next few tests simulate user providing -host and/or -port flags | ||||
| 
 | ||||
| 	Host = "not-localhost.com" | ||||
| 	if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected { | ||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	Host = "[::1]" | ||||
| 	if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected { | ||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	Host = "127.0.1.1" | ||||
| 	if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected { | ||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	Host = "not-localhost.com" | ||||
| 	Port = "1234" | ||||
| 	if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected { | ||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	Host = DefaultHost | ||||
| 	Port = "1234" | ||||
| 	if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected { | ||||
| 		t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n  ACTUAL: '%s'", Host, Port, Root, expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestResolveAddr(t *testing.T) { | ||||
| 	// NOTE: If tests fail due to comparing to string "127.0.0.1", | ||||
| 	// it's possible that system env resolves with IPv6, or ::1. | ||||
| 	// If that happens, maybe we should use actualAddr.IP.IsLoopback() | ||||
| 	// for the assertion, rather than a direct string comparison. | ||||
| 
 | ||||
| 	// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""} | ||||
| 	// will not behave the same cross-platform, so they have been omitted. | ||||
| 
 | ||||
| 	for i, test := range []struct { | ||||
| 		config         server.Config | ||||
| 		shouldWarnErr  bool | ||||
| 		shouldFatalErr bool | ||||
| 		expectedIP     string | ||||
| 		expectedPort   int | ||||
| 	}{ | ||||
| 		{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234}, | ||||
| 		{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80}, | ||||
| 		{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234}, | ||||
| 		{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234}, | ||||
| 		{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234}, | ||||
| 		{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80}, | ||||
| 		{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443}, | ||||
| 		{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234}, | ||||
| 		{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0}, | ||||
| 		{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234}, | ||||
| 		{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234}, | ||||
| 		{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234}, | ||||
| 	} { | ||||
| 		actualAddr, warnErr, fatalErr := resolveAddr(test.config) | ||||
| 
 | ||||
| 		if test.shouldFatalErr && fatalErr == nil { | ||||
| 			t.Errorf("Test %d: Expected error, but there wasn't any", i) | ||||
| 		} | ||||
| 		if !test.shouldFatalErr && fatalErr != nil { | ||||
| 			t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr) | ||||
| 		} | ||||
| 		if fatalErr != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if test.shouldWarnErr && warnErr == nil { | ||||
| 			t.Errorf("Test %d: Expected warning, but there wasn't any", i) | ||||
| 		} | ||||
| 		if !test.shouldWarnErr && warnErr != nil { | ||||
| 			t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr) | ||||
| 		} | ||||
| 
 | ||||
| 		if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected { | ||||
| 			t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected) | ||||
| 		} | ||||
| 		if actual, expected := actualAddr.Port, test.expectedPort; actual != expected { | ||||
| 			t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMakeOnces(t *testing.T) { | ||||
| 	directives := []directive{ | ||||
| 		{"dummy", nil}, | ||||
| 		{"dummy2", nil}, | ||||
| 	} | ||||
| 	directiveOrder = directives | ||||
| 	onces := makeOnces() | ||||
| 	if len(onces) != len(directives) { | ||||
| 		t.Errorf("onces had len %d , expected %d", len(onces), len(directives)) | ||||
| 	} | ||||
| 	expected := map[string]*sync.Once{ | ||||
| 		"dummy":  new(sync.Once), | ||||
| 		"dummy2": new(sync.Once), | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(onces, expected) { | ||||
| 		t.Errorf("onces was %v, expected %v", onces, expected) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMakeStorages(t *testing.T) { | ||||
| 	directives := []directive{ | ||||
| 		{"dummy", nil}, | ||||
| 		{"dummy2", nil}, | ||||
| 	} | ||||
| 	directiveOrder = directives | ||||
| 	storages := makeStorages() | ||||
| 	if len(storages) != len(directives) { | ||||
| 		t.Errorf("storages had len %d , expected %d", len(storages), len(directives)) | ||||
| 	} | ||||
| 	expected := map[string]interface{}{ | ||||
| 		"dummy":  nil, | ||||
| 		"dummy2": nil, | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(storages, expected) { | ||||
| 		t.Errorf("storages was %v, expected %v", storages, expected) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestValidDirective(t *testing.T) { | ||||
| 	directives := []directive{ | ||||
| 		{"dummy", nil}, | ||||
| 		{"dummy2", nil}, | ||||
| 	} | ||||
| 	directiveOrder = directives | ||||
| 	for i, test := range []struct { | ||||
| 		directive string | ||||
| 		valid     bool | ||||
| 	}{ | ||||
| 		{"dummy", true}, | ||||
| 		{"dummy2", true}, | ||||
| 		{"dummy3", false}, | ||||
| 	} { | ||||
| 		if actual, expected := validDirective(test.directive), test.valid; actual != expected { | ||||
| 			t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -1,109 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy/caddy/https" | ||||
| 	"github.com/mholt/caddy/caddy/parse" | ||||
| 	"github.com/mholt/caddy/caddy/setup" | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	// The parse package must know which directives | ||||
| 	// are valid, but it must not import the setup | ||||
| 	// or config package. To solve this problem, we | ||||
| 	// fill up this map in our init function here. | ||||
| 	// The parse package does not need to know the | ||||
| 	// ordering of the directives. | ||||
| 	for _, dir := range directiveOrder { | ||||
| 		parse.ValidDirectives[dir.name] = struct{}{} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Directives are registered in the order they should be | ||||
| // executed. Middleware (directives that inject a handler) | ||||
| // are executed in the order A-B-C-*-C-B-A, assuming | ||||
| // they all call the Next handler in the chain. | ||||
| // | ||||
| // Ordering is VERY important. Every middleware will | ||||
| // feel the effects of all other middleware below | ||||
| // (after) them during a request, but they must not | ||||
| // care what middleware above them are doing. | ||||
| // | ||||
| // For example, log needs to know the status code and | ||||
| // exactly how many bytes were written to the client, | ||||
| // which every other middleware can affect, so it gets | ||||
| // registered first. The errors middleware does not | ||||
| // care if gzip or log modifies its response, so it | ||||
| // gets registered below them. Gzip, on the other hand, | ||||
| // DOES care what errors does to the response since it | ||||
| // must compress every output to the client, even error | ||||
| // pages, so it must be registered before the errors | ||||
| // middleware and any others that would write to the | ||||
| // response. | ||||
| var directiveOrder = []directive{ | ||||
| 	// Essential directives that initialize vital configuration settings | ||||
| 	{"root", setup.Root}, | ||||
| 	{"bind", setup.BindHost}, | ||||
| 	{"tls", https.Setup}, | ||||
| 
 | ||||
| 	// Other directives that don't create HTTP handlers | ||||
| 	{"startup", setup.Startup}, | ||||
| 	{"shutdown", setup.Shutdown}, | ||||
| 
 | ||||
| 	// Directives that inject handlers (middleware) | ||||
| 	{"log", setup.Log}, | ||||
| 	{"gzip", setup.Gzip}, | ||||
| 	{"errors", setup.Errors}, | ||||
| 	{"header", setup.Headers}, | ||||
| 	{"rewrite", setup.Rewrite}, | ||||
| 	{"redir", setup.Redir}, | ||||
| 	{"ext", setup.Ext}, | ||||
| 	{"mime", setup.Mime}, | ||||
| 	{"basicauth", setup.BasicAuth}, | ||||
| 	{"internal", setup.Internal}, | ||||
| 	{"pprof", setup.PProf}, | ||||
| 	{"expvar", setup.ExpVar}, | ||||
| 	{"proxy", setup.Proxy}, | ||||
| 	{"fastcgi", setup.FastCGI}, | ||||
| 	{"websocket", setup.WebSocket}, | ||||
| 	{"markdown", setup.Markdown}, | ||||
| 	{"templates", setup.Templates}, | ||||
| 	{"browse", setup.Browse}, | ||||
| } | ||||
| 
 | ||||
| // Directives returns the list of directives in order of priority. | ||||
| func Directives() []string { | ||||
| 	directives := make([]string, len(directiveOrder)) | ||||
| 	for i, d := range directiveOrder { | ||||
| 		directives[i] = d.name | ||||
| 	} | ||||
| 	return directives | ||||
| } | ||||
| 
 | ||||
| // RegisterDirective adds the given directive to caddy's list of directives. | ||||
| // Pass the name of a directive you want it to be placed after, | ||||
| // otherwise it will be placed at the bottom of the stack. | ||||
| func RegisterDirective(name string, setup SetupFunc, after string) { | ||||
| 	dir := directive{name: name, setup: setup} | ||||
| 	idx := len(directiveOrder) | ||||
| 	for i := range directiveOrder { | ||||
| 		if directiveOrder[i].name == after { | ||||
| 			idx = i + 1 | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...) | ||||
| 	directiveOrder = newDirectives | ||||
| 	parse.ValidDirectives[name] = struct{}{} | ||||
| } | ||||
| 
 | ||||
| // directive ties together a directive name with its setup function. | ||||
| type directive struct { | ||||
| 	name  string | ||||
| 	setup SetupFunc | ||||
| } | ||||
| 
 | ||||
| // SetupFunc takes a controller and may optionally return a middleware. | ||||
| // If the resulting middleware is not nil, it will be chained into | ||||
| // the HTTP handlers in the order specified in this package. | ||||
| type SetupFunc func(c *setup.Controller) (middleware.Middleware, error) | ||||
| @ -1,31 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestRegister(t *testing.T) { | ||||
| 	directives := []directive{ | ||||
| 		{"dummy", nil}, | ||||
| 		{"dummy2", nil}, | ||||
| 	} | ||||
| 	directiveOrder = directives | ||||
| 	RegisterDirective("foo", nil, "dummy") | ||||
| 	if len(directiveOrder) != 3 { | ||||
| 		t.Fatal("Should have 3 directives now") | ||||
| 	} | ||||
| 	getNames := func() (s []string) { | ||||
| 		for _, d := range directiveOrder { | ||||
| 			s = append(s, d.name) | ||||
| 		} | ||||
| 		return s | ||||
| 	} | ||||
| 	if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) { | ||||
| 		t.Fatalf("directive order doesn't match: %s", getNames()) | ||||
| 	} | ||||
| 	RegisterDirective("bar", nil, "ASDASD") | ||||
| 	if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) { | ||||
| 		t.Fatalf("directive order doesn't match: %s", getNames()) | ||||
| 	} | ||||
| } | ||||
| @ -1,64 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // isLocalhost returns true if host looks explicitly like a localhost address. | ||||
| func isLocalhost(host string) bool { | ||||
| 	return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.") | ||||
| } | ||||
| 
 | ||||
| // checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. | ||||
| func checkFdlimit() { | ||||
| 	const min = 4096 | ||||
| 
 | ||||
| 	// Warn if ulimit is too low for production sites | ||||
| 	if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { | ||||
| 		out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH | ||||
| 		if err == nil { | ||||
| 			// Note that an error here need not be reported | ||||
| 			lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) | ||||
| 			if err == nil && lim < min { | ||||
| 				fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // IsRestart returns whether this process is, according | ||||
| // to env variables, a fork as part of a graceful restart. | ||||
| func IsRestart() bool { | ||||
| 	return startedBefore | ||||
| } | ||||
| 
 | ||||
| // writePidFile writes the process ID to the file at PidFile, if specified. | ||||
| func writePidFile() error { | ||||
| 	pid := []byte(strconv.Itoa(os.Getpid()) + "\n") | ||||
| 	return ioutil.WriteFile(PidFile, pid, 0644) | ||||
| } | ||||
| 
 | ||||
| // CaddyfileInput represents a Caddyfile as input | ||||
| // and is simply a convenient way to implement | ||||
| // the Input interface. | ||||
| type CaddyfileInput struct { | ||||
| 	Filepath string | ||||
| 	Contents []byte | ||||
| 	RealFile bool | ||||
| } | ||||
| 
 | ||||
| // Body returns c.Contents. | ||||
| func (c CaddyfileInput) Body() []byte { return c.Contents } | ||||
| 
 | ||||
| // Path returns c.Filepath. | ||||
| func (c CaddyfileInput) Path() string { return c.Filepath } | ||||
| 
 | ||||
| // IsFile returns true if the original input was a real file on the file system. | ||||
| func (c CaddyfileInput) IsFile() bool { return c.RealFile } | ||||
| @ -1,57 +0,0 @@ | ||||
| package https | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"crypto/ecdsa" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| ) | ||||
| 
 | ||||
| // loadPrivateKey loads a PEM-encoded ECC/RSA private key from file. | ||||
| func loadPrivateKey(file string) (crypto.PrivateKey, error) { | ||||
| 	keyBytes, err := ioutil.ReadFile(file) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	keyBlock, _ := pem.Decode(keyBytes) | ||||
| 
 | ||||
| 	switch keyBlock.Type { | ||||
| 	case "RSA PRIVATE KEY": | ||||
| 		return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) | ||||
| 	case "EC PRIVATE KEY": | ||||
| 		return x509.ParseECPrivateKey(keyBlock.Bytes) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, errors.New("unknown private key type") | ||||
| } | ||||
| 
 | ||||
| // savePrivateKey saves a PEM-encoded ECC/RSA private key to file. | ||||
| func savePrivateKey(key crypto.PrivateKey, file string) error { | ||||
| 	var pemType string | ||||
| 	var keyBytes []byte | ||||
| 	switch key := key.(type) { | ||||
| 	case *ecdsa.PrivateKey: | ||||
| 		var err error | ||||
| 		pemType = "EC" | ||||
| 		keyBytes, err = x509.MarshalECPrivateKey(key) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	case *rsa.PrivateKey: | ||||
| 		pemType = "RSA" | ||||
| 		keyBytes = x509.MarshalPKCS1PrivateKey(key) | ||||
| 	} | ||||
| 
 | ||||
| 	pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} | ||||
| 	keyOut, err := os.Create(file) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	keyOut.Chmod(0600) | ||||
| 	defer keyOut.Close() | ||||
| 	return pem.Encode(keyOut, &pemKey) | ||||
| } | ||||
| @ -1,42 +0,0 @@ | ||||
| package https | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/http/httputil" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| const challengeBasePath = "/.well-known/acme-challenge" | ||||
| 
 | ||||
| // RequestCallback proxies challenge requests to ACME client if the | ||||
| // request path starts with challengeBasePath. It returns true if it | ||||
| // handled the request and no more needs to be done; it returns false | ||||
| // if this call was a no-op and the request still needs handling. | ||||
| func RequestCallback(w http.ResponseWriter, r *http.Request) bool { | ||||
| 	if strings.HasPrefix(r.URL.Path, challengeBasePath) { | ||||
| 		scheme := "http" | ||||
| 		if r.TLS != nil { | ||||
| 			scheme = "https" | ||||
| 		} | ||||
| 
 | ||||
| 		upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort) | ||||
| 		if err != nil { | ||||
| 			w.WriteHeader(http.StatusInternalServerError) | ||||
| 			log.Printf("[ERROR] ACME proxy handler: %v", err) | ||||
| 			return true | ||||
| 		} | ||||
| 
 | ||||
| 		proxy := httputil.NewSingleHostReverseProxy(upstream) | ||||
| 		proxy.Transport = &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs | ||||
| 		} | ||||
| 		proxy.ServeHTTP(w, r) | ||||
| 
 | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	return false | ||||
| } | ||||
| @ -1,411 +0,0 @@ | ||||
| // Package https facilitates the management of TLS assets and integrates | ||||
| // Let's Encrypt functionality into Caddy with first-class support for | ||||
| // creating and renewing certificates automatically. It is designed to | ||||
| // configure sites for HTTPS by default. | ||||
| package https | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/redirect" | ||||
| 	"github.com/mholt/caddy/server" | ||||
| 	"github.com/xenolf/lego/acme" | ||||
| ) | ||||
| 
 | ||||
| // Activate sets up TLS for each server config in configs | ||||
| // as needed; this consists of acquiring and maintaining | ||||
| // certificates and keys for qualifying configs and enabling | ||||
| // OCSP stapling for all TLS-enabled configs. | ||||
| // | ||||
| // This function may prompt the user to provide an email | ||||
| // address if none is available through other means. It | ||||
| // prefers the email address specified in the config, but | ||||
| // if that is not available it will check the command line | ||||
| // argument. If absent, it will use the most recent email | ||||
| // address from last time. If there isn't one, the user | ||||
| // will be prompted and shown SA link. | ||||
| // | ||||
| // Also note that calling this function activates asset | ||||
| // management automatically, which keeps certificates | ||||
| // renewed and OCSP stapling updated. | ||||
| // | ||||
| // Activate returns the updated list of configs, since | ||||
| // some may have been appended, for example, to redirect | ||||
| // plaintext HTTP requests to their HTTPS counterpart. | ||||
| // This function only appends; it does not splice. | ||||
| func Activate(configs []server.Config) ([]server.Config, error) { | ||||
| 	// just in case previous caller forgot... | ||||
| 	Deactivate() | ||||
| 
 | ||||
| 	// pre-screen each config and earmark the ones that qualify for managed TLS | ||||
| 	MarkQualified(configs) | ||||
| 
 | ||||
| 	// place certificates and keys on disk | ||||
| 	err := ObtainCerts(configs, true, false) | ||||
| 	if err != nil { | ||||
| 		return configs, err | ||||
| 	} | ||||
| 
 | ||||
| 	// update TLS configurations | ||||
| 	err = EnableTLS(configs, true) | ||||
| 	if err != nil { | ||||
| 		return configs, err | ||||
| 	} | ||||
| 
 | ||||
| 	// set up redirects | ||||
| 	configs = MakePlaintextRedirects(configs) | ||||
| 
 | ||||
| 	// renew all relevant certificates that need renewal. this is important | ||||
| 	// to do right away for a couple reasons, mainly because each restart, | ||||
| 	// the renewal ticker is reset, so if restarts happen more often than | ||||
| 	// the ticker interval, renewals would never happen. but doing | ||||
| 	// it right away at start guarantees that renewals aren't missed. | ||||
| 	err = renewManagedCertificates(true) | ||||
| 	if err != nil { | ||||
| 		return configs, err | ||||
| 	} | ||||
| 
 | ||||
| 	// keep certificates renewed and OCSP stapling updated | ||||
| 	go maintainAssets(stopChan) | ||||
| 
 | ||||
| 	return configs, nil | ||||
| } | ||||
| 
 | ||||
| // Deactivate cleans up long-term, in-memory resources | ||||
| // allocated by calling Activate(). Essentially, it stops | ||||
| // the asset maintainer from running, meaning that certificates | ||||
| // will not be renewed, OCSP staples will not be updated, etc. | ||||
| func Deactivate() (err error) { | ||||
| 	defer func() { | ||||
| 		if rec := recover(); rec != nil { | ||||
| 			err = errors.New("already deactivated") | ||||
| 		} | ||||
| 	}() | ||||
| 	close(stopChan) | ||||
| 	stopChan = make(chan struct{}) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // MarkQualified scans each config and, if it qualifies for managed | ||||
| // TLS, it sets the Managed field of the TLSConfig to true. | ||||
| func MarkQualified(configs []server.Config) { | ||||
| 	for i := 0; i < len(configs); i++ { | ||||
| 		if ConfigQualifies(configs[i]) { | ||||
| 			configs[i].TLS.Managed = true | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ObtainCerts obtains certificates for all these configs as long as a | ||||
| // certificate does not already exist on disk. It does not modify the | ||||
| // configs at all; it only obtains and stores certificates and keys to | ||||
| // the disk. If allowPrompts is true, the user may be shown a prompt. | ||||
| // If proxyACME is true, the ACME challenges will be proxied to our alt port. | ||||
| func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error { | ||||
| 	// We group configs by email so we don't make the same clients over and | ||||
| 	// over. This has the potential to prompt the user for an email, but we | ||||
| 	// prevent that by assuming that if we already have a listener that can | ||||
| 	// proxy ACME challenge requests, then the server is already running and | ||||
| 	// the operator is no longer present. | ||||
| 	groupedConfigs := groupConfigsByEmail(configs, allowPrompts) | ||||
| 
 | ||||
| 	for email, group := range groupedConfigs { | ||||
| 		// Wait as long as we can before creating the client, because it | ||||
| 		// may not be needed, for example, if we already have what we | ||||
| 		// need on disk. Creating a client involves the network and | ||||
| 		// potentially prompting the user, etc., so only do if necessary. | ||||
| 		var client *ACMEClient | ||||
| 
 | ||||
| 		for _, cfg := range group { | ||||
| 			if !HostQualifies(cfg.Host) || existingCertAndKey(cfg.Host) { | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			// Now we definitely do need a client | ||||
| 			if client == nil { | ||||
| 				var err error | ||||
| 				client, err = NewACMEClient(email, allowPrompts) | ||||
| 				if err != nil { | ||||
| 					return errors.New("error creating client: " + err.Error()) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// c.Configure assumes that allowPrompts == !proxyACME, | ||||
| 			// but that's not always true. For example, a restart where | ||||
| 			// the user isn't present and we're not listening on port 80. | ||||
| 			// TODO: This could probably be refactored better. | ||||
| 			if proxyACME { | ||||
| 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort)) | ||||
| 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort)) | ||||
| 				client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) | ||||
| 			} else { | ||||
| 				client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, "")) | ||||
| 				client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, "")) | ||||
| 				client.ExcludeChallenges([]acme.Challenge{acme.DNS01}) | ||||
| 			} | ||||
| 
 | ||||
| 			err := client.Obtain([]string{cfg.Host}) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // groupConfigsByEmail groups configs by the email address to be used by an | ||||
| // ACME client. It only groups configs that have TLS enabled and that are | ||||
| // marked as Managed. If userPresent is true, the operator MAY be prompted | ||||
| // for an email address. | ||||
| func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config { | ||||
| 	initMap := make(map[string][]server.Config) | ||||
| 	for _, cfg := range configs { | ||||
| 		if !cfg.TLS.Managed { | ||||
| 			continue | ||||
| 		} | ||||
| 		leEmail := getEmail(cfg, userPresent) | ||||
| 		initMap[leEmail] = append(initMap[leEmail], cfg) | ||||
| 	} | ||||
| 	return initMap | ||||
| } | ||||
| 
 | ||||
| // EnableTLS configures each config to use TLS according to default settings. | ||||
| // It will only change configs that are marked as managed, and assumes that | ||||
| // certificates and keys are already on disk. If loadCertificates is true, | ||||
| // the certificates will be loaded from disk into the cache for this process | ||||
| // to use. If false, TLS will still be enabled and configured with default | ||||
| // settings, but no certificates will be parsed loaded into the cache, and | ||||
| // the returned error value will always be nil. | ||||
| func EnableTLS(configs []server.Config, loadCertificates bool) error { | ||||
| 	for i := 0; i < len(configs); i++ { | ||||
| 		if !configs[i].TLS.Managed { | ||||
| 			continue | ||||
| 		} | ||||
| 		configs[i].TLS.Enabled = true | ||||
| 		if loadCertificates && HostQualifies(configs[i].Host) { | ||||
| 			_, err := cacheManagedCertificate(configs[i].Host, false) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		setDefaultTLSParams(&configs[i]) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // hostHasOtherPort returns true if there is another config in the list with the same | ||||
| // hostname that has port otherPort, or false otherwise. All the configs are checked | ||||
| // against the hostname of allConfigs[thisConfigIdx]. | ||||
| func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool { | ||||
| 	for i, otherCfg := range allConfigs { | ||||
| 		if i == thisConfigIdx { | ||||
| 			continue // has to be a config OTHER than the one we're comparing against | ||||
| 		} | ||||
| 		if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS | ||||
| // hosts. You must pass in all configs, not just configs that qualify, since | ||||
| // we must know whether the same host already exists on port 80, and those would | ||||
| // not be in a list of configs that qualify for automatic HTTPS. This function will | ||||
| // only set up redirects for configs that qualify. It returns the updated list of | ||||
| // all configs. | ||||
| func MakePlaintextRedirects(allConfigs []server.Config) []server.Config { | ||||
| 	for i, cfg := range allConfigs { | ||||
| 		if cfg.TLS.Managed && | ||||
| 			!hostHasOtherPort(allConfigs, i, "80") && | ||||
| 			(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) { | ||||
| 			allConfigs = append(allConfigs, redirPlaintextHost(cfg)) | ||||
| 		} | ||||
| 	} | ||||
| 	return allConfigs | ||||
| } | ||||
| 
 | ||||
| // ConfigQualifies returns true if cfg qualifies for | ||||
| // fully managed TLS (but not on-demand TLS, which is | ||||
| // not considered here). It does NOT check to see if a | ||||
| // cert and key already exist for the config. If the | ||||
| // config does qualify, you should set cfg.TLS.Managed | ||||
| // to true and check that instead, because the process of | ||||
| // setting up the config may make it look like it | ||||
| // doesn't qualify even though it originally did. | ||||
| func ConfigQualifies(cfg server.Config) bool { | ||||
| 	return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key | ||||
| 
 | ||||
| 		// user can force-disable automatic HTTPS for this host | ||||
| 		cfg.Scheme != "http" && | ||||
| 		cfg.Port != "80" && | ||||
| 		cfg.TLS.LetsEncryptEmail != "off" && | ||||
| 
 | ||||
| 		// we get can't certs for some kinds of hostnames, but | ||||
| 		// on-demand TLS allows empty hostnames at startup | ||||
| 		(HostQualifies(cfg.Host) || cfg.TLS.OnDemand) | ||||
| } | ||||
| 
 | ||||
| // HostQualifies returns true if the hostname alone | ||||
| // appears eligible for automatic HTTPS. For example, | ||||
| // localhost, empty hostname, and IP addresses are | ||||
| // not eligible because we cannot obtain certificates | ||||
| // for those names. | ||||
| func HostQualifies(hostname string) bool { | ||||
| 	return hostname != "localhost" && // localhost is ineligible | ||||
| 
 | ||||
| 		// hostname must not be empty | ||||
| 		strings.TrimSpace(hostname) != "" && | ||||
| 
 | ||||
| 		// cannot be an IP address, see | ||||
| 		// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt | ||||
| 		// (also trim [] from either end, since that special case can sneak through | ||||
| 		// for IPv6 addresses using the -host flag and with empty/no Caddyfile) | ||||
| 		net.ParseIP(strings.Trim(hostname, "[]")) == nil | ||||
| } | ||||
| 
 | ||||
| // existingCertAndKey returns true if the host has a certificate | ||||
| // and private key in storage already, false otherwise. | ||||
| func existingCertAndKey(host string) bool { | ||||
| 	_, err := os.Stat(storage.SiteCertFile(host)) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	_, err = os.Stat(storage.SiteKeyFile(host)) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| // saveCertResource saves the certificate resource to disk. This | ||||
| // includes the certificate file itself, the private key, and the | ||||
| // metadata file. | ||||
| func saveCertResource(cert acme.CertificateResource) error { | ||||
| 	err := os.MkdirAll(storage.Site(cert.Domain), 0700) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Save cert | ||||
| 	err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Save private key | ||||
| 	err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Save cert metadata | ||||
| 	jsonBytes, err := json.MarshalIndent(&cert, "", "\t") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // redirPlaintextHost returns a new plaintext HTTP configuration for | ||||
| // a virtualHost that simply redirects to cfg, which is assumed to | ||||
| // be the HTTPS configuration. The returned configuration is set | ||||
| // to listen on port 80. | ||||
| func redirPlaintextHost(cfg server.Config) server.Config { | ||||
| 	toURL := "https://{host}" // serve any host, since cfg.Host could be empty | ||||
| 	if cfg.Port != "443" && cfg.Port != "80" { | ||||
| 		toURL += ":" + cfg.Port | ||||
| 	} | ||||
| 
 | ||||
| 	redirMidware := func(next middleware.Handler) middleware.Handler { | ||||
| 		return redirect.Redirect{Next: next, Rules: []redirect.Rule{ | ||||
| 			{ | ||||
| 				FromScheme: "http", | ||||
| 				FromPath:   "/", | ||||
| 				To:         toURL + "{uri}", | ||||
| 				Code:       http.StatusMovedPermanently, | ||||
| 			}, | ||||
| 		}} | ||||
| 	} | ||||
| 
 | ||||
| 	return server.Config{ | ||||
| 		Host:       cfg.Host, | ||||
| 		BindHost:   cfg.BindHost, | ||||
| 		Port:       "80", | ||||
| 		Middleware: []middleware.Middleware{redirMidware}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Revoke revokes the certificate for host via ACME protocol. | ||||
| func Revoke(host string) error { | ||||
| 	if !existingCertAndKey(host) { | ||||
| 		return errors.New("no certificate and key for " + host) | ||||
| 	} | ||||
| 
 | ||||
| 	email := getEmail(server.Config{Host: host}, true) | ||||
| 	if email == "" { | ||||
| 		return errors.New("email is required to revoke") | ||||
| 	} | ||||
| 
 | ||||
| 	client, err := NewACMEClient(email, true) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	certFile := storage.SiteCertFile(host) | ||||
| 	certBytes, err := ioutil.ReadFile(certFile) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = client.RevokeCertificate(certBytes) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = os.Remove(certFile) | ||||
| 	if err != nil { | ||||
| 		return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	// DefaultEmail represents the Let's Encrypt account email to use if none provided | ||||
| 	DefaultEmail string | ||||
| 
 | ||||
| 	// Agreed indicates whether user has agreed to the Let's Encrypt SA | ||||
| 	Agreed bool | ||||
| 
 | ||||
| 	// CAUrl represents the base URL to the CA's ACME endpoint | ||||
| 	CAUrl string | ||||
| ) | ||||
| 
 | ||||
| // AlternatePort is the port on which the acme client will open a | ||||
| // listener and solve the CA's challenges. If this alternate port | ||||
| // is used instead of the default port (80 or 443), then the | ||||
| // default port for the challenge must be forwarded to this one. | ||||
| const AlternatePort = "5033" | ||||
| 
 | ||||
| // KeyType is the type to use for new keys. | ||||
| // This shouldn't need to change except for in tests; | ||||
| // the size can be drastically reduced for speed. | ||||
| var KeyType = acme.RSA2048 | ||||
| 
 | ||||
| // stopChan is used to signal the maintenance goroutine | ||||
| // to terminate. | ||||
| var stopChan chan struct{} | ||||
| @ -1,332 +0,0 @@ | ||||
| package https | ||||
| 
 | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/redirect" | ||||
| 	"github.com/mholt/caddy/server" | ||||
| 	"github.com/xenolf/lego/acme" | ||||
| ) | ||||
| 
 | ||||
| func TestHostQualifies(t *testing.T) { | ||||
| 	for i, test := range []struct { | ||||
| 		host   string | ||||
| 		expect bool | ||||
| 	}{ | ||||
| 		{"localhost", false}, | ||||
| 		{"127.0.0.1", false}, | ||||
| 		{"127.0.1.5", false}, | ||||
| 		{"::1", false}, | ||||
| 		{"[::1]", false}, | ||||
| 		{"[::]", false}, | ||||
| 		{"::", false}, | ||||
| 		{"", false}, | ||||
| 		{" ", false}, | ||||
| 		{"0.0.0.0", false}, | ||||
| 		{"192.168.1.3", false}, | ||||
| 		{"10.0.2.1", false}, | ||||
| 		{"169.112.53.4", false}, | ||||
| 		{"foobar.com", true}, | ||||
| 		{"sub.foobar.com", true}, | ||||
| 	} { | ||||
| 		if HostQualifies(test.host) && !test.expect { | ||||
| 			t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host) | ||||
| 		} | ||||
| 		if !HostQualifies(test.host) && test.expect { | ||||
| 			t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestConfigQualifies(t *testing.T) { | ||||
| 	for i, test := range []struct { | ||||
| 		cfg    server.Config | ||||
| 		expect bool | ||||
| 	}{ | ||||
| 		{server.Config{Host: ""}, false}, | ||||
| 		{server.Config{Host: "localhost"}, false}, | ||||
| 		{server.Config{Host: "123.44.3.21"}, false}, | ||||
| 		{server.Config{Host: "example.com"}, true}, | ||||
| 		{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false}, | ||||
| 		{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false}, | ||||
| 		{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true}, | ||||
| 		{server.Config{Host: "example.com", Scheme: "http"}, false}, | ||||
| 		{server.Config{Host: "example.com", Port: "80"}, false}, | ||||
| 		{server.Config{Host: "example.com", Port: "1234"}, true}, | ||||
| 		{server.Config{Host: "example.com", Scheme: "https"}, true}, | ||||
| 		{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false}, | ||||
| 	} { | ||||
| 		if test.expect && !ConfigQualifies(test.cfg) { | ||||
| 			t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg) | ||||
| 		} | ||||
| 		if !test.expect && ConfigQualifies(test.cfg) { | ||||
| 			t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestRedirPlaintextHost(t *testing.T) { | ||||
| 	cfg := redirPlaintextHost(server.Config{ | ||||
| 		Host:     "example.com", | ||||
| 		BindHost: "93.184.216.34", | ||||
| 		Port:     "1234", | ||||
| 	}) | ||||
| 
 | ||||
| 	// Check host and port | ||||
| 	if actual, expected := cfg.Host, "example.com"; actual != expected { | ||||
| 		t.Errorf("Expected redir config to have host %s but got %s", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected { | ||||
| 		t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := cfg.Port, "80"; actual != expected { | ||||
| 		t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// Make sure redirect handler is set up properly | ||||
| 	if cfg.Middleware == nil || len(cfg.Middleware) != 1 { | ||||
| 		t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware) | ||||
| 	} | ||||
| 
 | ||||
| 	handler, ok := cfg.Middleware[0](nil).(redirect.Redirect) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler) | ||||
| 	} | ||||
| 	if len(handler.Rules) != 1 { | ||||
| 		t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check redirect rule for correctness | ||||
| 	if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected { | ||||
| 		t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected { | ||||
| 		t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected { | ||||
| 		t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected { | ||||
| 		t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// browsers can infer a default port from scheme, so make sure the port | ||||
| 	// doesn't get added in explicitly for default ports like 443 for https. | ||||
| 	cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"}) | ||||
| 	handler, _ = cfg.Middleware[0](nil).(redirect.Redirect) | ||||
| 	if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected { | ||||
| 		t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestSaveCertResource(t *testing.T) { | ||||
| 	storage = Storage("./le_test_save") | ||||
| 	defer func() { | ||||
| 		err := os.RemoveAll(string(storage)) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	domain := "example.com" | ||||
| 	certContents := "certificate" | ||||
| 	keyContents := "private key" | ||||
| 	metaContents := `{ | ||||
| 	"domain": "example.com", | ||||
| 	"certUrl": "https://example.com/cert", | ||||
| 	"certStableUrl": "https://example.com/cert/stable" | ||||
| }` | ||||
| 
 | ||||
| 	cert := acme.CertificateResource{ | ||||
| 		Domain:        domain, | ||||
| 		CertURL:       "https://example.com/cert", | ||||
| 		CertStableURL: "https://example.com/cert/stable", | ||||
| 		PrivateKey:    []byte(keyContents), | ||||
| 		Certificate:   []byte(certContents), | ||||
| 	} | ||||
| 
 | ||||
| 	err := saveCertResource(cert) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error reading certificate file, got: %v", err) | ||||
| 	} | ||||
| 	if string(certFile) != certContents { | ||||
| 		t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile)) | ||||
| 	} | ||||
| 
 | ||||
| 	keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error reading private key file, got: %v", err) | ||||
| 	} | ||||
| 	if string(keyFile) != keyContents { | ||||
| 		t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile)) | ||||
| 	} | ||||
| 
 | ||||
| 	metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error reading meta file, got: %v", err) | ||||
| 	} | ||||
| 	if string(metaFile) != metaContents { | ||||
| 		t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestExistingCertAndKey(t *testing.T) { | ||||
| 	storage = Storage("./le_test_existing") | ||||
| 	defer func() { | ||||
| 		err := os.RemoveAll(string(storage)) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	domain := "example.com" | ||||
| 
 | ||||
| 	if existingCertAndKey(domain) { | ||||
| 		t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain) | ||||
| 	} | ||||
| 
 | ||||
| 	err := saveCertResource(acme.CertificateResource{ | ||||
| 		Domain:      domain, | ||||
| 		PrivateKey:  []byte("key"), | ||||
| 		Certificate: []byte("cert"), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no error, got: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if !existingCertAndKey(domain) { | ||||
| 		t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestHostHasOtherPort(t *testing.T) { | ||||
| 	configs := []server.Config{ | ||||
| 		{Host: "example.com", Port: "80"}, | ||||
| 		{Host: "sub1.example.com", Port: "80"}, | ||||
| 		{Host: "sub1.example.com", Port: "443"}, | ||||
| 	} | ||||
| 
 | ||||
| 	if hostHasOtherPort(configs, 0, "80") { | ||||
| 		t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`) | ||||
| 	} | ||||
| 	if hostHasOtherPort(configs, 0, "443") { | ||||
| 		t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`) | ||||
| 	} | ||||
| 	if !hostHasOtherPort(configs, 1, "443") { | ||||
| 		t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMakePlaintextRedirects(t *testing.T) { | ||||
| 	configs := []server.Config{ | ||||
| 		// Happy path = standard redirect from 80 to 443 | ||||
| 		{Host: "example.com", TLS: server.TLSConfig{Managed: true}}, | ||||
| 
 | ||||
| 		// Host on port 80 already defined; don't change it (no redirect) | ||||
| 		{Host: "sub1.example.com", Port: "80", Scheme: "http"}, | ||||
| 		{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}}, | ||||
| 
 | ||||
| 		// Redirect from port 80 to port 5000 in this case | ||||
| 		{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}}, | ||||
| 
 | ||||
| 		// Can redirect from 80 to either 443 or 5001, but choose 443 | ||||
| 		{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}}, | ||||
| 		{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}}, | ||||
| 	} | ||||
| 
 | ||||
| 	result := MakePlaintextRedirects(configs) | ||||
| 	expectedRedirCount := 3 | ||||
| 
 | ||||
| 	if len(result) != len(configs)+expectedRedirCount { | ||||
| 		t.Errorf("Expected %d redirect(s) to be added, but got %d", | ||||
| 			expectedRedirCount, len(result)-len(configs)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestEnableTLS(t *testing.T) { | ||||
| 	configs := []server.Config{ | ||||
| 		{Host: "example.com", TLS: server.TLSConfig{Managed: true}}, | ||||
| 		{}, // not managed - no changes! | ||||
| 	} | ||||
| 
 | ||||
| 	EnableTLS(configs, false) | ||||
| 
 | ||||
| 	if !configs[0].TLS.Enabled { | ||||
| 		t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false") | ||||
| 	} | ||||
| 	if configs[1].TLS.Enabled { | ||||
| 		t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGroupConfigsByEmail(t *testing.T) { | ||||
| 	if groupConfigsByEmail([]server.Config{}, false) == nil { | ||||
| 		t.Errorf("With empty input, returned map was nil, but expected non-nil map") | ||||
| 	} | ||||
| 
 | ||||
| 	configs := []server.Config{ | ||||
| 		{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}}, | ||||
| 		{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}}, | ||||
| 		{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}}, | ||||
| 		{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}}, | ||||
| 		{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}}, | ||||
| 		{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed | ||||
| 	} | ||||
| 	DefaultEmail = "test@example.com" | ||||
| 
 | ||||
| 	groups := groupConfigsByEmail(configs, true) | ||||
| 
 | ||||
| 	if groups == nil { | ||||
| 		t.Fatalf("Returned map was nil, but expected values") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(groups) != 2 { | ||||
| 		t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups) | ||||
| 	} | ||||
| 	if len(groups["foo@bar"]) != 2 { | ||||
| 		t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"]) | ||||
| 	} | ||||
| 	if len(groups[DefaultEmail]) != 3 { | ||||
| 		t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"]) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMarkQualified(t *testing.T) { | ||||
| 	// TODO: TestConfigQualifies and this test share the same config list... | ||||
| 	configs := []server.Config{ | ||||
| 		{Host: ""}, | ||||
| 		{Host: "localhost"}, | ||||
| 		{Host: "123.44.3.21"}, | ||||
| 		{Host: "example.com"}, | ||||
| 		{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, | ||||
| 		{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, | ||||
| 		{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, | ||||
| 		{Host: "example.com", Scheme: "http"}, | ||||
| 		{Host: "example.com", Port: "80"}, | ||||
| 		{Host: "example.com", Port: "1234"}, | ||||
| 		{Host: "example.com", Scheme: "https"}, | ||||
| 		{Host: "example.com", Port: "80", Scheme: "https"}, | ||||
| 	} | ||||
| 	expectedManagedCount := 4 | ||||
| 
 | ||||
| 	MarkQualified(configs) | ||||
| 
 | ||||
| 	count := 0 | ||||
| 	for _, cfg := range configs { | ||||
| 		if cfg.TLS.Managed { | ||||
| 			count++ | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if count != expectedManagedCount { | ||||
| 		t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count) | ||||
| 	} | ||||
| } | ||||
| @ -1,355 +0,0 @@ | ||||
| package https | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/pem" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy/setup" | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/server" | ||||
| 	"github.com/xenolf/lego/acme" | ||||
| ) | ||||
| 
 | ||||
| // Setup sets up the TLS configuration and installs certificates that | ||||
| // are specified by the user in the config file. All the automatic HTTPS | ||||
| // stuff comes later outside of this function. | ||||
| func Setup(c *setup.Controller) (middleware.Middleware, error) { | ||||
| 	if c.Port == "80" || c.Scheme == "http" { | ||||
| 		c.TLS.Enabled = false | ||||
| 		log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address()) | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	c.TLS.Enabled = true | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		var certificateFile, keyFile, loadDir, maxCerts string | ||||
| 
 | ||||
| 		args := c.RemainingArgs() | ||||
| 		switch len(args) { | ||||
| 		case 1: | ||||
| 			c.TLS.LetsEncryptEmail = args[0] | ||||
| 
 | ||||
| 			// user can force-disable managed TLS this way | ||||
| 			if c.TLS.LetsEncryptEmail == "off" { | ||||
| 				c.TLS.Enabled = false | ||||
| 				return nil, nil | ||||
| 			} | ||||
| 		case 2: | ||||
| 			certificateFile = args[0] | ||||
| 			keyFile = args[1] | ||||
| 			c.TLS.Manual = true | ||||
| 		} | ||||
| 
 | ||||
| 		// Optional block with extra parameters | ||||
| 		var hadBlock bool | ||||
| 		for c.NextBlock() { | ||||
| 			hadBlock = true | ||||
| 			switch c.Val() { | ||||
| 			case "key_type": | ||||
| 				arg := c.RemainingArgs() | ||||
| 				value, ok := supportedKeyTypes[strings.ToUpper(arg[0])] | ||||
| 				if !ok { | ||||
| 					return nil, c.Errf("Wrong KeyType name or KeyType not supported '%s'", c.Val()) | ||||
| 				} | ||||
| 				KeyType = value | ||||
| 			case "protocols": | ||||
| 				args := c.RemainingArgs() | ||||
| 				if len(args) != 2 { | ||||
| 					return nil, c.ArgErr() | ||||
| 				} | ||||
| 				value, ok := supportedProtocols[strings.ToLower(args[0])] | ||||
| 				if !ok { | ||||
| 					return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val()) | ||||
| 				} | ||||
| 				c.TLS.ProtocolMinVersion = value | ||||
| 				value, ok = supportedProtocols[strings.ToLower(args[1])] | ||||
| 				if !ok { | ||||
| 					return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val()) | ||||
| 				} | ||||
| 				c.TLS.ProtocolMaxVersion = value | ||||
| 			case "ciphers": | ||||
| 				for c.NextArg() { | ||||
| 					value, ok := supportedCiphersMap[strings.ToUpper(c.Val())] | ||||
| 					if !ok { | ||||
| 						return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val()) | ||||
| 					} | ||||
| 					c.TLS.Ciphers = append(c.TLS.Ciphers, value) | ||||
| 				} | ||||
| 			case "clients": | ||||
| 				clientCertList := c.RemainingArgs() | ||||
| 				if len(clientCertList) == 0 { | ||||
| 					return nil, c.ArgErr() | ||||
| 				} | ||||
| 
 | ||||
| 				listStart, mustProvideCA := 1, true | ||||
| 				switch clientCertList[0] { | ||||
| 				case "request": | ||||
| 					c.TLS.ClientAuth = tls.RequestClientCert | ||||
| 					mustProvideCA = false | ||||
| 				case "require": | ||||
| 					c.TLS.ClientAuth = tls.RequireAnyClientCert | ||||
| 					mustProvideCA = false | ||||
| 				case "verify_if_given": | ||||
| 					c.TLS.ClientAuth = tls.VerifyClientCertIfGiven | ||||
| 				default: | ||||
| 					c.TLS.ClientAuth = tls.RequireAndVerifyClientCert | ||||
| 					listStart = 0 | ||||
| 				} | ||||
| 				if mustProvideCA && len(clientCertList) <= listStart { | ||||
| 					return nil, c.ArgErr() | ||||
| 				} | ||||
| 
 | ||||
| 				c.TLS.ClientCerts = clientCertList[listStart:] | ||||
| 			case "load": | ||||
| 				c.Args(&loadDir) | ||||
| 				c.TLS.Manual = true | ||||
| 			case "max_certs": | ||||
| 				c.Args(&maxCerts) | ||||
| 				c.TLS.OnDemand = true | ||||
| 			default: | ||||
| 				return nil, c.Errf("Unknown keyword '%s'", c.Val()) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// tls requires at least one argument if a block is not opened | ||||
| 		if len(args) == 0 && !hadBlock { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 
 | ||||
| 		// set certificate limit if on-demand TLS is enabled | ||||
| 		if maxCerts != "" { | ||||
| 			maxCertsNum, err := strconv.Atoi(maxCerts) | ||||
| 			if err != nil || maxCertsNum < 1 { | ||||
| 				return nil, c.Err("max_certs must be a positive integer") | ||||
| 			} | ||||
| 			if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost... | ||||
| 				onDemandMaxIssue = int32(maxCertsNum) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// don't try to load certificates unless we're supposed to | ||||
| 		if !c.TLS.Enabled || !c.TLS.Manual { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// load a single certificate and key, if specified | ||||
| 		if certificateFile != "" && keyFile != "" { | ||||
| 			err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile) | ||||
| 			if err != nil { | ||||
| 				return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err) | ||||
| 			} | ||||
| 			log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile) | ||||
| 		} | ||||
| 
 | ||||
| 		// load a directory of certificates, if specified | ||||
| 		if loadDir != "" { | ||||
| 			err := loadCertsInDir(c, loadDir) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	setDefaultTLSParams(c.Config) | ||||
| 
 | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| // loadCertsInDir loads all the certificates/keys in dir, as long as | ||||
| // the file ends with .pem. This method of loading certificates is | ||||
| // modeled after haproxy, which expects the certificate and key to | ||||
| // be bundled into the same file: | ||||
| // https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt | ||||
| // | ||||
| // This function may write to the log as it walks the directory tree. | ||||
| func loadCertsInDir(c *setup.Controller, dir string) error { | ||||
| 	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			log.Printf("[WARNING] Unable to traverse into %s; skipping", path) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if info.IsDir() { | ||||
| 			return nil | ||||
| 		} | ||||
| 		if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { | ||||
| 			certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer) | ||||
| 			var foundKey bool // use only the first key in the file | ||||
| 
 | ||||
| 			bundle, err := ioutil.ReadFile(path) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			for { | ||||
| 				// Decode next block so we can see what type it is | ||||
| 				var derBlock *pem.Block | ||||
| 				derBlock, bundle = pem.Decode(bundle) | ||||
| 				if derBlock == nil { | ||||
| 					break | ||||
| 				} | ||||
| 
 | ||||
| 				if derBlock.Type == "CERTIFICATE" { | ||||
| 					// Re-encode certificate as PEM, appending to certificate chain | ||||
| 					pem.Encode(certBuilder, derBlock) | ||||
| 				} else if derBlock.Type == "EC PARAMETERS" { | ||||
| 					// EC keys generated from openssl can be composed of two blocks: | ||||
| 					// parameters and key (parameter block should come first) | ||||
| 					if !foundKey { | ||||
| 						// Encode parameters | ||||
| 						pem.Encode(keyBuilder, derBlock) | ||||
| 
 | ||||
| 						// Key must immediately follow | ||||
| 						derBlock, bundle = pem.Decode(bundle) | ||||
| 						if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" { | ||||
| 							return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path) | ||||
| 						} | ||||
| 						pem.Encode(keyBuilder, derBlock) | ||||
| 						foundKey = true | ||||
| 					} | ||||
| 				} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { | ||||
| 					// RSA key | ||||
| 					if !foundKey { | ||||
| 						pem.Encode(keyBuilder, derBlock) | ||||
| 						foundKey = true | ||||
| 					} | ||||
| 				} else { | ||||
| 					return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes() | ||||
| 			if len(certPEMBytes) == 0 { | ||||
| 				return c.Errf("%s: failed to parse PEM data", path) | ||||
| 			} | ||||
| 			if len(keyPEMBytes) == 0 { | ||||
| 				return c.Errf("%s: no private key block found", path) | ||||
| 			} | ||||
| 
 | ||||
| 			err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes) | ||||
| 			if err != nil { | ||||
| 				return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err) | ||||
| 			} | ||||
| 			log.Printf("[INFO] Successfully loaded TLS assets from %s", path) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setDefaultTLSParams sets the default TLS cipher suites, protocol versions, | ||||
| // and server preferences of a server.Config if they were not previously set | ||||
| // (it does not overwrite; only fills in missing values). It will also set the | ||||
| // port to 443 if not already set, TLS is enabled, TLS is manual, and the host | ||||
| // does not equal localhost. | ||||
| func setDefaultTLSParams(c *server.Config) { | ||||
| 	// If no ciphers provided, use default list | ||||
| 	if len(c.TLS.Ciphers) == 0 { | ||||
| 		c.TLS.Ciphers = defaultCiphers | ||||
| 	} | ||||
| 
 | ||||
| 	// Not a cipher suite, but still important for mitigating protocol downgrade attacks | ||||
| 	// (prepend since having it at end breaks http2 due to non-h2-approved suites before it) | ||||
| 	c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...) | ||||
| 
 | ||||
| 	// Set default protocol min and max versions - must balance compatibility and security | ||||
| 	if c.TLS.ProtocolMinVersion == 0 { | ||||
| 		c.TLS.ProtocolMinVersion = tls.VersionTLS10 | ||||
| 	} | ||||
| 	if c.TLS.ProtocolMaxVersion == 0 { | ||||
| 		c.TLS.ProtocolMaxVersion = tls.VersionTLS12 | ||||
| 	} | ||||
| 
 | ||||
| 	// Prefer server cipher suites | ||||
| 	c.TLS.PreferServerCipherSuites = true | ||||
| 
 | ||||
| 	// Default TLS port is 443; only use if port is not manually specified, | ||||
| 	// TLS is enabled, and the host is not localhost | ||||
| 	if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" { | ||||
| 		c.Port = "443" | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Map of supported key types | ||||
| var supportedKeyTypes = map[string]acme.KeyType{ | ||||
| 	"P384":    acme.EC384, | ||||
| 	"P256":    acme.EC256, | ||||
| 	"RSA8192": acme.RSA8192, | ||||
| 	"RSA4096": acme.RSA4096, | ||||
| 	"RSA2048": acme.RSA2048, | ||||
| } | ||||
| 
 | ||||
| // Map of supported protocols. | ||||
| // SSLv3 will be not supported in future release. | ||||
| // HTTP/2 only supports TLS 1.2 and higher. | ||||
| var supportedProtocols = map[string]uint16{ | ||||
| 	"ssl3.0": tls.VersionSSL30, | ||||
| 	"tls1.0": tls.VersionTLS10, | ||||
| 	"tls1.1": tls.VersionTLS11, | ||||
| 	"tls1.2": tls.VersionTLS12, | ||||
| } | ||||
| 
 | ||||
| // Map of supported ciphers, used only for parsing config. | ||||
| // | ||||
| // Note that, at time of writing, HTTP/2 blacklists 276 cipher suites, | ||||
| // including all but two of the suites below (the two GCM suites). | ||||
| // See https://http2.github.io/http2-spec/#BadCipherSuites | ||||
| // | ||||
| // TLS_FALLBACK_SCSV is not in this list because we manually ensure | ||||
| // it is always added (even though it is not technically a cipher suite). | ||||
| // | ||||
| // This map, like any map, is NOT ORDERED. Do not range over this map. | ||||
| var supportedCiphersMap = map[string]uint16{ | ||||
| 	"ECDHE-RSA-AES256-GCM-SHA384":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||
| 	"ECDHE-RSA-AES128-GCM-SHA256":   tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 	"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 	"ECDHE-RSA-AES128-CBC-SHA":      tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	"ECDHE-RSA-AES256-CBC-SHA":      tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	"ECDHE-ECDSA-AES256-CBC-SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, | ||||
| 	"ECDHE-ECDSA-AES128-CBC-SHA":    tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | ||||
| 	"RSA-AES128-CBC-SHA":            tls.TLS_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	"RSA-AES256-CBC-SHA":            tls.TLS_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	"ECDHE-RSA-3DES-EDE-CBC-SHA":    tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, | ||||
| 	"RSA-3DES-EDE-CBC-SHA":          tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, | ||||
| } | ||||
| 
 | ||||
| // List of supported cipher suites in descending order of preference. | ||||
| // Ordering is very important! Getting the wrong order will break | ||||
| // mainstream clients, especially with HTTP/2. | ||||
| // | ||||
| // Note that TLS_FALLBACK_SCSV is not in this list since it is always | ||||
| // added manually. | ||||
| var supportedCiphers = []uint16{ | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | ||||
| 	tls.TLS_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	tls.TLS_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, | ||||
| 	tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, | ||||
| } | ||||
| 
 | ||||
| // List of all the ciphers we want to use by default | ||||
| var defaultCiphers = []uint16{ | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, | ||||
| 	tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, | ||||
| 	tls.TLS_RSA_WITH_AES_256_CBC_SHA, | ||||
| 	tls.TLS_RSA_WITH_AES_128_CBC_SHA, | ||||
| } | ||||
							
								
								
									
										7
									
								
								caddy/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								caddy/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| package main | ||||
| 
 | ||||
| import "github.com/mholt/caddy/caddy/caddymain" | ||||
| 
 | ||||
| func main() { | ||||
| 	caddymain.Run() | ||||
| } | ||||
| @ -1,165 +0,0 @@ | ||||
| package parse | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| type lexerTestCase struct { | ||||
| 	input    string | ||||
| 	expected []token | ||||
| } | ||||
| 
 | ||||
| func TestLexer(t *testing.T) { | ||||
| 	testCases := []lexerTestCase{ | ||||
| 		{ | ||||
| 			input: `host:123`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "host:123"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 | ||||
| 
 | ||||
| 					directive`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "host:123"}, | ||||
| 				{line: 3, text: "directive"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 { | ||||
| 						directive | ||||
| 					}`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "host:123"}, | ||||
| 				{line: 1, text: "{"}, | ||||
| 				{line: 2, text: "directive"}, | ||||
| 				{line: 3, text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 { directive }`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "host:123"}, | ||||
| 				{line: 1, text: "{"}, | ||||
| 				{line: 1, text: "directive"}, | ||||
| 				{line: 1, text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 { | ||||
| 						#comment | ||||
| 						directive | ||||
| 						# comment | ||||
| 						foobar # another comment | ||||
| 					}`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "host:123"}, | ||||
| 				{line: 1, text: "{"}, | ||||
| 				{line: 3, text: "directive"}, | ||||
| 				{line: 5, text: "foobar"}, | ||||
| 				{line: 6, text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `a "quoted value" b | ||||
| 					foobar`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "a"}, | ||||
| 				{line: 1, text: "quoted value"}, | ||||
| 				{line: 1, text: "b"}, | ||||
| 				{line: 2, text: "foobar"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `A "quoted \"value\" inside" B`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "A"}, | ||||
| 				{line: 1, text: `quoted "value" inside`}, | ||||
| 				{line: 1, text: "B"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `"don't\escape"`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: `don't\escape`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `"don't\\escape"`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: `don't\\escape`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `A "quoted value with line | ||||
| 					break inside" { | ||||
| 						foobar | ||||
| 					}`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "A"}, | ||||
| 				{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"}, | ||||
| 				{line: 2, text: "{"}, | ||||
| 				{line: 3, text: "foobar"}, | ||||
| 				{line: 4, text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `"C:\php\php-cgi.exe"`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: `C:\php\php-cgi.exe`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `empty "" string`, | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: `empty`}, | ||||
| 				{line: 1, text: ``}, | ||||
| 				{line: 1, text: `string`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: "skip those\r\nCR characters", | ||||
| 			expected: []token{ | ||||
| 				{line: 1, text: "skip"}, | ||||
| 				{line: 1, text: "those"}, | ||||
| 				{line: 2, text: "CR"}, | ||||
| 				{line: 2, text: "characters"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for i, testCase := range testCases { | ||||
| 		actual := tokenize(testCase.input) | ||||
| 		lexerCompare(t, i, testCase.expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func tokenize(input string) (tokens []token) { | ||||
| 	l := lexer{} | ||||
| 	l.load(strings.NewReader(input)) | ||||
| 	for l.next() { | ||||
| 		tokens = append(tokens, l.token) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func lexerCompare(t *testing.T, n int, expected, actual []token) { | ||||
| 	if len(expected) != len(actual) { | ||||
| 		t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) | ||||
| 	} | ||||
| 
 | ||||
| 	for i := 0; i < len(actual) && i < len(expected); i++ { | ||||
| 		if actual[i].line != expected[i].line { | ||||
| 			t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", | ||||
| 				n, i, expected[i].text, expected[i].line, actual[i].line) | ||||
| 			break | ||||
| 		} | ||||
| 		if actual[i].text != expected[i].text { | ||||
| 			t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", | ||||
| 				n, i, expected[i].text, actual[i].text) | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -1,32 +0,0 @@ | ||||
| // Package parse provides facilities for parsing configuration files. | ||||
| package parse | ||||
| 
 | ||||
| import "io" | ||||
| 
 | ||||
| // ServerBlocks parses the input just enough to organize tokens, | ||||
| // in order, by server block. No further parsing is performed. | ||||
| // If checkDirectives is true, only valid directives will be allowed | ||||
| // otherwise we consider it a parse error. Server blocks are returned | ||||
| // in the order in which they appear. | ||||
| func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) { | ||||
| 	p := parser{Dispenser: NewDispenser(filename, input)} | ||||
| 	p.checkDirectives = checkDirectives | ||||
| 	blocks, err := p.parseAll() | ||||
| 	return blocks, err | ||||
| } | ||||
| 
 | ||||
| // allTokens lexes the entire input, but does not parse it. | ||||
| // It returns all the tokens from the input, unstructured | ||||
| // and in order. | ||||
| func allTokens(input io.Reader) (tokens []token) { | ||||
| 	l := new(lexer) | ||||
| 	l.load(input) | ||||
| 	for l.next() { | ||||
| 		tokens = append(tokens, l.token) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // ValidDirectives is a set of directives that are valid (unordered). Populated | ||||
| // by config package's init function. | ||||
| var ValidDirectives = make(map[string]struct{}) | ||||
| @ -1,22 +0,0 @@ | ||||
| package parse | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestAllTokens(t *testing.T) { | ||||
| 	input := strings.NewReader("a b c\nd e") | ||||
| 	expected := []string{"a", "b", "c", "d", "e"} | ||||
| 	tokens := allTokens(input) | ||||
| 
 | ||||
| 	if len(tokens) != len(expected) { | ||||
| 		t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) | ||||
| 	} | ||||
| 
 | ||||
| 	for i, val := range expected { | ||||
| 		if tokens[i].text != val { | ||||
| 			t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -1,480 +0,0 @@ | ||||
| package parse | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestStandardAddress(t *testing.T) { | ||||
| 	for i, test := range []struct { | ||||
| 		input              string | ||||
| 		scheme, host, port string | ||||
| 		shouldErr          bool | ||||
| 	}{ | ||||
| 		{`localhost`, "", "localhost", "", false}, | ||||
| 		{`LOCALHOST`, "", "localhost", "", false}, | ||||
| 		{`localhost:1234`, "", "localhost", "1234", false}, | ||||
| 		{`LOCALHOST:1234`, "", "localhost", "1234", false}, | ||||
| 		{`localhost:`, "", "localhost", "", false}, | ||||
| 		{`0.0.0.0`, "", "0.0.0.0", "", false}, | ||||
| 		{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false}, | ||||
| 		{`:1234`, "", "", "1234", false}, | ||||
| 		{`[::1]`, "", "::1", "", false}, | ||||
| 		{`[::1]:1234`, "", "::1", "1234", false}, | ||||
| 		{`:`, "", "", "", false}, | ||||
| 		{`localhost:http`, "http", "localhost", "80", false}, | ||||
| 		{`localhost:https`, "https", "localhost", "443", false}, | ||||
| 		{`:http`, "http", "", "80", false}, | ||||
| 		{`:https`, "https", "", "443", false}, | ||||
| 		{`http://localhost:https`, "", "", "", true}, // conflict | ||||
| 		{`http://localhost:http`, "", "", "", true},  // repeated scheme | ||||
| 		{`http://localhost:443`, "", "", "", true},   // not conventional | ||||
| 		{`https://localhost:80`, "", "", "", true},   // not conventional | ||||
| 		{`http://localhost`, "http", "localhost", "80", false}, | ||||
| 		{`https://localhost`, "https", "localhost", "443", false}, | ||||
| 		{`http://127.0.0.1`, "http", "127.0.0.1", "80", false}, | ||||
| 		{`https://127.0.0.1`, "https", "127.0.0.1", "443", false}, | ||||
| 		{`http://[::1]`, "http", "::1", "80", false}, | ||||
| 		{`http://localhost:1234`, "http", "localhost", "1234", false}, | ||||
| 		{`http://LOCALHOST:1234`, "http", "localhost", "1234", false}, | ||||
| 		{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false}, | ||||
| 		{`http://[::1]:1234`, "http", "::1", "1234", false}, | ||||
| 		{``, "", "", "", false}, | ||||
| 		{`::1`, "", "::1", "", true}, | ||||
| 		{`localhost::`, "", "localhost::", "", true}, | ||||
| 		{`#$%@`, "", "#$%@", "", true}, | ||||
| 	} { | ||||
| 		actual, err := standardAddress(test.input) | ||||
| 
 | ||||
| 		if err != nil && !test.shouldErr { | ||||
| 			t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) | ||||
| 		} | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Errorf("Test %d (%s): Expected error, but had none", i, test.input) | ||||
| 		} | ||||
| 
 | ||||
| 		if actual.Scheme != test.scheme { | ||||
| 			t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme) | ||||
| 		} | ||||
| 		if actual.Host != test.host { | ||||
| 			t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host) | ||||
| 		} | ||||
| 		if actual.Port != test.port { | ||||
| 			t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestParseOneAndImport(t *testing.T) { | ||||
| 	setupParseTests() | ||||
| 
 | ||||
| 	testParseOne := func(input string) (ServerBlock, error) { | ||||
| 		p := testParser(input) | ||||
| 		p.Next() // parseOne doesn't call Next() to start, so we must | ||||
| 		err := p.parseOne() | ||||
| 		return p.block, err | ||||
| 	} | ||||
| 
 | ||||
| 	for i, test := range []struct { | ||||
| 		input     string | ||||
| 		shouldErr bool | ||||
| 		addresses []address | ||||
| 		tokens    map[string]int // map of directive name to number of tokens expected | ||||
| 	}{ | ||||
| 		{`localhost`, false, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1`, false, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234 | ||||
| 		  dir1 foo bar`, false, []address{ | ||||
| 			{"localhost:1234", "", "localhost", "1234"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost { | ||||
| 		    dir1 | ||||
| 		  }`, false, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234 { | ||||
| 		    dir1 foo bar | ||||
| 		    dir2 | ||||
| 		  }`, false, []address{ | ||||
| 			{"localhost:1234", "", "localhost", "1234"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 			"dir2": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost https://localhost | ||||
| 		  dir1 foo bar`, false, []address{ | ||||
| 			{"http://localhost", "http", "localhost", "80"}, | ||||
| 			{"https://localhost", "https", "localhost", "443"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost https://localhost { | ||||
| 		    dir1 foo bar | ||||
| 		  }`, false, []address{ | ||||
| 			{"http://localhost", "http", "localhost", "80"}, | ||||
| 			{"https://localhost", "https", "localhost", "443"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost, https://localhost { | ||||
| 		    dir1 foo bar | ||||
| 		  }`, false, []address{ | ||||
| 			{"http://localhost", "http", "localhost", "80"}, | ||||
| 			{"https://localhost", "https", "localhost", "443"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost, { | ||||
| 		  }`, true, []address{ | ||||
| 			{"http://localhost", "http", "localhost", "80"}, | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`host1:80, http://host2.com | ||||
| 		  dir1 foo bar | ||||
| 		  dir2 baz`, false, []address{ | ||||
| 			{"host1:80", "", "host1", "80"}, | ||||
| 			{"http://host2.com", "http", "host2.com", "80"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 			"dir2": 2, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://host1.com, | ||||
| 		  http://host2.com, | ||||
| 		  https://host3.com`, false, []address{ | ||||
| 			{"http://host1.com", "http", "host1.com", "80"}, | ||||
| 			{"http://host2.com", "http", "host2.com", "80"}, | ||||
| 			{"https://host3.com", "https", "host3.com", "443"}, | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`http://host1.com:1234, https://host2.com | ||||
| 		  dir1 foo { | ||||
| 		    bar baz | ||||
| 		  } | ||||
| 		  dir2`, false, []address{ | ||||
| 			{"http://host1.com:1234", "http", "host1.com", "1234"}, | ||||
| 			{"https://host2.com", "https", "host2.com", "443"}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 6, | ||||
| 			"dir2": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`127.0.0.1 | ||||
| 		  dir1 { | ||||
| 		    bar baz | ||||
| 		  } | ||||
| 		  dir2 { | ||||
| 		    foo bar | ||||
| 		  }`, false, []address{ | ||||
| 			{"127.0.0.1", "", "127.0.0.1", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 5, | ||||
| 			"dir2": 5, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`127.0.0.1 | ||||
| 		  unknown_directive`, true, []address{ | ||||
| 			{"127.0.0.1", "", "127.0.0.1", ""}, | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		    foo`, true, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		  }`, false, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		  } }`, true, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		    nested { | ||||
| 		      foo | ||||
| 		    } | ||||
| 		  } | ||||
| 		  dir2 foo bar`, false, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 7, | ||||
| 			"dir2": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{``, false, []address{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 arg1 | ||||
| 		  import import_test1.txt`, false, []address{ | ||||
| 			{"localhost", "", "localhost", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 2, | ||||
| 			"dir2": 3, | ||||
| 			"dir3": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`import import_test2.txt`, false, []address{ | ||||
| 			{"host1", "", "host1", ""}, | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 1, | ||||
| 			"dir2": 2, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`import not_found.txt`, true, []address{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`""`, false, []address{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{``, false, []address{}, map[string]int{}}, | ||||
| 	} { | ||||
| 		result, err := testParseOne(test.input) | ||||
| 
 | ||||
| 		if test.shouldErr && err == nil { | ||||
| 			t.Errorf("Test %d: Expected an error, but didn't get one", i) | ||||
| 		} | ||||
| 		if !test.shouldErr && err != nil { | ||||
| 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if len(result.Addresses) != len(test.addresses) { | ||||
| 			t.Errorf("Test %d: Expected %d addresses, got %d", | ||||
| 				i, len(test.addresses), len(result.Addresses)) | ||||
| 			continue | ||||
| 		} | ||||
| 		for j, addr := range result.Addresses { | ||||
| 			if addr.Host != test.addresses[j].Host { | ||||
| 				t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'", | ||||
| 					i, j, test.addresses[j].Host, addr.Host) | ||||
| 			} | ||||
| 			if addr.Port != test.addresses[j].Port { | ||||
| 				t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'", | ||||
| 					i, j, test.addresses[j].Port, addr.Port) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if len(result.Tokens) != len(test.tokens) { | ||||
| 			t.Errorf("Test %d: Expected %d directives, had %d", | ||||
| 				i, len(test.tokens), len(result.Tokens)) | ||||
| 			continue | ||||
| 		} | ||||
| 		for directive, tokens := range result.Tokens { | ||||
| 			if len(tokens) != test.tokens[directive] { | ||||
| 				t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d", | ||||
| 					i, directive, test.tokens[directive], len(tokens)) | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestParseAll(t *testing.T) { | ||||
| 	setupParseTests() | ||||
| 
 | ||||
| 	for i, test := range []struct { | ||||
| 		input     string | ||||
| 		shouldErr bool | ||||
| 		addresses [][]address // addresses per server block, in order | ||||
| 	}{ | ||||
| 		{`localhost`, false, [][]address{ | ||||
| 			{{"localhost", "", "localhost", ""}}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234`, false, [][]address{ | ||||
| 			{{"localhost:1234", "", "localhost", "1234"}}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234 { | ||||
| 		  } | ||||
| 		  localhost:2015 { | ||||
| 		  }`, false, [][]address{ | ||||
| 			{{"localhost:1234", "", "localhost", "1234"}}, | ||||
| 			{{"localhost:2015", "", "localhost", "2015"}}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234, http://host2`, false, [][]address{ | ||||
| 			{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234, http://host2,`, true, [][]address{}}, | ||||
| 
 | ||||
| 		{`http://host1.com, http://host2.com { | ||||
| 		  } | ||||
| 		  https://host3.com, https://host4.com { | ||||
| 		  }`, false, [][]address{ | ||||
| 			{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}}, | ||||
| 			{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`import import_glob*.txt`, false, [][]address{ | ||||
| 			{{"glob0.host0", "", "glob0.host0", ""}}, | ||||
| 			{{"glob0.host1", "", "glob0.host1", ""}}, | ||||
| 			{{"glob1.host0", "", "glob1.host0", ""}}, | ||||
| 			{{"glob2.host0", "", "glob2.host0", ""}}, | ||||
| 		}}, | ||||
| 	} { | ||||
| 		p := testParser(test.input) | ||||
| 		blocks, err := p.parseAll() | ||||
| 
 | ||||
| 		if test.shouldErr && err == nil { | ||||
| 			t.Errorf("Test %d: Expected an error, but didn't get one", i) | ||||
| 		} | ||||
| 		if !test.shouldErr && err != nil { | ||||
| 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if len(blocks) != len(test.addresses) { | ||||
| 			t.Errorf("Test %d: Expected %d server blocks, got %d", | ||||
| 				i, len(test.addresses), len(blocks)) | ||||
| 			continue | ||||
| 		} | ||||
| 		for j, block := range blocks { | ||||
| 			if len(block.Addresses) != len(test.addresses[j]) { | ||||
| 				t.Errorf("Test %d: Expected %d addresses in block %d, got %d", | ||||
| 					i, len(test.addresses[j]), j, len(block.Addresses)) | ||||
| 				continue | ||||
| 			} | ||||
| 			for k, addr := range block.Addresses { | ||||
| 				if addr.Host != test.addresses[j][k].Host { | ||||
| 					t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'", | ||||
| 						i, j, k, test.addresses[j][k].Host, addr.Host) | ||||
| 				} | ||||
| 				if addr.Port != test.addresses[j][k].Port { | ||||
| 					t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'", | ||||
| 						i, j, k, test.addresses[j][k].Port, addr.Port) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestEnvironmentReplacement(t *testing.T) { | ||||
| 	setupParseTests() | ||||
| 
 | ||||
| 	os.Setenv("PORT", "8080") | ||||
| 	os.Setenv("ADDRESS", "servername.com") | ||||
| 	os.Setenv("FOOBAR", "foobar") | ||||
| 
 | ||||
| 	// basic test; unix-style env vars | ||||
| 	p := testParser(`{$ADDRESS}`) | ||||
| 	blocks, _ := p.parseAll() | ||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { | ||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// multiple vars per token | ||||
| 	p = testParser(`{$ADDRESS}:{$PORT}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { | ||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { | ||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// windows-style var and unix style in same token | ||||
| 	p = testParser(`{%ADDRESS%}:{$PORT}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { | ||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { | ||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// reverse order | ||||
| 	p = testParser(`{$ADDRESS}:{%PORT%}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual { | ||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { | ||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// env var in server block body as argument | ||||
| 	p = testParser(":{%PORT%}\ndir1 {$FOOBAR}") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual { | ||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual { | ||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// combined windows env vars in argument | ||||
| 	p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual { | ||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// malformed env var (windows) | ||||
| 	p = testParser(":1234\ndir1 {%ADDRESS}") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual { | ||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// malformed (non-existent) env var (unix) | ||||
| 	p = testParser(`:{$PORT$}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual { | ||||
| 		t.Errorf("Expected port to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// in quoted field | ||||
| 	p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual { | ||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func setupParseTests() { | ||||
| 	// Set up some bogus directives for testing | ||||
| 	ValidDirectives = map[string]struct{}{ | ||||
| 		"dir1": {}, | ||||
| 		"dir2": {}, | ||||
| 		"dir3": {}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func testParser(input string) parser { | ||||
| 	buf := strings.NewReader(input) | ||||
| 	p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true} | ||||
| 	return p | ||||
| } | ||||
| @ -1,92 +0,0 @@ | ||||
| // +build !windows | ||||
| 
 | ||||
| package caddy | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy/https" | ||||
| ) | ||||
| 
 | ||||
| // Restart restarts the entire application; gracefully with zero | ||||
| // downtime if on a POSIX-compatible system, or forcefully if on | ||||
| // Windows but with imperceptibly-short downtime. | ||||
| // | ||||
| // The behavior can be controlled by the RestartMode variable, | ||||
| // where "inproc" will restart forcefully in process same as | ||||
| // Windows on a POSIX-compatible system. | ||||
| // | ||||
| // The restarted application will use newCaddyfile as its input | ||||
| // configuration. If newCaddyfile is nil, the current (existing) | ||||
| // Caddyfile configuration will be used. | ||||
| // | ||||
| // Note: The process must exist in the same place on the disk in | ||||
| // order for this to work. Thus, multiple graceful restarts don't | ||||
| // work if executing with `go run`, since the binary is cleaned up | ||||
| // when `go run` sees the initial parent process exit. | ||||
| func Restart(newCaddyfile Input) error { | ||||
| 	log.Println("[INFO] Restarting") | ||||
| 
 | ||||
| 	if newCaddyfile == nil { | ||||
| 		caddyfileMu.Lock() | ||||
| 		newCaddyfile = caddyfile | ||||
| 		caddyfileMu.Unlock() | ||||
| 	} | ||||
| 
 | ||||
| 	// Get certificates for any new hosts in the new Caddyfile without causing downtime | ||||
| 	err := getCertsForNewCaddyfile(newCaddyfile) | ||||
| 	if err != nil { | ||||
| 		return errors.New("TLS preload: " + err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Add file descriptors of all the sockets for new instance | ||||
| 	serversMu.Lock() | ||||
| 	for _, s := range servers { | ||||
| 		restartFds[s.Addr] = s.ListenerFd() | ||||
| 	} | ||||
| 	serversMu.Unlock() | ||||
| 
 | ||||
| 	return restartInProc(newCaddyfile) | ||||
| } | ||||
| 
 | ||||
| func getCertsForNewCaddyfile(newCaddyfile Input) error { | ||||
| 	// parse the new caddyfile only up to (and including) TLS | ||||
| 	// so we can know what we need to get certs for. | ||||
| 	configs, _, _, err := loadConfigsUpToIncludingTLS(filepath.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body())) | ||||
| 	if err != nil { | ||||
| 		return errors.New("loading Caddyfile: " + err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// first mark the configs that are qualified for managed TLS | ||||
| 	https.MarkQualified(configs) | ||||
| 
 | ||||
| 	// since we group by bind address to obtain certs, we must call | ||||
| 	// EnableTLS to make sure the port is set properly first | ||||
| 	// (can ignore error since we aren't actually using the certs) | ||||
| 	https.EnableTLS(configs, false) | ||||
| 
 | ||||
| 	// find out if we can let the acme package start its own challenge listener | ||||
| 	// on port 80 | ||||
| 	var proxyACME bool | ||||
| 	serversMu.Lock() | ||||
| 	for _, s := range servers { | ||||
| 		_, port, _ := net.SplitHostPort(s.Addr) | ||||
| 		if port == "80" { | ||||
| 			proxyACME = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	serversMu.Unlock() | ||||
| 
 | ||||
| 	// place certs on the disk | ||||
| 	err = https.ObtainCerts(configs, false, proxyACME) | ||||
| 	if err != nil { | ||||
| 		return errors.New("obtaining certs: " + err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| @ -1,17 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import "log" | ||||
| 
 | ||||
| // Restart restarts Caddy forcefully using newCaddyfile, | ||||
| // or, if nil, the current/existing Caddyfile is reused. | ||||
| func Restart(newCaddyfile Input) error { | ||||
| 	log.Println("[INFO] Restarting") | ||||
| 
 | ||||
| 	if newCaddyfile == nil { | ||||
| 		caddyfileMu.Lock() | ||||
| 		newCaddyfile = caddyfile | ||||
| 		caddyfileMu.Unlock() | ||||
| 	} | ||||
| 
 | ||||
| 	return restartInProc(newCaddyfile) | ||||
| } | ||||
| @ -1,28 +0,0 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import "log" | ||||
| 
 | ||||
| // restartInProc restarts Caddy forcefully in process using newCaddyfile. | ||||
| func restartInProc(newCaddyfile Input) error { | ||||
| 	wg.Add(1) // barrier so Wait() doesn't unblock | ||||
| 	defer wg.Done() | ||||
| 
 | ||||
| 	err := Stop() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	caddyfileMu.Lock() | ||||
| 	oldCaddyfile := caddyfile | ||||
| 	caddyfileMu.Unlock() | ||||
| 
 | ||||
| 	err = Start(newCaddyfile) | ||||
| 	if err != nil { | ||||
| 		// revert to old Caddyfile | ||||
| 		if oldErr := Start(oldCaddyfile); oldErr != nil { | ||||
| 			log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| @ -1,13 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import "github.com/mholt/caddy/middleware" | ||||
| 
 | ||||
| // BindHost sets the host to bind the listener to. | ||||
| func BindHost(c *Controller) (middleware.Middleware, error) { | ||||
| 	for c.Next() { | ||||
| 		if !c.Args(&c.BindHost) { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
| @ -1,83 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy/parse" | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/server" | ||||
| ) | ||||
| 
 | ||||
| // Controller is given to the setup function of middlewares which | ||||
| // gives them access to be able to read tokens and set config. Each | ||||
| // virtualhost gets their own server config and dispenser. | ||||
| type Controller struct { | ||||
| 	*server.Config | ||||
| 	parse.Dispenser | ||||
| 
 | ||||
| 	// OncePerServerBlock is a function that executes f | ||||
| 	// exactly once per server block, no matter how many | ||||
| 	// hosts are associated with it. If it is the first | ||||
| 	// time, the function f is executed immediately | ||||
| 	// (not deferred) and may return an error which is | ||||
| 	// returned by OncePerServerBlock. | ||||
| 	OncePerServerBlock func(f func() error) error | ||||
| 
 | ||||
| 	// ServerBlockIndex is the 0-based index of the | ||||
| 	// server block as it appeared in the input. | ||||
| 	ServerBlockIndex int | ||||
| 
 | ||||
| 	// ServerBlockHostIndex is the 0-based index of this | ||||
| 	// host as it appeared in the input at the head of the | ||||
| 	// server block. | ||||
| 	ServerBlockHostIndex int | ||||
| 
 | ||||
| 	// ServerBlockHosts is a list of hosts that are | ||||
| 	// associated with this server block. All these | ||||
| 	// hosts, consequently, share the same tokens. | ||||
| 	ServerBlockHosts []string | ||||
| 
 | ||||
| 	// ServerBlockStorage is used by a directive's | ||||
| 	// setup function to persist state between all | ||||
| 	// the hosts on a server block. | ||||
| 	ServerBlockStorage interface{} | ||||
| } | ||||
| 
 | ||||
| // NewTestController creates a new *Controller for | ||||
| // the input specified, with a filename of "Testfile". | ||||
| // The Config is bare, consisting only of a Root of cwd. | ||||
| // | ||||
| // Used primarily for testing but needs to be exported so | ||||
| // add-ons can use this as a convenience. Does not initialize | ||||
| // the server-block-related fields. | ||||
| func NewTestController(input string) *Controller { | ||||
| 	return &Controller{ | ||||
| 		Config: &server.Config{ | ||||
| 			Root: ".", | ||||
| 		}, | ||||
| 		Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), | ||||
| 		OncePerServerBlock: func(f func() error) error { | ||||
| 			return f() | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // EmptyNext is a no-op function that can be passed into | ||||
| // middleware.Middleware functions so that the assignment | ||||
| // to the Next field of the Handler can be tested. | ||||
| // | ||||
| // Used primarily for testing but needs to be exported so | ||||
| // add-ons can use this as a convenience. | ||||
| var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	return 0, nil | ||||
| }) | ||||
| 
 | ||||
| // SameNext does a pointer comparison between next1 and next2. | ||||
| // | ||||
| // Used primarily for testing but needs to be exported so | ||||
| // add-ons can use this as a convenience. | ||||
| func SameNext(next1, next2 middleware.Handler) bool { | ||||
| 	return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2) | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	stdexpvar "expvar" | ||||
| 	"runtime" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/expvar" | ||||
| ) | ||||
| 
 | ||||
| // ExpVar configures a new ExpVar middleware instance. | ||||
| func ExpVar(c *Controller) (middleware.Middleware, error) { | ||||
| 	resource, err := expVarParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// publish any extra information/metrics we may want to capture | ||||
| 	publishExtraVars() | ||||
| 
 | ||||
| 	expvar := expvar.ExpVar{Resource: resource} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		expvar.Next = next | ||||
| 		return expvar | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func expVarParse(c *Controller) (expvar.Resource, error) { | ||||
| 	var resource expvar.Resource | ||||
| 	var err error | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		args := c.RemainingArgs() | ||||
| 		switch len(args) { | ||||
| 		case 0: | ||||
| 			resource = expvar.Resource(defaultExpvarPath) | ||||
| 		case 1: | ||||
| 			resource = expvar.Resource(args[0]) | ||||
| 		default: | ||||
| 			return resource, c.ArgErr() | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return resource, err | ||||
| } | ||||
| 
 | ||||
| func publishExtraVars() { | ||||
| 	// By using sync.Once instead of an init() function, we don't clutter | ||||
| 	// the app's expvar export unnecessarily, or risk colliding with it. | ||||
| 	publishOnce.Do(func() { | ||||
| 		stdexpvar.Publish("Goroutines", stdexpvar.Func(func() interface{} { | ||||
| 			return runtime.NumGoroutine() | ||||
| 		})) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| var publishOnce sync.Once // publishing variables should only be done once | ||||
| var defaultExpvarPath = "/debug/vars" | ||||
| @ -1,39 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/expvar" | ||||
| ) | ||||
| 
 | ||||
| func TestExpvar(t *testing.T) { | ||||
| 	c := NewTestController(`expvar`) | ||||
| 	mid, err := ExpVar(c) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	} | ||||
| 
 | ||||
| 	c = NewTestController(`expvar /d/v`) | ||||
| 	mid, err = ExpVar(c) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(expvar.ExpVar) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler) | ||||
| 	} | ||||
| 	if myHandler.Resource != "/d/v" { | ||||
| 		t.Errorf("Expected /d/v as expvar resource") | ||||
| 	} | ||||
| 	if !SameNext(myHandler.Next, EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| } | ||||
| @ -1,55 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/extensions" | ||||
| ) | ||||
| 
 | ||||
| // Ext configures a new instance of 'extensions' middleware for clean URLs. | ||||
| func Ext(c *Controller) (middleware.Middleware, error) { | ||||
| 	root := c.Root | ||||
| 
 | ||||
| 	exts, err := extParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return extensions.Ext{ | ||||
| 			Next:       next, | ||||
| 			Extensions: exts, | ||||
| 			Root:       root, | ||||
| 		} | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // extParse sets up an instance of extension middleware | ||||
| // from a middleware controller and returns a list of extensions. | ||||
| func extParse(c *Controller) ([]string, error) { | ||||
| 	var exts []string | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		// At least one extension is required | ||||
| 		if !c.NextArg() { | ||||
| 			return exts, c.ArgErr() | ||||
| 		} | ||||
| 		exts = append(exts, c.Val()) | ||||
| 
 | ||||
| 		// Tack on any other extensions that may have been listed | ||||
| 		exts = append(exts, c.RemainingArgs()...) | ||||
| 	} | ||||
| 
 | ||||
| 	return exts, nil | ||||
| } | ||||
| 
 | ||||
| // resourceExists returns true if the file specified at | ||||
| // root + path exists; false otherwise. | ||||
| func resourceExists(root, path string) bool { | ||||
| 	_, err := os.Stat(filepath.Join(root, path)) | ||||
| 	// technically we should use os.IsNotExist(err) | ||||
| 	// but we don't handle any other kinds of errors anyway | ||||
| 	return err == nil | ||||
| } | ||||
| @ -1,31 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/inner" | ||||
| ) | ||||
| 
 | ||||
| // Internal configures a new Internal middleware instance. | ||||
| func Internal(c *Controller) (middleware.Middleware, error) { | ||||
| 	paths, err := internalParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return inner.Internal{Next: next, Paths: paths} | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func internalParse(c *Controller) ([]string, error) { | ||||
| 	var paths []string | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		if !c.NextArg() { | ||||
| 			return paths, c.ArgErr() | ||||
| 		} | ||||
| 		paths = append(paths, c.Val()) | ||||
| 	} | ||||
| 
 | ||||
| 	return paths, nil | ||||
| } | ||||
| @ -1,27 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/pprof" | ||||
| ) | ||||
| 
 | ||||
| //PProf returns a new instance of a pprof handler. It accepts no arguments or options. | ||||
| func PProf(c *Controller) (middleware.Middleware, error) { | ||||
| 	found := false | ||||
| 	for c.Next() { | ||||
| 		if found { | ||||
| 			return nil, c.Err("pprof can only be specified once") | ||||
| 		} | ||||
| 		if len(c.RemainingArgs()) != 0 { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 		if c.NextBlock() { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 		found = true | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return &pprof.Handler{Next: next, Mux: pprof.NewMux()} | ||||
| 	}, nil | ||||
| } | ||||
| @ -1,17 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/proxy" | ||||
| ) | ||||
| 
 | ||||
| // Proxy configures a new Proxy middleware instance. | ||||
| func Proxy(c *Controller) (middleware.Middleware, error) { | ||||
| 	upstreams, err := proxy.NewStaticUpstreams(c.Dispenser) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return proxy.Proxy{Next: next, Upstreams: upstreams} | ||||
| 	}, nil | ||||
| } | ||||
| @ -1,32 +0,0 @@ | ||||
| package setup | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| ) | ||||
| 
 | ||||
| // Root sets up the root file path of the server. | ||||
| func Root(c *Controller) (middleware.Middleware, error) { | ||||
| 	for c.Next() { | ||||
| 		if !c.NextArg() { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 		c.Root = c.Val() | ||||
| 	} | ||||
| 
 | ||||
| 	// Check if root path exists | ||||
| 	_, err := os.Stat(c.Root) | ||||
| 	if err != nil { | ||||
| 		if os.IsNotExist(err) { | ||||
| 			// Allow this, because the folder might appear later. | ||||
| 			// But make sure the user knows! | ||||
| 			log.Printf("[WARNING] Root path does not exist: %s", c.Root) | ||||
| 		} else { | ||||
| 			return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, nil | ||||
| } | ||||
							
								
								
									
										1
									
								
								caddy/setup/testdata/blog/first_post.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								caddy/setup/testdata/blog/first_post.md
									
									
									
									
										vendored
									
									
								
							| @ -1 +0,0 @@ | ||||
| # Test h1 | ||||
							
								
								
									
										10
									
								
								caddy/setup/testdata/tpl_with_include.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								caddy/setup/testdata/tpl_with_include.html
									
									
									
									
										vendored
									
									
								
							| @ -1,10 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
| <title>{{.Doc.title}}</title> | ||||
| </head> | ||||
| <body> | ||||
| {{.Include "header.html"}} | ||||
| {{.Doc.body}} | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										58
									
								
								caddy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								caddy_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| package caddy | ||||
| 
 | ||||
| import "testing" | ||||
| 
 | ||||
| /* | ||||
| // TODO | ||||
| func TestCaddyStartStop(t *testing.T) { | ||||
| 	caddyfile := "localhost:1984" | ||||
| 
 | ||||
| 	for i := 0; i < 2; i++ { | ||||
| 		_, err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Error starting, iteration %d: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		client := http.Client{ | ||||
| 			Timeout: time.Duration(2 * time.Second), | ||||
| 		} | ||||
| 		resp, err := client.Get("http://localhost:1984") | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) | ||||
| 		} | ||||
| 		resp.Body.Close() | ||||
| 
 | ||||
| 		err = Stop() | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Error stopping, iteration %d: %v", i, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| */ | ||||
| 
 | ||||
| func TestIsLoopback(t *testing.T) { | ||||
| 	for i, test := range []struct { | ||||
| 		input  string | ||||
| 		expect bool | ||||
| 	}{ | ||||
| 		{"example.com", false}, | ||||
| 		{"localhost", true}, | ||||
| 		{"localhost:1234", true}, | ||||
| 		{"localhost:", true}, | ||||
| 		{"127.0.0.1", true}, | ||||
| 		{"127.0.0.1:443", true}, | ||||
| 		{"127.0.1.5", true}, | ||||
| 		{"10.0.0.5", false}, | ||||
| 		{"12.7.0.1", false}, | ||||
| 		{"[::1]", true}, | ||||
| 		{"[::1]:1234", true}, | ||||
| 		{"::1", true}, | ||||
| 		{"::", false}, | ||||
| 		{"[::]", false}, | ||||
| 		{"local", false}, | ||||
| 	} { | ||||
| 		if got, want := IsLoopback(test.input), test.expect; got != want { | ||||
| 			t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package parse | ||||
| package caddyfile | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| @ -12,7 +12,7 @@ import ( | ||||
| // some really convenient methods. | ||||
| type Dispenser struct { | ||||
| 	filename string | ||||
| 	tokens   []token | ||||
| 	tokens   []Token | ||||
| 	cursor   int | ||||
| 	nesting  int | ||||
| } | ||||
| @ -27,7 +27,7 @@ func NewDispenser(filename string, input io.Reader) Dispenser { | ||||
| } | ||||
| 
 | ||||
| // NewDispenserTokens returns a Dispenser filled with the given tokens. | ||||
| func NewDispenserTokens(filename string, tokens []token) Dispenser { | ||||
| func NewDispenserTokens(filename string, tokens []Token) Dispenser { | ||||
| 	return Dispenser{ | ||||
| 		filename: filename, | ||||
| 		tokens:   tokens, | ||||
| @ -59,8 +59,8 @@ func (d *Dispenser) NextArg() bool { | ||||
| 		return false | ||||
| 	} | ||||
| 	if d.cursor < len(d.tokens)-1 && | ||||
| 		d.tokens[d.cursor].file == d.tokens[d.cursor+1].file && | ||||
| 		d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line { | ||||
| 		d.tokens[d.cursor].File == d.tokens[d.cursor+1].File && | ||||
| 		d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { | ||||
| 		d.cursor++ | ||||
| 		return true | ||||
| 	} | ||||
| @ -80,8 +80,8 @@ func (d *Dispenser) NextLine() bool { | ||||
| 		return false | ||||
| 	} | ||||
| 	if d.cursor < len(d.tokens)-1 && | ||||
| 		(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file || | ||||
| 			d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) { | ||||
| 		(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File || | ||||
| 			d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { | ||||
| 		d.cursor++ | ||||
| 		return true | ||||
| 	} | ||||
| @ -131,7 +131,7 @@ func (d *Dispenser) Val() string { | ||||
| 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return d.tokens[d.cursor].text | ||||
| 	return d.tokens[d.cursor].Text | ||||
| } | ||||
| 
 | ||||
| // Line gets the line number of the current token. If there is no token | ||||
| @ -140,7 +140,7 @@ func (d *Dispenser) Line() int { | ||||
| 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return d.tokens[d.cursor].line | ||||
| 	return d.tokens[d.cursor].Line | ||||
| } | ||||
| 
 | ||||
| // File gets the filename of the current token. If there is no token loaded, | ||||
| @ -149,7 +149,7 @@ func (d *Dispenser) File() string { | ||||
| 	if d.cursor < 0 || d.cursor >= len(d.tokens) { | ||||
| 		return d.filename | ||||
| 	} | ||||
| 	if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" { | ||||
| 	if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" { | ||||
| 		return tokenFilename | ||||
| 	} | ||||
| 	return d.filename | ||||
| @ -233,7 +233,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int { | ||||
| 	if tknIdx < 0 || tknIdx >= len(d.tokens) { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return strings.Count(d.tokens[tknIdx].text, "\n") | ||||
| 	return strings.Count(d.tokens[tknIdx].Text, "\n") | ||||
| } | ||||
| 
 | ||||
| // isNewLine determines whether the current token is on a different | ||||
| @ -246,6 +246,6 @@ func (d *Dispenser) isNewLine() bool { | ||||
| 	if d.cursor > len(d.tokens)-1 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file || | ||||
| 		d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line | ||||
| 	return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File || | ||||
| 		d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package parse | ||||
| package caddyfile | ||||
| 
 | ||||
| import ( | ||||
| 	"reflect" | ||||
| @ -4,31 +4,26 @@ import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/caddy/parse" | ||||
| ) | ||||
| 
 | ||||
| const filename = "Caddyfile" | ||||
| 
 | ||||
| // ToJSON converts caddyfile to its JSON representation. | ||||
| func ToJSON(caddyfile []byte) ([]byte, error) { | ||||
| 	var j Caddyfile | ||||
| 	var j EncodedCaddyfile | ||||
| 
 | ||||
| 	serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false) | ||||
| 	serverBlocks, err := ServerBlocks(filename, bytes.NewReader(caddyfile), nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, sb := range serverBlocks { | ||||
| 		block := ServerBlock{Body: [][]interface{}{}} | ||||
| 
 | ||||
| 		// Fill up host list | ||||
| 		for _, host := range sb.HostList() { | ||||
| 			block.Hosts = append(block.Hosts, standardizeScheme(host)) | ||||
| 		block := EncodedServerBlock{ | ||||
| 			Keys: sb.Keys, | ||||
| 			Body: [][]interface{}{}, | ||||
| 		} | ||||
| 
 | ||||
| 		// Extract directives deterministically by sorting them | ||||
| @ -40,7 +35,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) { | ||||
| 
 | ||||
| 		// Convert each directive's tokens into our JSON structure | ||||
| 		for _, dir := range directives { | ||||
| 			disp := parse.NewDispenserTokens(filename, sb.Tokens[dir]) | ||||
| 			disp := NewDispenserTokens(filename, sb.Tokens[dir]) | ||||
| 			for disp.Next() { | ||||
| 				block.Body = append(block.Body, constructLine(&disp)) | ||||
| 			} | ||||
| @ -62,7 +57,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) { | ||||
| // but only one line at a time, to be used at the top-level of | ||||
| // a server block only (where the first token on each line is a | ||||
| // directive) - not to be used at any other nesting level. | ||||
| func constructLine(d *parse.Dispenser) []interface{} { | ||||
| func constructLine(d *Dispenser) []interface{} { | ||||
| 	var args []interface{} | ||||
| 
 | ||||
| 	args = append(args, d.Val()) | ||||
| @ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} { | ||||
| // constructBlock recursively processes tokens into a | ||||
| // JSON-encodable structure. To be used in a directive's | ||||
| // block. Goes to end of block. | ||||
| func constructBlock(d *parse.Dispenser) [][]interface{} { | ||||
| func constructBlock(d *Dispenser) [][]interface{} { | ||||
| 	block := [][]interface{}{} | ||||
| 
 | ||||
| 	for d.Next() { | ||||
| @ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} { | ||||
| 
 | ||||
| // FromJSON converts JSON-encoded jsonBytes to Caddyfile text | ||||
| func FromJSON(jsonBytes []byte) ([]byte, error) { | ||||
| 	var j Caddyfile | ||||
| 	var j EncodedCaddyfile | ||||
| 	var result string | ||||
| 
 | ||||
| 	err := json.Unmarshal(jsonBytes, &j) | ||||
| @ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) { | ||||
| 		if sbPos > 0 { | ||||
| 			result += "\n\n" | ||||
| 		} | ||||
| 		for i, host := range sb.Hosts { | ||||
| 		for i, key := range sb.Keys { | ||||
| 			if i > 0 { | ||||
| 				result += ", " | ||||
| 			} | ||||
| 			result += standardizeScheme(host) | ||||
| 			//result += standardizeScheme(key) | ||||
| 			result += key | ||||
| 		} | ||||
| 		result += jsonToText(sb.Body, 1) | ||||
| 	} | ||||
| @ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string { | ||||
| 	return result | ||||
| } | ||||
| 
 | ||||
| // TODO: Will this function come in handy somewhere else? | ||||
| /* | ||||
| // standardizeScheme turns an address like host:https into https://host, | ||||
| // or "host:" into "host". | ||||
| func standardizeScheme(addr string) string { | ||||
| @ -174,12 +172,13 @@ func standardizeScheme(addr string) string { | ||||
| 	} | ||||
| 	return strings.TrimSuffix(addr, ":") | ||||
| } | ||||
| */ | ||||
| 
 | ||||
| // Caddyfile encapsulates a slice of ServerBlocks. | ||||
| type Caddyfile []ServerBlock | ||||
| // EncodedCaddyfile encapsulates a slice of EncodedServerBlocks. | ||||
| type EncodedCaddyfile []EncodedServerBlock | ||||
| 
 | ||||
| // ServerBlock represents a server block. | ||||
| type ServerBlock struct { | ||||
| 	Hosts []string        `json:"hosts"` | ||||
| 	Body  [][]interface{} `json:"body"` | ||||
| // EncodedServerBlock represents a server block ripe for encoding. | ||||
| type EncodedServerBlock struct { | ||||
| 	Keys []string        `json:"keys"` | ||||
| 	Body [][]interface{} `json:"body"` | ||||
| } | ||||
| @ -9,7 +9,7 @@ var tests = []struct { | ||||
| 		caddyfile: `foo { | ||||
| 	root /bar | ||||
| }`, | ||||
| 		json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`, | ||||
| 		json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`, | ||||
| 	}, | ||||
| 	{ // 1 | ||||
| 		caddyfile: `host1, host2 { | ||||
| @ -17,7 +17,7 @@ var tests = []struct { | ||||
| 		def | ||||
| 	} | ||||
| }`, | ||||
| 		json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`, | ||||
| 		json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`, | ||||
| 	}, | ||||
| 	{ // 2 | ||||
| 		caddyfile: `host1, host2 { | ||||
| @ -26,58 +26,58 @@ var tests = []struct { | ||||
| 		jkl | ||||
| 	} | ||||
| }`, | ||||
| 		json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`, | ||||
| 		json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`, | ||||
| 	}, | ||||
| 	{ // 3 | ||||
| 		caddyfile: `host1:1234, host2:5678 { | ||||
| 	dir abc { | ||||
| 	} | ||||
| }`, | ||||
| 		json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`, | ||||
| 		json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`, | ||||
| 	}, | ||||
| 	{ // 4 | ||||
| 		caddyfile: `host { | ||||
| 	foo "bar baz" | ||||
| }`, | ||||
| 		json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`, | ||||
| 		json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`, | ||||
| 	}, | ||||
| 	{ // 5 | ||||
| 		caddyfile: `host, host:80 { | ||||
| 	foo "bar \"baz\"" | ||||
| }`, | ||||
| 		json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`, | ||||
| 		json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`, | ||||
| 	}, | ||||
| 	{ // 6 | ||||
| 		caddyfile: `host { | ||||
| 	foo "bar | ||||
| baz" | ||||
| }`, | ||||
| 		json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`, | ||||
| 		json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`, | ||||
| 	}, | ||||
| 	{ // 7 | ||||
| 		caddyfile: `host { | ||||
| 	dir 123 4.56 true | ||||
| }`, | ||||
| 		json: `[{"hosts":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...? | ||||
| 		json: `[{"keys":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...? | ||||
| 	}, | ||||
| 	{ // 8 | ||||
| 		caddyfile: `http://host, https://host { | ||||
| }`, | ||||
| 		json: `[{"hosts":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency | ||||
| 		json: `[{"keys":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency | ||||
| 	}, | ||||
| 	{ // 9 | ||||
| 		caddyfile: `host { | ||||
| 	dir1 a b | ||||
| 	dir2 c d | ||||
| }`, | ||||
| 		json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`, | ||||
| 		json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`, | ||||
| 	}, | ||||
| 	{ // 10 | ||||
| 		caddyfile: `host { | ||||
| 	dir a b | ||||
| 	dir c d | ||||
| }`, | ||||
| 		json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`, | ||||
| 		json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`, | ||||
| 	}, | ||||
| 	{ // 11 | ||||
| 		caddyfile: `host { | ||||
| @ -87,7 +87,7 @@ baz" | ||||
| 		d | ||||
| 	} | ||||
| }`, | ||||
| 		json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`, | ||||
| 		json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`, | ||||
| 	}, | ||||
| 	{ // 12 | ||||
| 		caddyfile: `host1 { | ||||
| @ -97,7 +97,7 @@ baz" | ||||
| host2 { | ||||
| 	dir2 | ||||
| }`, | ||||
| 		json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`, | ||||
| 		json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`, | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| @ -125,17 +125,19 @@ func TestFromJSON(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TODO: Will these tests come in handy somewhere else? | ||||
| /* | ||||
| func TestStandardizeAddress(t *testing.T) { | ||||
| 	// host:https should be converted to https://host | ||||
| 	output, err := ToJSON([]byte(`host:https`)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if expected, actual := `[{"hosts":["https://host"],"body":[]}]`, string(output); expected != actual { | ||||
| 	if expected, actual := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual { | ||||
| 		t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	output, err = FromJSON([]byte(`[{"hosts":["https://host"],"body":[]}]`)) | ||||
| 	output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| @ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) { | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if expected, actual := `[{"hosts":["host"],"body":[]}]`, string(output); expected != actual { | ||||
| 	if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual { | ||||
| 		t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual) | ||||
| 	} | ||||
| 	output, err = FromJSON([]byte(`[{"hosts":["host:"],"body":[]}]`)) | ||||
| 	output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| @ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) { | ||||
| 		t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual) | ||||
| 	} | ||||
| } | ||||
| */ | ||||
| @ -1,4 +1,4 @@ | ||||
| package parse | ||||
| package caddyfile | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| @ -13,15 +13,15 @@ type ( | ||||
| 	// in quotes if it contains whitespace. | ||||
| 	lexer struct { | ||||
| 		reader *bufio.Reader | ||||
| 		token  token | ||||
| 		token  Token | ||||
| 		line   int | ||||
| 	} | ||||
| 
 | ||||
| 	// token represents a single parsable unit. | ||||
| 	token struct { | ||||
| 		file string | ||||
| 		line int | ||||
| 		text string | ||||
| 	// Token represents a single parsable unit. | ||||
| 	Token struct { | ||||
| 		File string | ||||
| 		Line int | ||||
| 		Text string | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| @ -47,7 +47,7 @@ func (l *lexer) next() bool { | ||||
| 	var comment, quoted, escaped bool | ||||
| 
 | ||||
| 	makeToken := func() bool { | ||||
| 		l.token.text = string(val) | ||||
| 		l.token.Text = string(val) | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| @ -110,7 +110,7 @@ func (l *lexer) next() bool { | ||||
| 		} | ||||
| 
 | ||||
| 		if len(val) == 0 { | ||||
| 			l.token = token{line: l.line} | ||||
| 			l.token = Token{Line: l.line} | ||||
| 			if ch == '"' { | ||||
| 				quoted = true | ||||
| 				continue | ||||
							
								
								
									
										165
									
								
								caddyfile/lexer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								caddyfile/lexer_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | ||||
| package caddyfile | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| type lexerTestCase struct { | ||||
| 	input    string | ||||
| 	expected []Token | ||||
| } | ||||
| 
 | ||||
| func TestLexer(t *testing.T) { | ||||
| 	testCases := []lexerTestCase{ | ||||
| 		{ | ||||
| 			input: `host:123`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "host:123"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 | ||||
| 
 | ||||
| 					directive`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "host:123"}, | ||||
| 				{Line: 3, Text: "directive"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 { | ||||
| 						directive | ||||
| 					}`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "host:123"}, | ||||
| 				{Line: 1, Text: "{"}, | ||||
| 				{Line: 2, Text: "directive"}, | ||||
| 				{Line: 3, Text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 { directive }`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "host:123"}, | ||||
| 				{Line: 1, Text: "{"}, | ||||
| 				{Line: 1, Text: "directive"}, | ||||
| 				{Line: 1, Text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `host:123 { | ||||
| 						#comment | ||||
| 						directive | ||||
| 						# comment | ||||
| 						foobar # another comment | ||||
| 					}`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "host:123"}, | ||||
| 				{Line: 1, Text: "{"}, | ||||
| 				{Line: 3, Text: "directive"}, | ||||
| 				{Line: 5, Text: "foobar"}, | ||||
| 				{Line: 6, Text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `a "quoted value" b | ||||
| 					foobar`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "a"}, | ||||
| 				{Line: 1, Text: "quoted value"}, | ||||
| 				{Line: 1, Text: "b"}, | ||||
| 				{Line: 2, Text: "foobar"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `A "quoted \"value\" inside" B`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "A"}, | ||||
| 				{Line: 1, Text: `quoted "value" inside`}, | ||||
| 				{Line: 1, Text: "B"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `"don't\escape"`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: `don't\escape`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `"don't\\escape"`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: `don't\\escape`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `A "quoted value with line | ||||
| 					break inside" { | ||||
| 						foobar | ||||
| 					}`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "A"}, | ||||
| 				{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"}, | ||||
| 				{Line: 2, Text: "{"}, | ||||
| 				{Line: 3, Text: "foobar"}, | ||||
| 				{Line: 4, Text: "}"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `"C:\php\php-cgi.exe"`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: `C:\php\php-cgi.exe`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: `empty "" string`, | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: `empty`}, | ||||
| 				{Line: 1, Text: ``}, | ||||
| 				{Line: 1, Text: `string`}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			input: "skip those\r\nCR characters", | ||||
| 			expected: []Token{ | ||||
| 				{Line: 1, Text: "skip"}, | ||||
| 				{Line: 1, Text: "those"}, | ||||
| 				{Line: 2, Text: "CR"}, | ||||
| 				{Line: 2, Text: "characters"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for i, testCase := range testCases { | ||||
| 		actual := tokenize(testCase.input) | ||||
| 		lexerCompare(t, i, testCase.expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func tokenize(input string) (tokens []Token) { | ||||
| 	l := lexer{} | ||||
| 	l.load(strings.NewReader(input)) | ||||
| 	for l.next() { | ||||
| 		tokens = append(tokens, l.token) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func lexerCompare(t *testing.T, n int, expected, actual []Token) { | ||||
| 	if len(expected) != len(actual) { | ||||
| 		t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) | ||||
| 	} | ||||
| 
 | ||||
| 	for i := 0; i < len(actual) && i < len(expected); i++ { | ||||
| 		if actual[i].Line != expected[i].Line { | ||||
| 			t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", | ||||
| 				n, i, expected[i].Text, expected[i].Line, actual[i].Line) | ||||
| 			break | ||||
| 		} | ||||
| 		if actual[i].Text != expected[i].Text { | ||||
| 			t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", | ||||
| 				n, i, expected[i].Text, actual[i].Text) | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -1,18 +1,41 @@ | ||||
| package parse | ||||
| package caddyfile | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // ServerBlocks parses the input just enough to group tokens, | ||||
| // in order, by server block. No further parsing is performed. | ||||
| // Server blocks are returned in the order in which they appear. | ||||
| // Directives that do not appear in validDirectives will cause | ||||
| // an error. If you do not want to check for valid directives, | ||||
| // pass in nil instead. | ||||
| func ServerBlocks(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) { | ||||
| 	p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives} | ||||
| 	blocks, err := p.parseAll() | ||||
| 	return blocks, err | ||||
| } | ||||
| 
 | ||||
| // allTokens lexes the entire input, but does not parse it. | ||||
| // It returns all the tokens from the input, unstructured | ||||
| // and in order. | ||||
| func allTokens(input io.Reader) (tokens []Token) { | ||||
| 	l := new(lexer) | ||||
| 	l.load(input) | ||||
| 	for l.next() { | ||||
| 		tokens = append(tokens, l.token) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| type parser struct { | ||||
| 	Dispenser | ||||
| 	block           ServerBlock // current server block being parsed | ||||
| 	validDirectives []string    // a directive must be valid or it's an error | ||||
| 	eof             bool        // if we encounter a valid EOF in a hard place | ||||
| 	checkDirectives bool        // if true, directives must be known | ||||
| } | ||||
| 
 | ||||
| func (p *parser) parseAll() ([]ServerBlock, error) { | ||||
| @ -23,7 +46,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { | ||||
| 		if err != nil { | ||||
| 			return blocks, err | ||||
| 		} | ||||
| 		if len(p.block.Addresses) > 0 { | ||||
| 		if len(p.block.Keys) > 0 { | ||||
| 			blocks = append(blocks, p.block) | ||||
| 		} | ||||
| 	} | ||||
| @ -32,7 +55,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { | ||||
| } | ||||
| 
 | ||||
| func (p *parser) parseOne() error { | ||||
| 	p.block = ServerBlock{Tokens: make(map[string][]token)} | ||||
| 	p.block = ServerBlock{Tokens: make(map[string][]Token)} | ||||
| 
 | ||||
| 	err := p.begin() | ||||
| 	if err != nil { | ||||
| @ -89,7 +112,7 @@ func (p *parser) addresses() error { | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		if tkn != "" { // empty token possible if user typed "" in Caddyfile | ||||
| 		if tkn != "" { // empty token possible if user typed "" | ||||
| 			// Trailing comma indicates another address will follow, which | ||||
| 			// may possibly be on the next line | ||||
| 			if tkn[len(tkn)-1] == ',' { | ||||
| @ -99,13 +122,7 @@ func (p *parser) addresses() error { | ||||
| 				expectingAnother = false // but we may still see another one on this line | ||||
| 			} | ||||
| 
 | ||||
| 			// Parse and save this address | ||||
| 			addr, err := standardAddress(tkn) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			p.block.Addresses = append(p.block.Addresses, addr) | ||||
| 			p.block.Keys = append(p.block.Keys, tkn) | ||||
| 		} | ||||
| 
 | ||||
| 		// Advance token and possibly break out of loop or return error | ||||
| @ -207,7 +224,7 @@ func (p *parser) doImport() error { | ||||
| 	tokensAfter := p.tokens[p.cursor+1:] | ||||
| 
 | ||||
| 	// collect all the imported tokens | ||||
| 	var importedTokens []token | ||||
| 	var importedTokens []Token | ||||
| 	for _, importFile := range matches { | ||||
| 		newTokens, err := p.doSingleImport(importFile) | ||||
| 		if err != nil { | ||||
| @ -226,7 +243,7 @@ func (p *parser) doImport() error { | ||||
| 
 | ||||
| // doSingleImport lexes the individual file at importFile and returns | ||||
| // its tokens or an error, if any. | ||||
| func (p *parser) doSingleImport(importFile string) ([]token, error) { | ||||
| func (p *parser) doSingleImport(importFile string) ([]Token, error) { | ||||
| 	file, err := os.Open(importFile) | ||||
| 	if err != nil { | ||||
| 		return nil, p.Errf("Could not import %s: %v", importFile, err) | ||||
| @ -237,7 +254,7 @@ func (p *parser) doSingleImport(importFile string) ([]token, error) { | ||||
| 	// Tack the filename onto these tokens so errors show the imported file's name | ||||
| 	filename := filepath.Base(importFile) | ||||
| 	for i := 0; i < len(importedTokens); i++ { | ||||
| 		importedTokens[i].file = filename | ||||
| 		importedTokens[i].File = filename | ||||
| 	} | ||||
| 
 | ||||
| 	return importedTokens, nil | ||||
| @ -253,10 +270,9 @@ func (p *parser) directive() error { | ||||
| 	dir := p.Val() | ||||
| 	nesting := 0 | ||||
| 
 | ||||
| 	if p.checkDirectives { | ||||
| 		if _, ok := ValidDirectives[dir]; !ok { | ||||
| 			return p.Errf("Unknown directive '%s'", dir) | ||||
| 		} | ||||
| 	// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type") | ||||
| 	if !p.validDirective(dir) { | ||||
| 		return p.Errf("Unknown directive '%s'", dir) | ||||
| 	} | ||||
| 
 | ||||
| 	// The directive itself is appended as a relevant token | ||||
| @ -273,7 +289,7 @@ func (p *parser) directive() error { | ||||
| 		} else if p.Val() == "}" && nesting == 0 { | ||||
| 			return p.Err("Unexpected '}' because no matching opening brace") | ||||
| 		} | ||||
| 		p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text) | ||||
| 		p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) | ||||
| 		p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) | ||||
| 	} | ||||
| 
 | ||||
| @ -305,63 +321,17 @@ func (p *parser) closeCurlyBrace() error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // standardAddress parses an address string into a structured format with separate | ||||
| // scheme, host, and port portions, as well as the original input string. | ||||
| func standardAddress(str string) (address, error) { | ||||
| 	var scheme string | ||||
| 	var err error | ||||
| 
 | ||||
| 	// first check for scheme and strip it off | ||||
| 	input := str | ||||
| 	if strings.HasPrefix(str, "https://") { | ||||
| 		scheme = "https" | ||||
| 		str = str[8:] | ||||
| 	} else if strings.HasPrefix(str, "http://") { | ||||
| 		scheme = "http" | ||||
| 		str = str[7:] | ||||
| // validDirective returns true if dir is in p.validDirectives. | ||||
| func (p *parser) validDirective(dir string) bool { | ||||
| 	if p.validDirectives == nil { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	// separate host and port | ||||
| 	host, port, err := net.SplitHostPort(str) | ||||
| 	if err != nil { | ||||
| 		host, port, err = net.SplitHostPort(str + ":") | ||||
| 		if err != nil { | ||||
| 			host = str | ||||
| 	for _, d := range p.validDirectives { | ||||
| 		if d == dir { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// "The host subcomponent is case-insensitive." (RFC 3986) | ||||
| 	host = strings.ToLower(host) | ||||
| 
 | ||||
| 	// see if we can set port based off scheme | ||||
| 	if port == "" { | ||||
| 		if scheme == "http" { | ||||
| 			port = "80" | ||||
| 		} else if scheme == "https" { | ||||
| 			port = "443" | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// repeated or conflicting scheme is confusing, so error | ||||
| 	if scheme != "" && (port == "http" || port == "https") { | ||||
| 		return address{}, fmt.Errorf("[%s] scheme specified twice in address", input) | ||||
| 	} | ||||
| 
 | ||||
| 	// error if scheme and port combination violate convention | ||||
| 	if (scheme == "http" && port == "443") || (scheme == "https" && port == "80") { | ||||
| 		return address{}, fmt.Errorf("[%s] scheme and port violate convention", input) | ||||
| 	} | ||||
| 
 | ||||
| 	// standardize http and https ports to their respective port numbers | ||||
| 	if port == "http" { | ||||
| 		scheme = "http" | ||||
| 		port = "80" | ||||
| 	} else if port == "https" { | ||||
| 		scheme = "https" | ||||
| 		port = "443" | ||||
| 	} | ||||
| 
 | ||||
| 	return address{Original: input, Scheme: scheme, Host: host, Port: port}, err | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // replaceEnvVars replaces environment variables that appear in the token | ||||
| @ -389,27 +359,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string { | ||||
| 	return s | ||||
| } | ||||
| 
 | ||||
| type ( | ||||
| 	// ServerBlock associates tokens with a list of addresses | ||||
| 	// and groups tokens by directive name. | ||||
| 	ServerBlock struct { | ||||
| 		Addresses []address | ||||
| 		Tokens    map[string][]token | ||||
| 	} | ||||
| 
 | ||||
| 	address struct { | ||||
| 		Original, Scheme, Host, Port string | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| // HostList converts the list of addresses that are | ||||
| // associated with this server block into a slice of | ||||
| // strings, where each address is as it was originally | ||||
| // read from the input. | ||||
| func (sb ServerBlock) HostList() []string { | ||||
| 	sbHosts := make([]string, len(sb.Addresses)) | ||||
| 	for j, addr := range sb.Addresses { | ||||
| 		sbHosts[j] = addr.Original | ||||
| 	} | ||||
| 	return sbHosts | ||||
| // ServerBlock associates any number of keys (usually addresses | ||||
| // of some sort) with tokens (grouped by directive name). | ||||
| type ServerBlock struct { | ||||
| 	Keys   []string | ||||
| 	Tokens map[string][]Token | ||||
| } | ||||
							
								
								
									
										399
									
								
								caddyfile/parse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								caddyfile/parse_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,399 @@ | ||||
| package caddyfile | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestAllTokens(t *testing.T) { | ||||
| 	input := strings.NewReader("a b c\nd e") | ||||
| 	expected := []string{"a", "b", "c", "d", "e"} | ||||
| 	tokens := allTokens(input) | ||||
| 
 | ||||
| 	if len(tokens) != len(expected) { | ||||
| 		t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) | ||||
| 	} | ||||
| 
 | ||||
| 	for i, val := range expected { | ||||
| 		if tokens[i].Text != val { | ||||
| 			t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestParseOneAndImport(t *testing.T) { | ||||
| 	testParseOne := func(input string) (ServerBlock, error) { | ||||
| 		p := testParser(input) | ||||
| 		p.Next() // parseOne doesn't call Next() to start, so we must | ||||
| 		err := p.parseOne() | ||||
| 		return p.block, err | ||||
| 	} | ||||
| 
 | ||||
| 	for i, test := range []struct { | ||||
| 		input     string | ||||
| 		shouldErr bool | ||||
| 		keys      []string | ||||
| 		tokens    map[string]int // map of directive name to number of tokens expected | ||||
| 	}{ | ||||
| 		{`localhost`, false, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1`, false, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234 | ||||
| 		  dir1 foo bar`, false, []string{ | ||||
| 			"localhost:1234", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost { | ||||
| 		    dir1 | ||||
| 		  }`, false, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234 { | ||||
| 		    dir1 foo bar | ||||
| 		    dir2 | ||||
| 		  }`, false, []string{ | ||||
| 			"localhost:1234", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 			"dir2": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost https://localhost | ||||
| 		  dir1 foo bar`, false, []string{ | ||||
| 			"http://localhost", | ||||
| 			"https://localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost https://localhost { | ||||
| 		    dir1 foo bar | ||||
| 		  }`, false, []string{ | ||||
| 			"http://localhost", | ||||
| 			"https://localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost, https://localhost { | ||||
| 		    dir1 foo bar | ||||
| 		  }`, false, []string{ | ||||
| 			"http://localhost", | ||||
| 			"https://localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://localhost, { | ||||
| 		  }`, true, []string{ | ||||
| 			"http://localhost", | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`host1:80, http://host2.com | ||||
| 		  dir1 foo bar | ||||
| 		  dir2 baz`, false, []string{ | ||||
| 			"host1:80", | ||||
| 			"http://host2.com", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 			"dir2": 2, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`http://host1.com, | ||||
| 		  http://host2.com, | ||||
| 		  https://host3.com`, false, []string{ | ||||
| 			"http://host1.com", | ||||
| 			"http://host2.com", | ||||
| 			"https://host3.com", | ||||
| 		}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`http://host1.com:1234, https://host2.com | ||||
| 		  dir1 foo { | ||||
| 		    bar baz | ||||
| 		  } | ||||
| 		  dir2`, false, []string{ | ||||
| 			"http://host1.com:1234", | ||||
| 			"https://host2.com", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 6, | ||||
| 			"dir2": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`127.0.0.1 | ||||
| 		  dir1 { | ||||
| 		    bar baz | ||||
| 		  } | ||||
| 		  dir2 { | ||||
| 		    foo bar | ||||
| 		  }`, false, []string{ | ||||
| 			"127.0.0.1", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 5, | ||||
| 			"dir2": 5, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		    foo`, true, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		  }`, false, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		  } }`, true, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 { | ||||
| 		    nested { | ||||
| 		      foo | ||||
| 		    } | ||||
| 		  } | ||||
| 		  dir2 foo bar`, false, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 7, | ||||
| 			"dir2": 3, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{``, false, []string{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`localhost | ||||
| 		  dir1 arg1 | ||||
| 		  import testdata/import_test1.txt`, false, []string{ | ||||
| 			"localhost", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 2, | ||||
| 			"dir2": 3, | ||||
| 			"dir3": 1, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`import testdata/import_test2.txt`, false, []string{ | ||||
| 			"host1", | ||||
| 		}, map[string]int{ | ||||
| 			"dir1": 1, | ||||
| 			"dir2": 2, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`import testdata/not_found.txt`, true, []string{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{`""`, false, []string{}, map[string]int{}}, | ||||
| 
 | ||||
| 		{``, false, []string{}, map[string]int{}}, | ||||
| 	} { | ||||
| 		result, err := testParseOne(test.input) | ||||
| 
 | ||||
| 		if test.shouldErr && err == nil { | ||||
| 			t.Errorf("Test %d: Expected an error, but didn't get one", i) | ||||
| 		} | ||||
| 		if !test.shouldErr && err != nil { | ||||
| 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if len(result.Keys) != len(test.keys) { | ||||
| 			t.Errorf("Test %d: Expected %d keys, got %d", | ||||
| 				i, len(test.keys), len(result.Keys)) | ||||
| 			continue | ||||
| 		} | ||||
| 		for j, addr := range result.Keys { | ||||
| 			if addr != test.keys[j] { | ||||
| 				t.Errorf("Test %d, key %d: Expected '%s', but was '%s'", | ||||
| 					i, j, test.keys[j], addr) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if len(result.Tokens) != len(test.tokens) { | ||||
| 			t.Errorf("Test %d: Expected %d directives, had %d", | ||||
| 				i, len(test.tokens), len(result.Tokens)) | ||||
| 			continue | ||||
| 		} | ||||
| 		for directive, tokens := range result.Tokens { | ||||
| 			if len(tokens) != test.tokens[directive] { | ||||
| 				t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d", | ||||
| 					i, directive, test.tokens[directive], len(tokens)) | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestParseAll(t *testing.T) { | ||||
| 	for i, test := range []struct { | ||||
| 		input     string | ||||
| 		shouldErr bool | ||||
| 		keys      [][]string // keys per server block, in order | ||||
| 	}{ | ||||
| 		{`localhost`, false, [][]string{ | ||||
| 			{"localhost"}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234`, false, [][]string{ | ||||
| 			{"localhost:1234"}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234 { | ||||
| 		  } | ||||
| 		  localhost:2015 { | ||||
| 		  }`, false, [][]string{ | ||||
| 			{"localhost:1234"}, | ||||
| 			{"localhost:2015"}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234, http://host2`, false, [][]string{ | ||||
| 			{"localhost:1234", "http://host2"}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`localhost:1234, http://host2,`, true, [][]string{}}, | ||||
| 
 | ||||
| 		{`http://host1.com, http://host2.com { | ||||
| 		  } | ||||
| 		  https://host3.com, https://host4.com { | ||||
| 		  }`, false, [][]string{ | ||||
| 			{"http://host1.com", "http://host2.com"}, | ||||
| 			{"https://host3.com", "https://host4.com"}, | ||||
| 		}}, | ||||
| 
 | ||||
| 		{`import testdata/import_glob*.txt`, false, [][]string{ | ||||
| 			{"glob0.host0"}, | ||||
| 			{"glob0.host1"}, | ||||
| 			{"glob1.host0"}, | ||||
| 			{"glob2.host0"}, | ||||
| 		}}, | ||||
| 	} { | ||||
| 		p := testParser(test.input) | ||||
| 		blocks, err := p.parseAll() | ||||
| 
 | ||||
| 		if test.shouldErr && err == nil { | ||||
| 			t.Errorf("Test %d: Expected an error, but didn't get one", i) | ||||
| 		} | ||||
| 		if !test.shouldErr && err != nil { | ||||
| 			t.Errorf("Test %d: Expected no error, but got: %v", i, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if len(blocks) != len(test.keys) { | ||||
| 			t.Errorf("Test %d: Expected %d server blocks, got %d", | ||||
| 				i, len(test.keys), len(blocks)) | ||||
| 			continue | ||||
| 		} | ||||
| 		for j, block := range blocks { | ||||
| 			if len(block.Keys) != len(test.keys[j]) { | ||||
| 				t.Errorf("Test %d: Expected %d keys in block %d, got %d", | ||||
| 					i, len(test.keys[j]), j, len(block.Keys)) | ||||
| 				continue | ||||
| 			} | ||||
| 			for k, addr := range block.Keys { | ||||
| 				if addr != test.keys[j][k] { | ||||
| 					t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'", | ||||
| 						i, j, k, test.keys[j][k], addr) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestEnvironmentReplacement(t *testing.T) { | ||||
| 	os.Setenv("PORT", "8080") | ||||
| 	os.Setenv("ADDRESS", "servername.com") | ||||
| 	os.Setenv("FOOBAR", "foobar") | ||||
| 
 | ||||
| 	// basic test; unix-style env vars | ||||
| 	p := testParser(`{$ADDRESS}`) | ||||
| 	blocks, _ := p.parseAll() | ||||
| 	if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual { | ||||
| 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// multiple vars per token | ||||
| 	p = testParser(`{$ADDRESS}:{$PORT}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { | ||||
| 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// windows-style var and unix style in same token | ||||
| 	p = testParser(`{%ADDRESS%}:{$PORT}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { | ||||
| 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// reverse order | ||||
| 	p = testParser(`{$ADDRESS}:{%PORT%}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { | ||||
| 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// env var in server block body as argument | ||||
| 	p = testParser(":{%PORT%}\ndir1 {$FOOBAR}") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual { | ||||
| 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual { | ||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// combined windows env vars in argument | ||||
| 	p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual { | ||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// malformed env var (windows) | ||||
| 	p = testParser(":1234\ndir1 {%ADDRESS}") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual { | ||||
| 		t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// malformed (non-existent) env var (unix) | ||||
| 	p = testParser(`:{$PORT$}`) | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Keys[0], ":"; expected != actual { | ||||
| 		t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| 
 | ||||
| 	// in quoted field | ||||
| 	p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") | ||||
| 	blocks, _ = p.parseAll() | ||||
| 	if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual { | ||||
| 		t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func testParser(input string) parser { | ||||
| 	buf := strings.NewReader(input) | ||||
| 	p := parser{Dispenser: NewDispenser("Test", buf)} | ||||
| 	return p | ||||
| } | ||||
| @ -13,7 +13,7 @@ import ( | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/jimstudt/http-authentication/basic" | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // BasicAuth is middleware to protect resources with a username and password. | ||||
| @ -22,12 +22,12 @@ import ( | ||||
| // security of HTTP Basic Auth is disputed. Use discretion when deciding | ||||
| // what to protect with BasicAuth. | ||||
| type BasicAuth struct { | ||||
| 	Next     middleware.Handler | ||||
| 	Next     httpserver.Handler | ||||
| 	SiteRoot string | ||||
| 	Rules    []Rule | ||||
| } | ||||
| 
 | ||||
| // ServeHTTP implements the middleware.Handler interface. | ||||
| // ServeHTTP implements the httpserver.Handler interface. | ||||
| func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 
 | ||||
| 	var hasAuth bool | ||||
| @ -35,7 +35,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error | ||||
| 
 | ||||
| 	for _, rule := range a.Rules { | ||||
| 		for _, res := range rule.Resources { | ||||
| 			if !middleware.Path(r.URL.Path).Matches(res) { | ||||
| 			if !httpserver.Path(r.URL.Path).Matches(res) { | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| @ -10,13 +10,12 @@ import ( | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestBasicAuth(t *testing.T) { | ||||
| 
 | ||||
| 	rw := BasicAuth{ | ||||
| 		Next: middleware.HandlerFunc(contentHandler), | ||||
| 		Next: httpserver.HandlerFunc(contentHandler), | ||||
| 		Rules: []Rule{ | ||||
| 			{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}}, | ||||
| 		}, | ||||
| @ -67,7 +66,7 @@ func TestBasicAuth(t *testing.T) { | ||||
| 
 | ||||
| func TestMultipleOverlappingRules(t *testing.T) { | ||||
| 	rw := BasicAuth{ | ||||
| 		Next: middleware.HandlerFunc(contentHandler), | ||||
| 		Next: httpserver.HandlerFunc(contentHandler), | ||||
| 		Rules: []Rule{ | ||||
| 			{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}}, | ||||
| 			{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}}, | ||||
| @ -1,43 +1,55 @@ | ||||
| package setup | ||||
| package basicauth | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/basicauth" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // BasicAuth configures a new BasicAuth middleware instance. | ||||
| func BasicAuth(c *Controller) (middleware.Middleware, error) { | ||||
| 	root := c.Root | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "basicauth", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setup configures a new BasicAuth middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	cfg := httpserver.GetConfig(c.Key) | ||||
| 	root := cfg.Root | ||||
| 
 | ||||
| 	rules, err := basicAuthParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	basic := basicauth.BasicAuth{Rules: rules} | ||||
| 	basic := BasicAuth{Rules: rules} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 	cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		basic.Next = next | ||||
| 		basic.SiteRoot = root | ||||
| 		return basic | ||||
| 	}, nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { | ||||
| 	var rules []basicauth.Rule | ||||
| func basicAuthParse(c *caddy.Controller) ([]Rule, error) { | ||||
| 	var rules []Rule | ||||
| 	cfg := httpserver.GetConfig(c.Key) | ||||
| 
 | ||||
| 	var err error | ||||
| 	for c.Next() { | ||||
| 		var rule basicauth.Rule | ||||
| 		var rule Rule | ||||
| 
 | ||||
| 		args := c.RemainingArgs() | ||||
| 
 | ||||
| 		switch len(args) { | ||||
| 		case 2: | ||||
| 			rule.Username = args[0] | ||||
| 			if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil { | ||||
| 			if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil { | ||||
| 				return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) | ||||
| 			} | ||||
| 
 | ||||
| @ -50,7 +62,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { | ||||
| 		case 3: | ||||
| 			rule.Resources = append(rule.Resources, args[0]) | ||||
| 			rule.Username = args[1] | ||||
| 			if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil { | ||||
| 			if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil { | ||||
| 				return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) | ||||
| 			} | ||||
| 		default: | ||||
| @ -63,10 +75,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { | ||||
| 	return rules, nil | ||||
| } | ||||
| 
 | ||||
| func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) { | ||||
| func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) { | ||||
| 	if !strings.HasPrefix(passw, "htpasswd=") { | ||||
| 		return basicauth.PlainMatcher(passw), nil | ||||
| 		return PlainMatcher(passw), nil | ||||
| 	} | ||||
| 
 | ||||
| 	return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot) | ||||
| 	return GetHtpasswdMatcher(passw[9:], username, siteRoot) | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package setup | ||||
| package basicauth | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| @ -7,27 +7,27 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/basicauth" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestBasicAuth(t *testing.T) { | ||||
| 	c := NewTestController(`basicauth user pwd`) | ||||
| 
 | ||||
| 	mid, err := BasicAuth(c) | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`basicauth user pwd`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, but got: %v", err) | ||||
| 	} | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middleware, got 0 instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(basicauth.BasicAuth) | ||||
| 	handler := mids[0](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(BasicAuth) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler) | ||||
| 	} | ||||
| 
 | ||||
| 	if !SameNext(myHandler.Next, EmptyNext) { | ||||
| 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| } | ||||
| @ -54,41 +54,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` | ||||
| 		input     string | ||||
| 		shouldErr bool | ||||
| 		password  string | ||||
| 		expected  []basicauth.Rule | ||||
| 		expected  []Rule | ||||
| 	}{ | ||||
| 		{`basicauth user pwd`, false, "pwd", []basicauth.Rule{ | ||||
| 		{`basicauth user pwd`, false, "pwd", []Rule{ | ||||
| 			{Username: "user"}, | ||||
| 		}}, | ||||
| 		{`basicauth user pwd { | ||||
| 		}`, false, "pwd", []basicauth.Rule{ | ||||
| 		}`, false, "pwd", []Rule{ | ||||
| 			{Username: "user"}, | ||||
| 		}}, | ||||
| 		{`basicauth user pwd { | ||||
| 			/resource1 | ||||
| 			/resource2 | ||||
| 		}`, false, "pwd", []basicauth.Rule{ | ||||
| 		}`, false, "pwd", []Rule{ | ||||
| 			{Username: "user", Resources: []string{"/resource1", "/resource2"}}, | ||||
| 		}}, | ||||
| 		{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{ | ||||
| 		{`basicauth /resource user pwd`, false, "pwd", []Rule{ | ||||
| 			{Username: "user", Resources: []string{"/resource"}}, | ||||
| 		}}, | ||||
| 		{`basicauth /res1 user1 pwd1 | ||||
| 		  basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{ | ||||
| 		  basicauth /res2 user2 pwd2`, false, "pwd", []Rule{ | ||||
| 			{Username: "user1", Resources: []string{"/res1"}}, | ||||
| 			{Username: "user2", Resources: []string{"/res2"}}, | ||||
| 		}}, | ||||
| 		{`basicauth user`, true, "", []basicauth.Rule{}}, | ||||
| 		{`basicauth`, true, "", []basicauth.Rule{}}, | ||||
| 		{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}}, | ||||
| 		{`basicauth user`, true, "", []Rule{}}, | ||||
| 		{`basicauth`, true, "", []Rule{}}, | ||||
| 		{`basicauth /resource user pwd asdf`, true, "", []Rule{}}, | ||||
| 
 | ||||
| 		{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{ | ||||
| 		{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{ | ||||
| 			{Username: "sha1"}, | ||||
| 		}}, | ||||
| 	} | ||||
| 
 | ||||
| 	for i, test := range tests { | ||||
| 		c := NewTestController(test.input) | ||||
| 		actual, err := basicAuthParse(c) | ||||
| 		actual, err := basicAuthParse(caddy.NewTestController(test.input)) | ||||
| 
 | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | ||||
							
								
								
									
										25
									
								
								caddyhttp/bind/bind.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								caddyhttp/bind/bind.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| package bind | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "bind", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setupBind, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func setupBind(c *caddy.Controller) error { | ||||
| 	config := httpserver.GetConfig(c.Key) | ||||
| 	for c.Next() { | ||||
| 		if !c.Args(&config.ListenHost) { | ||||
| 			return c.ArgErr() | ||||
| 		} | ||||
| 		config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309 | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										23
									
								
								caddyhttp/bind/bind_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								caddyhttp/bind/bind_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| package bind | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestSetupBind(t *testing.T) { | ||||
| 	err := setupBind(caddy.NewTestController(`bind 1.2.3.4`)) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Expected no errors, but got: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	cfg := httpserver.GetConfig("") | ||||
| 	if got, want := cfg.ListenHost, "1.2.3.4"; got != want { | ||||
| 		t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got) | ||||
| 	} | ||||
| 	if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want { | ||||
| 		t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got) | ||||
| 	} | ||||
| } | ||||
| @ -16,13 +16,14 @@ import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/dustin/go-humanize" | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| 	"github.com/mholt/caddy/caddyhttp/staticfiles" | ||||
| ) | ||||
| 
 | ||||
| // Browse is an http.Handler that can show a file listing when | ||||
| // directories in the given paths are specified. | ||||
| type Browse struct { | ||||
| 	Next          middleware.Handler | ||||
| 	Next          httpserver.Handler | ||||
| 	Configs       []Config | ||||
| 	IgnoreIndexes bool | ||||
| } | ||||
| @ -67,7 +68,7 @@ type Listing struct { | ||||
| 	// Optional custom variables for use in browse templates | ||||
| 	User interface{} | ||||
| 
 | ||||
| 	middleware.Context | ||||
| 	httpserver.Context | ||||
| } | ||||
| 
 | ||||
| // BreadcrumbMap returns l.Path where every element is a map | ||||
| @ -195,7 +196,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listin | ||||
| 	for _, f := range files { | ||||
| 		name := f.Name() | ||||
| 
 | ||||
| 		for _, indexName := range middleware.IndexPages { | ||||
| 		for _, indexName := range staticfiles.IndexPages { | ||||
| 			if name == indexName { | ||||
| 				hasIndexFile = true | ||||
| 				break | ||||
| @ -237,7 +238,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	var bc *Config | ||||
| 	// See if there's a browse configuration to match the path | ||||
| 	for i := range b.Configs { | ||||
| 		if middleware.Path(r.URL.Path).Matches(b.Configs[i].PathScope) { | ||||
| 		if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) { | ||||
| 			bc = &b.Configs[i] | ||||
| 			goto inScope | ||||
| 		} | ||||
| @ -370,7 +371,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi | ||||
| 	if containsIndex && !b.IgnoreIndexes { // directory isn't browsable | ||||
| 		return b.Next.ServeHTTP(w, r) | ||||
| 	} | ||||
| 	listing.Context = middleware.Context{ | ||||
| 	listing.Context = httpserver.Context{ | ||||
| 		Root: bc.Root, | ||||
| 		Req:  r, | ||||
| 		URL:  r.URL, | ||||
| @ -12,20 +12,9 @@ import ( | ||||
| 	"text/template" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // "sort" package has "IsSorted" function, but no "IsReversed"; | ||||
| func isReversed(data sort.Interface) bool { | ||||
| 	n := data.Len() | ||||
| 	for i := n - 1; i > 0; i-- { | ||||
| 		if !data.Less(i, i-1) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func TestSort(t *testing.T) { | ||||
| 	// making up []fileInfo with bogus values; | ||||
| 	// to be used to make up our "listing" | ||||
| @ -111,7 +100,7 @@ func TestBrowseHTTPMethods(t *testing.T) { | ||||
| 	} | ||||
| 
 | ||||
| 	b := Browse{ | ||||
| 		Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 			return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield | ||||
| 		}), | ||||
| 		Configs: []Config{ | ||||
| @ -149,7 +138,7 @@ func TestBrowseTemplate(t *testing.T) { | ||||
| 	} | ||||
| 
 | ||||
| 	b := Browse{ | ||||
| 		Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 			t.Fatalf("Next shouldn't be called") | ||||
| 			return 0, nil | ||||
| 		}), | ||||
| @ -202,9 +191,8 @@ func TestBrowseTemplate(t *testing.T) { | ||||
| } | ||||
| 
 | ||||
| func TestBrowseJson(t *testing.T) { | ||||
| 
 | ||||
| 	b := Browse{ | ||||
| 		Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 			t.Fatalf("Next shouldn't be called") | ||||
| 			return 0, nil | ||||
| 		}), | ||||
| @ -354,3 +342,14 @@ func TestBrowseJson(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // "sort" package has "IsSorted" function, but no "IsReversed"; | ||||
| func isReversed(data sort.Interface) bool { | ||||
| 	n := data.Len() | ||||
| 	for i := n - 1; i > 0; i-- { | ||||
| 		if !data.Less(i, i-1) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package setup | ||||
| package browse | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| @ -6,32 +6,44 @@ import ( | ||||
| 	"net/http" | ||||
| 	"text/template" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/browse" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Browse configures a new Browse middleware instance. | ||||
| func Browse(c *Controller) (middleware.Middleware, error) { | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "browse", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setup configures a new Browse middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	configs, err := browseParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	browse := browse.Browse{ | ||||
| 	b := Browse{ | ||||
| 		Configs:       configs, | ||||
| 		IgnoreIndexes: false, | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		browse.Next = next | ||||
| 		return browse | ||||
| 	}, nil | ||||
| 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		b.Next = next | ||||
| 		return b | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func browseParse(c *Controller) ([]browse.Config, error) { | ||||
| 	var configs []browse.Config | ||||
| func browseParse(c *caddy.Controller) ([]Config, error) { | ||||
| 	var configs []Config | ||||
| 
 | ||||
| 	appendCfg := func(bc browse.Config) error { | ||||
| 	cfg := httpserver.GetConfig(c.Key) | ||||
| 
 | ||||
| 	appendCfg := func(bc Config) error { | ||||
| 		for _, c := range configs { | ||||
| 			if c.PathScope == bc.PathScope { | ||||
| 				return fmt.Errorf("duplicate browsing config for %s", c.PathScope) | ||||
| @ -42,7 +54,7 @@ func browseParse(c *Controller) ([]browse.Config, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		var bc browse.Config | ||||
| 		var bc Config | ||||
| 
 | ||||
| 		// First argument is directory to allow browsing; default is site root | ||||
| 		if c.NextArg() { | ||||
| @ -50,7 +62,7 @@ func browseParse(c *Controller) ([]browse.Config, error) { | ||||
| 		} else { | ||||
| 			bc.PathScope = "/" | ||||
| 		} | ||||
| 		bc.Root = http.Dir(c.Root) | ||||
| 		bc.Root = http.Dir(cfg.Root) | ||||
| 		theRoot, err := bc.Root.Open("/") // catch a missing path early | ||||
| 		if err != nil { | ||||
| 			return configs, err | ||||
| @ -1,4 +1,4 @@ | ||||
| package setup | ||||
| package browse | ||||
| 
 | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| @ -8,12 +8,13 @@ import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/browse" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestBrowse(t *testing.T) { | ||||
| 
 | ||||
| 	tempDirPath, err := getTempDirPath() | ||||
| func TestSetup(t *testing.T) { | ||||
| 	tempDirPath := os.TempDir() | ||||
| 	_, err := os.Stat(tempDirPath) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) | ||||
| 	} | ||||
| @ -35,7 +36,7 @@ func TestBrowse(t *testing.T) { | ||||
| 		// test case #0 tests handling of multiple pathscopes | ||||
| 		{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false}, | ||||
| 
 | ||||
| 		// test case #1 tests instantiation of browse.Config with default values | ||||
| 		// test case #1 tests instantiation of Config with default values | ||||
| 		{"browse /", []string{"/"}, false}, | ||||
| 
 | ||||
| 		// test case #2 tests detectaction of custom template | ||||
| @ -48,14 +49,16 @@ func TestBrowse(t *testing.T) { | ||||
| 		{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true}, | ||||
| 	} { | ||||
| 
 | ||||
| 		recievedFunc, err := Browse(NewTestController(test.input)) | ||||
| 		err := setup(caddy.NewTestController(test.input)) | ||||
| 		if err != nil && !test.shouldErr { | ||||
| 			t.Errorf("Test case #%d recieved an error of %v", i, err) | ||||
| 		} | ||||
| 		if test.expectedPathScope == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		recievedConfigs := recievedFunc(nil).(browse.Browse).Configs | ||||
| 		mids := httpserver.GetConfig("").Middleware() | ||||
| 		mid := mids[len(mids)-1] | ||||
| 		recievedConfigs := mid(nil).(Browse).Configs | ||||
| 		for j, config := range recievedConfigs { | ||||
| 			if config.PathScope != test.expectedPathScope[j] { | ||||
| 				t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope) | ||||
							
								
								
									
										29
									
								
								caddyhttp/caddyhttp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								caddyhttp/caddyhttp.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| package caddyhttp | ||||
| 
 | ||||
| import ( | ||||
| 	// plug in the server | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| 
 | ||||
| 	// plug in the standard directives | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/basicauth" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/bind" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/browse" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/errors" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/expvar" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/extensions" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/fastcgi" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/gzip" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/header" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/internalsrv" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/log" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/markdown" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/mime" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/pprof" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/proxy" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/redirect" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/rewrite" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/root" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/templates" | ||||
| 	_ "github.com/mholt/caddy/caddyhttp/websocket" | ||||
| 	_ "github.com/mholt/caddy/startupshutdown" | ||||
| ) | ||||
| @ -11,16 +11,25 @@ import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "errors", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // ErrorHandler handles HTTP errors (and errors from other middleware). | ||||
| type ErrorHandler struct { | ||||
| 	Next       middleware.Handler | ||||
| 	Next       httpserver.Handler | ||||
| 	ErrorPages map[int]string // map of status code to filename | ||||
| 	LogFile    string | ||||
| 	Log        *log.Logger | ||||
| 	LogRoller  *middleware.LogRoller | ||||
| 	LogRoller  *httpserver.LogRoller | ||||
| 	Debug      bool // if true, errors are written out to client rather than to a log | ||||
| } | ||||
| 
 | ||||
| @ -31,13 +40,12 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err) | ||||
| 
 | ||||
| 		if h.Debug { | ||||
| 			// Write error to response instead of to log | ||||
| 			w.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||||
| 			w.WriteHeader(status) | ||||
| 			fmt.Fprintln(w, errMsg) | ||||
| 			return 0, err // returning < 400 signals that a response has been written | ||||
| 			return 0, err // returning 0 signals that a response has been written | ||||
| 		} | ||||
| 		h.Log.Println(errMsg) | ||||
| 	} | ||||
| @ -54,18 +62,15 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er | ||||
| // code. If there is an error serving the error page, a plaintext error | ||||
| // message is written instead, and the extra error is logged. | ||||
| func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) { | ||||
| 	defaultBody := fmt.Sprintf("%d %s", code, http.StatusText(code)) | ||||
| 
 | ||||
| 	// See if an error page for this status code was specified | ||||
| 	if pagePath, ok := h.ErrorPages[code]; ok { | ||||
| 
 | ||||
| 		// Try to open it | ||||
| 		errorPage, err := os.Open(pagePath) | ||||
| 		if err != nil { | ||||
| 			// An additional error handling an error... <insert grumpy cat here> | ||||
| 			h.Log.Printf("%s [NOTICE %d %s] could not load error page: %v", | ||||
| 				time.Now().Format(timeFormat), code, r.URL.String(), err) | ||||
| 			http.Error(w, defaultBody, code) | ||||
| 			httpserver.DefaultErrorFunc(w, r, code) | ||||
| 			return | ||||
| 		} | ||||
| 		defer errorPage.Close() | ||||
| @ -79,14 +84,14 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int | ||||
| 			// Epic fail... sigh. | ||||
| 			h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v", | ||||
| 				time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err) | ||||
| 			http.Error(w, defaultBody, code) | ||||
| 			httpserver.DefaultErrorFunc(w, r, code) | ||||
| 		} | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Default error response | ||||
| 	http.Error(w, defaultBody, code) | ||||
| 	httpserver.DefaultErrorFunc(w, r, code) | ||||
| } | ||||
| 
 | ||||
| func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) { | ||||
| @ -125,9 +130,7 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) { | ||||
| 		// Write error and stack trace to the response rather than to a log | ||||
| 		var stackBuf [4096]byte | ||||
| 		stack := stackBuf[:runtime.Stack(stackBuf[:], false)] | ||||
| 		w.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||||
| 		w.WriteHeader(http.StatusInternalServerError) | ||||
| 		fmt.Fprintf(w, "%s\n\n%s", panicMsg, stack) | ||||
| 		httpserver.WriteTextResponse(w, http.StatusInternalServerError, fmt.Sprintf("%s\n\n%s", panicMsg, stack)) | ||||
| 	} else { | ||||
| 		// Currently we don't use the function name, since file:line is more conventional | ||||
| 		h.Log.Printf(panicMsg) | ||||
| @ -13,7 +13,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestErrors(t *testing.T) { | ||||
| @ -44,7 +44,7 @@ func TestErrors(t *testing.T) { | ||||
| 
 | ||||
| 	testErr := errors.New("test error") | ||||
| 	tests := []struct { | ||||
| 		next         middleware.Handler | ||||
| 		next         httpserver.Handler | ||||
| 		expectedCode int | ||||
| 		expectedBody string | ||||
| 		expectedLog  string | ||||
| @ -124,7 +124,7 @@ func TestVisibleErrorWithPanic(t *testing.T) { | ||||
| 	eh := ErrorHandler{ | ||||
| 		ErrorPages: make(map[int]string), | ||||
| 		Debug:      true, | ||||
| 		Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 			panic(panicMsg) | ||||
| 		}), | ||||
| 	} | ||||
| @ -146,7 +146,7 @@ func TestVisibleErrorWithPanic(t *testing.T) { | ||||
| 
 | ||||
| 	body := rec.Body.String() | ||||
| 
 | ||||
| 	if !strings.Contains(body, "[PANIC /] middleware/errors/errors_test.go") { | ||||
| 	if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") { | ||||
| 		t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body) | ||||
| 	} | ||||
| 	if !strings.Contains(body, panicMsg) { | ||||
| @ -157,8 +157,8 @@ func TestVisibleErrorWithPanic(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func genErrorHandler(status int, err error, body string) middleware.Handler { | ||||
| 	return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| func genErrorHandler(status int, err error, body string) httpserver.Handler { | ||||
| 	return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		if len(body) > 0 { | ||||
| 			w.Header().Set("Content-Length", strconv.Itoa(len(body))) | ||||
| 			fmt.Fprint(w, body) | ||||
| @ -1,4 +1,4 @@ | ||||
| package setup | ||||
| package errors | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| @ -8,19 +8,19 @@ import ( | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/hashicorp/go-syslog" | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/errors" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Errors configures a new errors middleware instance. | ||||
| func Errors(c *Controller) (middleware.Middleware, error) { | ||||
| // setup configures a new errors middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	handler, err := errorsParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Open the log file for writing when the server starts | ||||
| 	c.Startup = append(c.Startup, func() error { | ||||
| 	c.OnStartup(func() error { | ||||
| 		var err error | ||||
| 		var writer io.Writer | ||||
| 
 | ||||
| @ -62,17 +62,21 @@ func Errors(c *Controller) (middleware.Middleware, error) { | ||||
| 		return nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		handler.Next = next | ||||
| 		return handler | ||||
| 	}, nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func errorsParse(c *Controller) (*errors.ErrorHandler, error) { | ||||
| 	// Very important that we make a pointer because the Startup | ||||
| func errorsParse(c *caddy.Controller) (*ErrorHandler, error) { | ||||
| 	// Very important that we make a pointer because the startup | ||||
| 	// function that opens the log file must have access to the | ||||
| 	// same instance of the handler, not a copy. | ||||
| 	handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)} | ||||
| 	handler := &ErrorHandler{ErrorPages: make(map[int]string)} | ||||
| 
 | ||||
| 	cfg := httpserver.GetConfig(c.Key) | ||||
| 
 | ||||
| 	optionalBlock := func() (bool, error) { | ||||
| 		var hadBlock bool | ||||
| @ -94,7 +98,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { | ||||
| 					if c.NextArg() { | ||||
| 						if c.Val() == "{" { | ||||
| 							c.IncrNest() | ||||
| 							logRoller, err := parseRoller(c) | ||||
| 							logRoller, err := httpserver.ParseRoller(c) | ||||
| 							if err != nil { | ||||
| 								return hadBlock, err | ||||
| 							} | ||||
| @ -104,7 +108,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { | ||||
| 				} | ||||
| 			} else { | ||||
| 				// Error page; ensure it exists | ||||
| 				where = filepath.Join(c.Root, where) | ||||
| 				where = filepath.Join(cfg.Root, where) | ||||
| 				f, err := os.Open(where) | ||||
| 				if err != nil { | ||||
| 					log.Printf("[WARNING] Unable to open error page '%s': %v", where, err) | ||||
| @ -1,27 +1,24 @@ | ||||
| package setup | ||||
| package errors | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/errors" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestErrors(t *testing.T) { | ||||
| 	c := NewTestController(`errors`) | ||||
| 	mid, err := Errors(c) | ||||
| 
 | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`errors`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middlewares, was nil instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(*errors.ErrorHandler) | ||||
| 
 | ||||
| 	handler := mids[0](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(*ErrorHandler) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler) | ||||
| 	} | ||||
| @ -32,53 +29,53 @@ func TestErrors(t *testing.T) { | ||||
| 	if myHandler.LogRoller != nil { | ||||
| 		t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller) | ||||
| 	} | ||||
| 	if !SameNext(myHandler.Next, EmptyNext) { | ||||
| 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| 
 | ||||
| 	// Test Startup function | ||||
| 	if len(c.Startup) == 0 { | ||||
| 		t.Fatal("Expected 1 startup function, had 0") | ||||
| 	} | ||||
| 	c.Startup[0]() | ||||
| 	if myHandler.Log == nil { | ||||
| 		t.Error("Expected Log to be non-nil after startup because Debug is not enabled") | ||||
| 	} | ||||
| 	// Test Startup function -- TODO | ||||
| 	// if len(c.Startup) == 0 { | ||||
| 	// 	t.Fatal("Expected 1 startup function, had 0") | ||||
| 	// } | ||||
| 	// c.Startup[0]() | ||||
| 	// if myHandler.Log == nil { | ||||
| 	// 	t.Error("Expected Log to be non-nil after startup because Debug is not enabled") | ||||
| 	// } | ||||
| } | ||||
| 
 | ||||
| func TestErrorsParse(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		inputErrorsRules     string | ||||
| 		shouldErr            bool | ||||
| 		expectedErrorHandler errors.ErrorHandler | ||||
| 		expectedErrorHandler ErrorHandler | ||||
| 	}{ | ||||
| 		{`errors`, false, errors.ErrorHandler{ | ||||
| 		{`errors`, false, ErrorHandler{ | ||||
| 			LogFile: "", | ||||
| 		}}, | ||||
| 		{`errors errors.txt`, false, errors.ErrorHandler{ | ||||
| 		{`errors errors.txt`, false, ErrorHandler{ | ||||
| 			LogFile: "errors.txt", | ||||
| 		}}, | ||||
| 		{`errors visible`, false, errors.ErrorHandler{ | ||||
| 		{`errors visible`, false, ErrorHandler{ | ||||
| 			LogFile: "", | ||||
| 			Debug:   true, | ||||
| 		}}, | ||||
| 		{`errors { log visible }`, false, errors.ErrorHandler{ | ||||
| 		{`errors { log visible }`, false, ErrorHandler{ | ||||
| 			LogFile: "", | ||||
| 			Debug:   true, | ||||
| 		}}, | ||||
| 		{`errors { log errors.txt | ||||
|         404 404.html | ||||
|         500 500.html | ||||
| }`, false, errors.ErrorHandler{ | ||||
| }`, false, ErrorHandler{ | ||||
| 			LogFile: "errors.txt", | ||||
| 			ErrorPages: map[int]string{ | ||||
| 				404: "404.html", | ||||
| 				500: "500.html", | ||||
| 			}, | ||||
| 		}}, | ||||
| 		{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{ | ||||
| 		{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, ErrorHandler{ | ||||
| 			LogFile: "errors.txt", | ||||
| 			LogRoller: &middleware.LogRoller{ | ||||
| 			LogRoller: &httpserver.LogRoller{ | ||||
| 				MaxSize:    2, | ||||
| 				MaxAge:     10, | ||||
| 				MaxBackups: 3, | ||||
| @ -92,13 +89,13 @@ func TestErrorsParse(t *testing.T) { | ||||
|         } | ||||
|         404 404.html | ||||
|         503 503.html | ||||
| }`, false, errors.ErrorHandler{ | ||||
| }`, false, ErrorHandler{ | ||||
| 			LogFile: "errors.txt", | ||||
| 			ErrorPages: map[int]string{ | ||||
| 				404: "404.html", | ||||
| 				503: "503.html", | ||||
| 			}, | ||||
| 			LogRoller: &middleware.LogRoller{ | ||||
| 			LogRoller: &httpserver.LogRoller{ | ||||
| 				MaxSize:    3, | ||||
| 				MaxAge:     11, | ||||
| 				MaxBackups: 5, | ||||
| @ -107,8 +104,7 @@ func TestErrorsParse(t *testing.T) { | ||||
| 		}}, | ||||
| 	} | ||||
| 	for i, test := range tests { | ||||
| 		c := NewTestController(test.inputErrorsRules) | ||||
| 		actualErrorsRule, err := errorsParse(c) | ||||
| 		actualErrorsRule, err := errorsParse(caddy.NewTestController(test.inputErrorsRules)) | ||||
| 
 | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | ||||
| @ -5,23 +5,22 @@ import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // ExpVar is a simple struct to hold expvar's configuration | ||||
| type ExpVar struct { | ||||
| 	Next     middleware.Handler | ||||
| 	Next     httpserver.Handler | ||||
| 	Resource Resource | ||||
| } | ||||
| 
 | ||||
| // ServeHTTP handles requests to expvar's configured entry point with | ||||
| // expvar, or passes all other requests up the chain. | ||||
| func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	if middleware.Path(r.URL.Path).Matches(string(e.Resource)) { | ||||
| 	if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) { | ||||
| 		expvarHandler(w, r) | ||||
| 		return 0, nil | ||||
| 	} | ||||
| 
 | ||||
| 	return e.Next.ServeHTTP(w, r) | ||||
| } | ||||
| 
 | ||||
| @ -6,12 +6,12 @@ import ( | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestExpVar(t *testing.T) { | ||||
| 	rw := ExpVar{ | ||||
| 		Next:     middleware.HandlerFunc(contentHandler), | ||||
| 		Next:     httpserver.HandlerFunc(contentHandler), | ||||
| 		Resource: "/d/v", | ||||
| 	} | ||||
| 
 | ||||
							
								
								
									
										70
									
								
								caddyhttp/expvar/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								caddyhttp/expvar/setup.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| package expvar | ||||
| 
 | ||||
| import ( | ||||
| 	"expvar" | ||||
| 	"runtime" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "expvar", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setup configures a new ExpVar middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	resource, err := expVarParse(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// publish any extra information/metrics we may want to capture | ||||
| 	publishExtraVars() | ||||
| 
 | ||||
| 	ev := ExpVar{Resource: resource} | ||||
| 
 | ||||
| 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		ev.Next = next | ||||
| 		return ev | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func expVarParse(c *caddy.Controller) (Resource, error) { | ||||
| 	var resource Resource | ||||
| 	var err error | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		args := c.RemainingArgs() | ||||
| 		switch len(args) { | ||||
| 		case 0: | ||||
| 			resource = Resource(defaultExpvarPath) | ||||
| 		case 1: | ||||
| 			resource = Resource(args[0]) | ||||
| 		default: | ||||
| 			return resource, c.ArgErr() | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return resource, err | ||||
| } | ||||
| 
 | ||||
| func publishExtraVars() { | ||||
| 	// By using sync.Once instead of an init() function, we don't clutter | ||||
| 	// the app's expvar export unnecessarily, or risk colliding with it. | ||||
| 	publishOnce.Do(func() { | ||||
| 		expvar.Publish("Goroutines", expvar.Func(func() interface{} { | ||||
| 			return runtime.NumGoroutine() | ||||
| 		})) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| var publishOnce sync.Once // publishing variables should only be done once | ||||
| var defaultExpvarPath = "/debug/vars" | ||||
							
								
								
									
										40
									
								
								caddyhttp/expvar/setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								caddyhttp/expvar/setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| package expvar | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`expvar`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middleware, got 0 instead") | ||||
| 	} | ||||
| 
 | ||||
| 	err = setup(caddy.NewTestController(`expvar /d/v`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 	mids = httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middleware, got 0 instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mids[1](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(ExpVar) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler) | ||||
| 	} | ||||
| 	if myHandler.Resource != "/d/v" { | ||||
| 		t.Errorf("Expected /d/v as expvar resource") | ||||
| 	} | ||||
| 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| } | ||||
| @ -12,14 +12,14 @@ import ( | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Ext can assume an extension from clean URLs. | ||||
| // It tries extensions in the order listed in Extensions. | ||||
| type Ext struct { | ||||
| 	// Next handler in the chain | ||||
| 	Next middleware.Handler | ||||
| 	Next httpserver.Handler | ||||
| 
 | ||||
| 	// Path to ther root of the site | ||||
| 	Root string | ||||
| @ -28,7 +28,7 @@ type Ext struct { | ||||
| 	Extensions []string | ||||
| } | ||||
| 
 | ||||
| // ServeHTTP implements the middleware.Handler interface. | ||||
| // ServeHTTP implements the httpserver.Handler interface. | ||||
| func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	urlpath := strings.TrimSuffix(r.URL.Path, "/") | ||||
| 	if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' { | ||||
							
								
								
									
										54
									
								
								caddyhttp/extensions/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								caddyhttp/extensions/setup.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| package extensions | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "ext", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setup configures a new instance of 'extensions' middleware for clean URLs. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	cfg := httpserver.GetConfig(c.Key) | ||||
| 	root := cfg.Root | ||||
| 
 | ||||
| 	exts, err := extParse(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		return Ext{ | ||||
| 			Next:       next, | ||||
| 			Extensions: exts, | ||||
| 			Root:       root, | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // extParse sets up an instance of extension middleware | ||||
| // from a middleware controller and returns a list of extensions. | ||||
| func extParse(c *caddy.Controller) ([]string, error) { | ||||
| 	var exts []string | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		// At least one extension is required | ||||
| 		if !c.NextArg() { | ||||
| 			return exts, c.ArgErr() | ||||
| 		} | ||||
| 		exts = append(exts, c.Val()) | ||||
| 
 | ||||
| 		// Tack on any other extensions that may have been listed | ||||
| 		exts = append(exts, c.RemainingArgs()...) | ||||
| 	} | ||||
| 
 | ||||
| 	return exts, nil | ||||
| } | ||||
| @ -1,26 +1,25 @@ | ||||
| package setup | ||||
| package extensions | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/extensions" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestExt(t *testing.T) { | ||||
| 	c := NewTestController(`ext .html .htm .php`) | ||||
| 
 | ||||
| 	mid, err := Ext(c) | ||||
| 
 | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`ext .html .htm .php`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 		t.Fatalf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middleware, had 0 instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(extensions.Ext) | ||||
| 	handler := mids[0](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(Ext) | ||||
| 
 | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type Ext, got: %#v", handler) | ||||
| @ -35,7 +34,7 @@ func TestExt(t *testing.T) { | ||||
| 	if myHandler.Extensions[2] != ".php" { | ||||
| 		t.Errorf("Expected .php in the list of Extensions") | ||||
| 	} | ||||
| 	if !SameNext(myHandler.Next, EmptyNext) { | ||||
| 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| 
 | ||||
| @ -52,8 +51,7 @@ func TestExtParse(t *testing.T) { | ||||
| 		{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}}, | ||||
| 	} | ||||
| 	for i, test := range tests { | ||||
| 		c := NewTestController(test.inputExts) | ||||
| 		actualExts, err := extParse(c) | ||||
| 		actualExts, err := extParse(caddy.NewTestController(test.inputExts)) | ||||
| 
 | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | ||||
							
								
								
									
										14
									
								
								middleware/fastcgi/fastcgi.go → caddyhttp/fastcgi/fastcgi.go
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										14
									
								
								middleware/fastcgi/fastcgi.go → caddyhttp/fastcgi/fastcgi.go
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -13,12 +13,12 @@ import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Handler is a middleware type that can handle requests as a FastCGI client. | ||||
| type Handler struct { | ||||
| 	Next    middleware.Handler | ||||
| 	Next    httpserver.Handler | ||||
| 	Rules   []Rule | ||||
| 	Root    string | ||||
| 	AbsRoot string // same as root, but absolute path | ||||
| @ -31,12 +31,12 @@ type Handler struct { | ||||
| 	ServerPort      string | ||||
| } | ||||
| 
 | ||||
| // ServeHTTP satisfies the middleware.Handler interface. | ||||
| // ServeHTTP satisfies the httpserver.Handler interface. | ||||
| func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	for _, rule := range h.Rules { | ||||
| 
 | ||||
| 		// First requirement: Base path must match and the path must be allowed. | ||||
| 		if !middleware.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) { | ||||
| 		if !httpserver.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| @ -47,7 +47,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) | ||||
| 
 | ||||
| 		fpath := r.URL.Path | ||||
| 
 | ||||
| 		if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok { | ||||
| 		if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok { | ||||
| 			fpath = idx | ||||
| 			// Index file present. | ||||
| 			// If request path cannot be split, return error. | ||||
| @ -305,7 +305,7 @@ func (r Rule) canSplit(path string) bool { | ||||
| // splitPos returns the index where path should be split | ||||
| // based on rule.SplitPath. | ||||
| func (r Rule) splitPos(path string) int { | ||||
| 	if middleware.CaseSensitivePath { | ||||
| 	if httpserver.CaseSensitivePath { | ||||
| 		return strings.Index(path, r.SplitPath) | ||||
| 	} | ||||
| 	return strings.Index(strings.ToLower(path), strings.ToLower(r.SplitPath)) | ||||
| @ -314,7 +314,7 @@ func (r Rule) splitPos(path string) int { | ||||
| // AllowedPath checks if requestPath is not an ignored path. | ||||
| func (r Rule) AllowedPath(requestPath string) bool { | ||||
| 	for _, ignoredSubPath := range r.IgnoredSubPaths { | ||||
| 		if middleware.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) { | ||||
| 		if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| @ -1,46 +1,57 @@ | ||||
| package setup | ||||
| package fastcgi | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/fastcgi" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // FastCGI configures a new FastCGI middleware instance. | ||||
| func FastCGI(c *Controller) (middleware.Middleware, error) { | ||||
| 	absRoot, err := filepath.Abs(c.Root) | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "fastcgi", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // setup configures a new FastCGI middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	cfg := httpserver.GetConfig(c.Key) | ||||
| 	absRoot, err := filepath.Abs(cfg.Root) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	rules, err := fastcgiParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return fastcgi.Handler{ | ||||
| 	cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		return Handler{ | ||||
| 			Next:            next, | ||||
| 			Rules:           rules, | ||||
| 			Root:            c.Root, | ||||
| 			Root:            cfg.Root, | ||||
| 			AbsRoot:         absRoot, | ||||
| 			FileSys:         http.Dir(c.Root), | ||||
| 			SoftwareName:    c.AppName, | ||||
| 			SoftwareVersion: c.AppVersion, | ||||
| 			ServerName:      c.Host, | ||||
| 			ServerPort:      c.Port, | ||||
| 			FileSys:         http.Dir(cfg.Root), | ||||
| 			SoftwareName:    caddy.AppName, | ||||
| 			SoftwareVersion: caddy.AppVersion, | ||||
| 			ServerName:      cfg.Addr.Host, | ||||
| 			ServerPort:      cfg.Addr.Port, | ||||
| 		} | ||||
| 	}, nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) { | ||||
| 	var rules []fastcgi.Rule | ||||
| func fastcgiParse(c *caddy.Controller) ([]Rule, error) { | ||||
| 	var rules []Rule | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		var rule fastcgi.Rule | ||||
| 		var rule Rule | ||||
| 
 | ||||
| 		args := c.RemainingArgs() | ||||
| 
 | ||||
| @ -103,7 +114,7 @@ func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) { | ||||
| 
 | ||||
| // fastcgiPreset configures rule according to name. It returns an error if | ||||
| // name is not a recognized preset name. | ||||
| func fastcgiPreset(name string, rule *fastcgi.Rule) error { | ||||
| func fastcgiPreset(name string, rule *Rule) error { | ||||
| 	switch name { | ||||
| 	case "php": | ||||
| 		rule.Ext = ".php" | ||||
| @ -1,28 +1,25 @@ | ||||
| package setup | ||||
| package fastcgi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/fastcgi" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestFastCGI(t *testing.T) { | ||||
| 
 | ||||
| 	c := NewTestController(`fastcgi / 127.0.0.1:9000`) | ||||
| 
 | ||||
| 	mid, err := FastCGI(c) | ||||
| 
 | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`fastcgi / 127.0.0.1:9000`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, got: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middleware, got 0 instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(fastcgi.Handler) | ||||
| 	handler := mids[0](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(Handler) | ||||
| 
 | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type , got: %#v", handler) | ||||
| @ -41,11 +38,11 @@ func TestFastcgiParse(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		inputFastcgiConfig    string | ||||
| 		shouldErr             bool | ||||
| 		expectedFastcgiConfig []fastcgi.Rule | ||||
| 		expectedFastcgiConfig []Rule | ||||
| 	}{ | ||||
| 
 | ||||
| 		{`fastcgi /blog 127.0.0.1:9000 php`, | ||||
| 			false, []fastcgi.Rule{{ | ||||
| 			false, []Rule{{ | ||||
| 				Path:       "/blog", | ||||
| 				Address:    "127.0.0.1:9000", | ||||
| 				Ext:        ".php", | ||||
| @ -55,7 +52,7 @@ func TestFastcgiParse(t *testing.T) { | ||||
| 		{`fastcgi / 127.0.0.1:9001 { | ||||
| 	              split .html | ||||
| 	              }`, | ||||
| 			false, []fastcgi.Rule{{ | ||||
| 			false, []Rule{{ | ||||
| 				Path:       "/", | ||||
| 				Address:    "127.0.0.1:9001", | ||||
| 				Ext:        "", | ||||
| @ -66,7 +63,7 @@ func TestFastcgiParse(t *testing.T) { | ||||
| 	              split .html | ||||
| 	              except /admin /user | ||||
| 	              }`, | ||||
| 			false, []fastcgi.Rule{{ | ||||
| 			false, []Rule{{ | ||||
| 				Path:            "/", | ||||
| 				Address:         "127.0.0.1:9001", | ||||
| 				Ext:             "", | ||||
| @ -76,8 +73,7 @@ func TestFastcgiParse(t *testing.T) { | ||||
| 			}}}, | ||||
| 	} | ||||
| 	for i, test := range tests { | ||||
| 		c := NewTestController(test.inputFastcgiConfig) | ||||
| 		actualFastcgiConfigs, err := fastcgiParse(c) | ||||
| 		actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController(test.inputFastcgiConfig)) | ||||
| 
 | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | ||||
| @ -1,4 +1,4 @@ | ||||
| // Package gzip provides a simple middleware layer that performs | ||||
| // Package gzip provides a middleware layer that performs | ||||
| // gzip compression on the response. | ||||
| package gzip | ||||
| 
 | ||||
| @ -12,15 +12,24 @@ import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "gzip", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Gzip is a middleware type which gzips HTTP responses. It is | ||||
| // imperative that any handler which writes to a gzipped response | ||||
| // specifies the Content-Type, otherwise some clients will assume | ||||
| // application/x-gzip and try to download a file. | ||||
| type Gzip struct { | ||||
| 	Next    middleware.Handler | ||||
| 	Next    httpserver.Handler | ||||
| 	Configs []Config | ||||
| } | ||||
| 
 | ||||
| @ -36,7 +45,6 @@ func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { | ||||
| 		return g.Next.ServeHTTP(w, r) | ||||
| 	} | ||||
| 
 | ||||
| outer: | ||||
| 	for _, c := range g.Configs { | ||||
| 
 | ||||
| @ -79,9 +87,7 @@ outer: | ||||
| 		// to send something back before gzipWriter gets closed at | ||||
| 		// the return of this method! | ||||
| 		if status >= 400 { | ||||
| 			gz.Header().Set("Content-Type", "text/plain") // very necessary | ||||
| 			gz.WriteHeader(status) | ||||
| 			fmt.Fprintf(gz, "%d %s", status, http.StatusText(status)) | ||||
| 			httpserver.DefaultErrorFunc(w, r, status) | ||||
| 			return 0, err | ||||
| 		} | ||||
| 		return status, err | ||||
| @ -8,11 +8,10 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestGzipHandler(t *testing.T) { | ||||
| 
 | ||||
| 	pathFilter := PathFilter{make(Set)} | ||||
| 	badPaths := []string{"/bad", "/nogzip", "/nongzip"} | ||||
| 	for _, p := range badPaths { | ||||
| @ -80,9 +79,8 @@ func TestGzipHandler(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func nextFunc(shouldGzip bool) middleware.Handler { | ||||
| 	return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 
 | ||||
| func nextFunc(shouldGzip bool) httpserver.Handler { | ||||
| 	return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		// write a relatively large text file | ||||
| 		b, err := ioutil.ReadFile("testdata/test.txt") | ||||
| 		if err != nil { | ||||
| @ -4,7 +4,7 @@ import ( | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // RequestFilter determines if a request should be gzipped. | ||||
| @ -15,7 +15,8 @@ type RequestFilter interface { | ||||
| } | ||||
| 
 | ||||
| // defaultExtensions is the list of default extensions for which to enable gzipping. | ||||
| var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json", ".md", ".xml", ".svg"} | ||||
| var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json", | ||||
| 	".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp"} | ||||
| 
 | ||||
| // DefaultExtFilter creates an ExtFilter with default extensions. | ||||
| func DefaultExtFilter() ExtFilter { | ||||
| @ -54,7 +55,7 @@ type PathFilter struct { | ||||
| // is found and true otherwise. | ||||
| func (p PathFilter) ShouldCompress(r *http.Request) bool { | ||||
| 	return !p.IgnoredPaths.ContainsFunc(func(value string) bool { | ||||
| 		return middleware.Path(r.URL.Path).Matches(value) | ||||
| 		return httpserver.Path(r.URL.Path).Matches(value) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| @ -7,7 +7,7 @@ import ( | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestLengthFilter(t *testing.T) { | ||||
| @ -61,7 +61,7 @@ func TestResponseFilterWriter(t *testing.T) { | ||||
| 	}} | ||||
| 
 | ||||
| 	for i, ts := range tests { | ||||
| 		server.Next = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 		server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 			w.Header().Set("Content-Length", fmt.Sprint(len(ts.body))) | ||||
| 			w.Write([]byte(ts.body)) | ||||
| 			return 200, nil | ||||
| @ -1,38 +1,40 @@ | ||||
| package setup | ||||
| package gzip | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/gzip" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Gzip configures a new gzip middleware instance. | ||||
| func Gzip(c *Controller) (middleware.Middleware, error) { | ||||
| // setup configures a new gzip middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	configs, err := gzipParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return gzip.Gzip{Next: next, Configs: configs} | ||||
| 	}, nil | ||||
| 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		return Gzip{Next: next, Configs: configs} | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func gzipParse(c *Controller) ([]gzip.Config, error) { | ||||
| 	var configs []gzip.Config | ||||
| func gzipParse(c *caddy.Controller) ([]Config, error) { | ||||
| 	var configs []Config | ||||
| 
 | ||||
| 	for c.Next() { | ||||
| 		config := gzip.Config{} | ||||
| 		config := Config{} | ||||
| 
 | ||||
| 		// Request Filters | ||||
| 		pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)} | ||||
| 		extFilter := gzip.ExtFilter{Exts: make(gzip.Set)} | ||||
| 		pathFilter := PathFilter{IgnoredPaths: make(Set)} | ||||
| 		extFilter := ExtFilter{Exts: make(Set)} | ||||
| 
 | ||||
| 		// Response Filters | ||||
| 		lengthFilter := gzip.LengthFilter(0) | ||||
| 		lengthFilter := LengthFilter(0) | ||||
| 
 | ||||
| 		// No extra args expected | ||||
| 		if len(c.RemainingArgs()) > 0 { | ||||
| @ -47,7 +49,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) { | ||||
| 					return configs, c.ArgErr() | ||||
| 				} | ||||
| 				for _, e := range exts { | ||||
| 					if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard && e != "" { | ||||
| 					if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" { | ||||
| 						return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e) | ||||
| 					} | ||||
| 					extFilter.Exts.Add(e) | ||||
| @ -82,18 +84,18 @@ func gzipParse(c *Controller) ([]gzip.Config, error) { | ||||
| 				} else if length == 0 { | ||||
| 					return configs, fmt.Errorf(`gzip: min_length must be greater than 0`) | ||||
| 				} | ||||
| 				lengthFilter = gzip.LengthFilter(length) | ||||
| 				lengthFilter = LengthFilter(length) | ||||
| 			default: | ||||
| 				return configs, c.ArgErr() | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// Request Filters | ||||
| 		config.RequestFilters = []gzip.RequestFilter{} | ||||
| 		config.RequestFilters = []RequestFilter{} | ||||
| 
 | ||||
| 		// If ignored paths are specified, put in front to filter with path first | ||||
| 		if len(pathFilter.IgnoredPaths) > 0 { | ||||
| 			config.RequestFilters = []gzip.RequestFilter{pathFilter} | ||||
| 			config.RequestFilters = []RequestFilter{pathFilter} | ||||
| 		} | ||||
| 
 | ||||
| 		// Then, if extensions are specified, use those to filter. | ||||
| @ -101,7 +103,7 @@ func gzipParse(c *Controller) ([]gzip.Config, error) { | ||||
| 		if len(extFilter.Exts) > 0 { | ||||
| 			config.RequestFilters = append(config.RequestFilters, extFilter) | ||||
| 		} else { | ||||
| 			config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter()) | ||||
| 			config.RequestFilters = append(config.RequestFilters, DefaultExtFilter()) | ||||
| 		} | ||||
| 
 | ||||
| 		// Response Filters | ||||
| @ -1,29 +1,29 @@ | ||||
| package setup | ||||
| package gzip | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/gzip" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestGzip(t *testing.T) { | ||||
| 	c := NewTestController(`gzip`) | ||||
| 
 | ||||
| 	mid, err := Gzip(c) | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`gzip`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, but got: %v", err) | ||||
| 	} | ||||
| 	if mid == nil { | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if mids == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(gzip.Gzip) | ||||
| 	handler := mids[0](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(Gzip) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type Gzip, got: %#v", handler) | ||||
| 	} | ||||
| 
 | ||||
| 	if !SameNext(myHandler.Next, EmptyNext) { | ||||
| 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| 
 | ||||
| @ -90,8 +90,7 @@ func TestGzip(t *testing.T) { | ||||
| 		`, false}, | ||||
| 	} | ||||
| 	for i, test := range tests { | ||||
| 		c := NewTestController(test.input) | ||||
| 		_, err := gzipParse(c) | ||||
| 		_, err := gzipParse(caddy.NewTestController(test.input)) | ||||
| 		if test.shouldErr && err == nil { | ||||
| 			t.Errorf("Test %v: Expected error but found nil", i) | ||||
| 		} else if !test.shouldErr && err != nil { | ||||
| @ -1,28 +1,28 @@ | ||||
| // Package headers provides middleware that appends headers to | ||||
| // Package header provides middleware that appends headers to | ||||
| // requests based on a set of configuration rules that define | ||||
| // which routes receive which headers. | ||||
| package headers | ||||
| package header | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Headers is middleware that adds headers to the responses | ||||
| // for requests matching a certain path. | ||||
| type Headers struct { | ||||
| 	Next  middleware.Handler | ||||
| 	Next  httpserver.Handler | ||||
| 	Rules []Rule | ||||
| } | ||||
| 
 | ||||
| // ServeHTTP implements the middleware.Handler interface and serves requests, | ||||
| // ServeHTTP implements the httpserver.Handler interface and serves requests, | ||||
| // setting headers on the response according to the configured rules. | ||||
| func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 	replacer := middleware.NewReplacer(r, nil, "") | ||||
| 	replacer := httpserver.NewReplacer(r, nil, "") | ||||
| 	for _, rule := range h.Rules { | ||||
| 		if middleware.Path(r.URL.Path).Matches(rule.Path) { | ||||
| 		if httpserver.Path(r.URL.Path).Matches(rule.Path) { | ||||
| 			for _, header := range rule.Headers { | ||||
| 				if strings.HasPrefix(header.Name, "-") { | ||||
| 					w.Header().Del(strings.TrimLeft(header.Name, "-")) | ||||
| @ -1,4 +1,4 @@ | ||||
| package headers | ||||
| package header | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| @ -6,10 +6,10 @@ import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestHeaders(t *testing.T) { | ||||
| func TestHeader(t *testing.T) { | ||||
| 	hostname, err := os.Hostname() | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Could not determine hostname: %v", err) | ||||
| @ -27,7 +27,7 @@ func TestHeaders(t *testing.T) { | ||||
| 		{"/b", "Bar", "Removed in /a"}, | ||||
| 	} { | ||||
| 		he := Headers{ | ||||
| 			Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 			Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { | ||||
| 				return 0, nil | ||||
| 			}), | ||||
| 			Rules: []Rule{ | ||||
| @ -1,27 +1,37 @@ | ||||
| package setup | ||||
| package header | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/mholt/caddy/middleware" | ||||
| 	"github.com/mholt/caddy/middleware/headers" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| // Headers configures a new Headers middleware instance. | ||||
| func Headers(c *Controller) (middleware.Middleware, error) { | ||||
| 	rules, err := headersParse(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return func(next middleware.Handler) middleware.Handler { | ||||
| 		return headers.Headers{Next: next, Rules: rules} | ||||
| 	}, nil | ||||
| func init() { | ||||
| 	caddy.RegisterPlugin(caddy.Plugin{ | ||||
| 		Name:       "header", | ||||
| 		ServerType: "http", | ||||
| 		Action:     setup, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func headersParse(c *Controller) ([]headers.Rule, error) { | ||||
| 	var rules []headers.Rule | ||||
| // setup configures a new Headers middleware instance. | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	rules, err := headersParse(c) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { | ||||
| 		return Headers{Next: next, Rules: rules} | ||||
| 	}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func headersParse(c *caddy.Controller) ([]Rule, error) { | ||||
| 	var rules []Rule | ||||
| 
 | ||||
| 	for c.NextLine() { | ||||
| 		var head headers.Rule | ||||
| 		var head Rule | ||||
| 		var isNewPattern bool | ||||
| 
 | ||||
| 		if !c.NextArg() { | ||||
| @ -46,7 +56,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) { | ||||
| 		for c.NextBlock() { | ||||
| 			// A block of headers was opened... | ||||
| 
 | ||||
| 			h := headers.Header{Name: c.Val()} | ||||
| 			h := Header{Name: c.Val()} | ||||
| 
 | ||||
| 			if c.NextArg() { | ||||
| 				h.Value = c.Val() | ||||
| @ -57,7 +67,7 @@ func headersParse(c *Controller) ([]headers.Rule, error) { | ||||
| 		if c.NextArg() { | ||||
| 			// ... or single header was defined as an argument instead. | ||||
| 
 | ||||
| 			h := headers.Header{Name: c.Val()} | ||||
| 			h := Header{Name: c.Val()} | ||||
| 
 | ||||
| 			h.Value = c.Val() | ||||
| 
 | ||||
| @ -1,30 +1,31 @@ | ||||
| package setup | ||||
| package header | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/mholt/caddy/middleware/headers" | ||||
| 	"github.com/mholt/caddy" | ||||
| 	"github.com/mholt/caddy/caddyhttp/httpserver" | ||||
| ) | ||||
| 
 | ||||
| func TestHeaders(t *testing.T) { | ||||
| 	c := NewTestController(`header / Foo Bar`) | ||||
| 
 | ||||
| 	mid, err := Headers(c) | ||||
| func TestSetup(t *testing.T) { | ||||
| 	err := setup(caddy.NewTestController(`header / Foo Bar`)) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no errors, but got: %v", err) | ||||
| 	} | ||||
| 	if mid == nil { | ||||
| 		t.Fatal("Expected middleware, was nil instead") | ||||
| 
 | ||||
| 	mids := httpserver.GetConfig("").Middleware() | ||||
| 	if len(mids) == 0 { | ||||
| 		t.Fatal("Expected middleware, had 0 instead") | ||||
| 	} | ||||
| 
 | ||||
| 	handler := mid(EmptyNext) | ||||
| 	myHandler, ok := handler.(headers.Headers) | ||||
| 	handler := mids[0](httpserver.EmptyNext) | ||||
| 	myHandler, ok := handler.(Headers) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("Expected handler to be type Headers, got: %#v", handler) | ||||
| 	} | ||||
| 
 | ||||
| 	if !SameNext(myHandler.Next, EmptyNext) { | ||||
| 	if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { | ||||
| 		t.Error("'Next' field of handler was not set properly") | ||||
| 	} | ||||
| } | ||||
| @ -33,17 +34,17 @@ func TestHeadersParse(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		input     string | ||||
| 		shouldErr bool | ||||
| 		expected  []headers.Rule | ||||
| 		expected  []Rule | ||||
| 	}{ | ||||
| 		{`header /foo Foo "Bar Baz"`, | ||||
| 			false, []headers.Rule{ | ||||
| 				{Path: "/foo", Headers: []headers.Header{ | ||||
| 			false, []Rule{ | ||||
| 				{Path: "/foo", Headers: []Header{ | ||||
| 					{Name: "Foo", Value: "Bar Baz"}, | ||||
| 				}}, | ||||
| 			}}, | ||||
| 		{`header /bar { Foo "Bar Baz" Baz Qux }`, | ||||
| 			false, []headers.Rule{ | ||||
| 				{Path: "/bar", Headers: []headers.Header{ | ||||
| 			false, []Rule{ | ||||
| 				{Path: "/bar", Headers: []Header{ | ||||
| 					{Name: "Foo", Value: "Bar Baz"}, | ||||
| 					{Name: "Baz", Value: "Qux"}, | ||||
| 				}}, | ||||
| @ -51,8 +52,7 @@ func TestHeadersParse(t *testing.T) { | ||||
| 	} | ||||
| 
 | ||||
| 	for i, test := range tests { | ||||
| 		c := NewTestController(test.input) | ||||
| 		actual, err := headersParse(c) | ||||
| 		actual, err := headersParse(caddy.NewTestController(test.input)) | ||||
| 
 | ||||
| 		if err == nil && test.shouldErr { | ||||
| 			t.Errorf("Test %d didn't error, but it should have", i) | ||||
| @ -1,4 +1,4 @@ | ||||
| package middleware | ||||
| package httpserver | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user