mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-26 00:02:45 -04:00 
			
		
		
		
	core: Major refactor for graceful restarts; numerous fixes
Merged config and app packages into one called caddy. Abstracted away caddy startup functionality making it easier to embed Caddy in any Go application and use it as a library. Graceful restart (should) now ensure child starts properly. Now piping a gob bundle to child process so that the child can match up inherited listeners to server address. Much cleanup still to do.
This commit is contained in:
		
							parent
							
								
									6936658019
								
							
						
					
					
						commit
						4ebff9a130
					
				
							
								
								
									
										172
									
								
								app/app.go
									
									
									
									
									
								
							
							
						
						
									
										172
									
								
								app/app.go
									
									
									
									
									
								
							| @ -1,172 +0,0 @@ | |||||||
| // Package app holds application-global state to make it accessible |  | ||||||
| // by other packages in the application. |  | ||||||
| // |  | ||||||
| // This package differs from config in that the things in app aren't |  | ||||||
| // really related to server configuration. |  | ||||||
| package app |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 	"os/signal" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"sync" |  | ||||||
| 	"syscall" |  | ||||||
| 
 |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// Name is the program name |  | ||||||
| 	Name = "Caddy" |  | ||||||
| 
 |  | ||||||
| 	// Version is the program version |  | ||||||
| 	Version = "0.7.6" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	// Servers is a list of all the currently-listening servers |  | ||||||
| 	Servers []*server.Server |  | ||||||
| 
 |  | ||||||
| 	// ServersMutex protects the Servers slice during changes |  | ||||||
| 	ServersMutex sync.Mutex |  | ||||||
| 
 |  | ||||||
| 	// Wg is used to wait for all servers to shut down |  | ||||||
| 	Wg sync.WaitGroup |  | ||||||
| 
 |  | ||||||
| 	// HTTP2 indicates whether HTTP2 is enabled or not |  | ||||||
| 	HTTP2 bool // TODO: temporary flag until http2 is standard |  | ||||||
| 
 |  | ||||||
| 	// Quiet mode hides non-error initialization output |  | ||||||
| 	Quiet bool |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	go func() { |  | ||||||
| 		// Wait for signal |  | ||||||
| 		interrupt := make(chan os.Signal, 1) |  | ||||||
| 		signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGTERM? Or that should not run callbacks... |  | ||||||
| 		<-interrupt |  | ||||||
| 
 |  | ||||||
| 		// Run shutdown callbacks |  | ||||||
| 		var exitCode int |  | ||||||
| 		ServersMutex.Lock() |  | ||||||
| 		errs := server.ShutdownCallbacks(Servers) |  | ||||||
| 		ServersMutex.Unlock() |  | ||||||
| 		if len(errs) > 0 { |  | ||||||
| 			for _, err := range errs { |  | ||||||
| 				log.Println(err) |  | ||||||
| 			} |  | ||||||
| 			exitCode = 1 |  | ||||||
| 		} |  | ||||||
| 		os.Exit(exitCode) |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 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 restarted application will use caddyfile as its input |  | ||||||
| // configuration; it will not look elsewhere for the config |  | ||||||
| // to use. |  | ||||||
| func Restart(caddyfile []byte) error { |  | ||||||
| 	// TODO: This is POSIX-only right now; also, os.Args[0] is required! |  | ||||||
| 	// TODO: Pipe the Caddyfile to stdin of child! |  | ||||||
| 	// TODO: Before stopping this process, verify child started successfully (valid Caddyfile, etc) |  | ||||||
| 
 |  | ||||||
| 	// Tell the child that it's a restart |  | ||||||
| 	os.Setenv("CADDY_RESTART", "true") |  | ||||||
| 
 |  | ||||||
| 	// Pass along current environment and file descriptors to child. |  | ||||||
| 	// We pass along the file descriptors explicitly to ensure proper |  | ||||||
| 	// order, since losing the original order will break the child. |  | ||||||
| 	fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} |  | ||||||
| 
 |  | ||||||
| 	// Now add file descriptors of the sockets |  | ||||||
| 	ServersMutex.Lock() |  | ||||||
| 	for _, s := range Servers { |  | ||||||
| 		fds = append(fds, s.ListenerFd()) |  | ||||||
| 	} |  | ||||||
| 	ServersMutex.Unlock() |  | ||||||
| 
 |  | ||||||
| 	// Fork the process with the current environment and file descriptors |  | ||||||
| 	execSpec := &syscall.ProcAttr{ |  | ||||||
| 		Env:   os.Environ(), |  | ||||||
| 		Files: fds, |  | ||||||
| 	} |  | ||||||
| 	fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Println("FORK ERR:", err, fork) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Child process is listening now; we can stop all our servers here. |  | ||||||
| 	ServersMutex.Lock() |  | ||||||
| 	for _, s := range Servers { |  | ||||||
| 		go s.Stop() // TODO: error checking/reporting |  | ||||||
| 	} |  | ||||||
| 	ServersMutex.Unlock() |  | ||||||
| 
 |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 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%). |  | ||||||
| func SetCPU(cpu string) error { |  | ||||||
| 	var numCPU int |  | ||||||
| 
 |  | ||||||
| 	availCPU := runtime.NumCPU() |  | ||||||
| 
 |  | ||||||
| 	if strings.HasSuffix(cpu, "%") { |  | ||||||
| 		// Percent |  | ||||||
| 		var percent float32 |  | ||||||
| 		pctStr := cpu[:len(cpu)-1] |  | ||||||
| 		pctInt, err := strconv.Atoi(pctStr) |  | ||||||
| 		if err != nil || pctInt < 1 || pctInt > 100 { |  | ||||||
| 			return errors.New("invalid CPU value: percentage must be between 1-100") |  | ||||||
| 		} |  | ||||||
| 		percent = float32(pctInt) / 100 |  | ||||||
| 		numCPU = int(float32(availCPU) * percent) |  | ||||||
| 	} else { |  | ||||||
| 		// Number |  | ||||||
| 		num, err := strconv.Atoi(cpu) |  | ||||||
| 		if err != nil || num < 1 { |  | ||||||
| 			return errors.New("invalid CPU value: provide a number or percent greater than 0") |  | ||||||
| 		} |  | ||||||
| 		numCPU = num |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if numCPU > availCPU { |  | ||||||
| 		numCPU = availCPU |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	runtime.GOMAXPROCS(numCPU) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // DataFolder returns the path to the folder |  | ||||||
| // where the application may store data. This |  | ||||||
| // currently resolves to ~/.caddy |  | ||||||
| func DataFolder() string { |  | ||||||
| 	return filepath.Join(userHomeDir(), ".caddy") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // userHomeDir returns the user's home directory according to |  | ||||||
| // environment variables. |  | ||||||
| // |  | ||||||
| // Credit: http://stackoverflow.com/a/7922977/1048862 |  | ||||||
| func userHomeDir() string { |  | ||||||
| 	if runtime.GOOS == "windows" { |  | ||||||
| 		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") |  | ||||||
| 		if home == "" { |  | ||||||
| 			home = os.Getenv("USERPROFILE") |  | ||||||
| 		} |  | ||||||
| 		return home |  | ||||||
| 	} |  | ||||||
| 	return os.Getenv("HOME") |  | ||||||
| } |  | ||||||
							
								
								
									
										29
									
								
								caddy/assets/path.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								caddy/assets/path.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | package assets | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"runtime" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Path returns the path to the folder | ||||||
|  | // where the application may store data. This | ||||||
|  | // currently resolves to ~/.caddy | ||||||
|  | func Path() string { | ||||||
|  | 	return filepath.Join(userHomeDir(), ".caddy") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // userHomeDir returns the user's home directory according to | ||||||
|  | // environment variables. | ||||||
|  | // | ||||||
|  | // Credit: http://stackoverflow.com/a/7922977/1048862 | ||||||
|  | func userHomeDir() string { | ||||||
|  | 	if runtime.GOOS == "windows" { | ||||||
|  | 		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") | ||||||
|  | 		if home == "" { | ||||||
|  | 			home = os.Getenv("USERPROFILE") | ||||||
|  | 		} | ||||||
|  | 		return home | ||||||
|  | 	} | ||||||
|  | 	return os.Getenv("HOME") | ||||||
|  | } | ||||||
							
								
								
									
										470
									
								
								caddy/caddy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										470
									
								
								caddy/caddy.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,470 @@ | |||||||
|  | package caddy | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/gob" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"os/signal" | ||||||
|  | 	"path" | ||||||
|  | 	"runtime" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"syscall" | ||||||
|  | 
 | ||||||
|  | 	"github.com/mholt/caddy/caddy/letsencrypt" | ||||||
|  | 	"github.com/mholt/caddy/server" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Configurable application parameters | ||||||
|  | var ( | ||||||
|  | 	// The name and version of the application. | ||||||
|  | 	AppName, AppVersion string | ||||||
|  | 
 | ||||||
|  | 	// If true, initialization will not show any output. | ||||||
|  | 	Quiet bool | ||||||
|  | 
 | ||||||
|  | 	// DefaultInput is the default configuration to use when config input is empty or missing. | ||||||
|  | 	DefaultInput = CaddyfileInput{ | ||||||
|  | 		Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", DefaultHost, DefaultPort, DefaultRoot)), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// HTTP2 indicates whether HTTP2 is enabled or not | ||||||
|  | 	HTTP2 bool // TODO: temporary flag until http2 is standard | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	// caddyfile is the input configuration text used for this process | ||||||
|  | 	caddyfile Input | ||||||
|  | 
 | ||||||
|  | 	// caddyfileMu protects caddyfile during changes | ||||||
|  | 	caddyfileMu sync.Mutex | ||||||
|  | 
 | ||||||
|  | 	// incompleteRestartErr occurs if this process is a fork | ||||||
|  | 	// of the parent but no Caddyfile was piped in | ||||||
|  | 	incompleteRestartErr = errors.New("cannot finish restart successfully") | ||||||
|  | 
 | ||||||
|  | 	// 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 | ||||||
|  | 
 | ||||||
|  | 	// loadedGob is used if this is a child process as part of | ||||||
|  | 	// a graceful restart; it is used to map listeners to their | ||||||
|  | 	// index in the list of inherited file descriptors. This | ||||||
|  | 	// variable is not safe for concurrent access. | ||||||
|  | 	loadedGob caddyfileGob | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	DefaultHost = "0.0.0.0" | ||||||
|  | 	DefaultPort = "2015" | ||||||
|  | 	DefaultRoot = "." | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // caddyfileGob maps bind address to index of the file descriptor | ||||||
|  | // in the Files array passed to the child process. It also contains | ||||||
|  | // the caddyfile contents. | ||||||
|  | type caddyfileGob struct { | ||||||
|  | 	ListenerFds map[string]uintptr | ||||||
|  | 	Caddyfile   []byte | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Start starts Caddy with the given Caddyfile. If cdyfile | ||||||
|  | // is nil or the process is forked from a parent as part of | ||||||
|  | // a graceful restart, Caddy will check to see if Caddyfile | ||||||
|  | // was piped from stdin and use that. | ||||||
|  | // | ||||||
|  | // If this process is a fork and no Caddyfile was piped in, | ||||||
|  | // an error will be returned. If this process is NOT a fork | ||||||
|  | // and cdyfile is nil, a default configuration will be assumed. | ||||||
|  | // In any case, an error is returned if Caddy could not be | ||||||
|  | // started. | ||||||
|  | func Start(cdyfile Input) error { | ||||||
|  | 	var 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() | ||||||
|  | 
 | ||||||
|  | 	groupings, err := Load(path.Base(caddyfile.Path()), bytes.NewReader(caddyfile.Body())) | ||||||
|  | 
 | ||||||
|  | 	// Start each server with its one or more configurations | ||||||
|  | 	for i, group := range groupings { | ||||||
|  | 		s, err := server.New(group.BindAddr.String(), group.Configs) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		s.HTTP2 = HTTP2 // TODO: This setting is temporary | ||||||
|  | 
 | ||||||
|  | 		var ln server.ListenerFile | ||||||
|  | 		if isRestart() { | ||||||
|  | 			// Look up this server's listener in the map of inherited file descriptors; | ||||||
|  | 			// if we don't have one, we must make a new one. | ||||||
|  | 			if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok { | ||||||
|  | 				file := os.NewFile(fdIndex, "") | ||||||
|  | 
 | ||||||
|  | 				fln, err := net.FileListener(file) | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Fatal("FILE LISTENER:", err) | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				ln, ok = fln.(server.ListenerFile) | ||||||
|  | 				if !ok { | ||||||
|  | 					log.Fatal("Listener was not a ListenerFile") | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				delete(loadedGob.ListenerFds, s.Addr) // mark it as used | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		wg.Add(1) | ||||||
|  | 		go func(s *server.Server, i int, ln server.ListenerFile) { | ||||||
|  | 			defer wg.Done() | ||||||
|  | 			if ln == nil { | ||||||
|  | 				err := s.ListenAndServe() | ||||||
|  | 				// "use of closed network connection" is normal if doing graceful shutdown... | ||||||
|  | 				if !strings.Contains(err.Error(), "use of closed network connection") { | ||||||
|  | 					// But an error at initial startup must be fatal | ||||||
|  | 					log.Fatal(err) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				err := s.Serve(ln) | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Println(err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}(s, i, ln) | ||||||
|  | 
 | ||||||
|  | 		serversMu.Lock() | ||||||
|  | 		servers = append(servers, s) | ||||||
|  | 		serversMu.Unlock() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Close remaining file descriptors we may have inherited that we don't need | ||||||
|  | 	if isRestart() { | ||||||
|  | 		for _, fdIndex := range loadedGob.ListenerFds { | ||||||
|  | 			file := os.NewFile(fdIndex, "") | ||||||
|  | 			fln, err := net.FileListener(file) | ||||||
|  | 			if err == nil { | ||||||
|  | 				fln.Close() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 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 | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Tell parent we're A-OK | ||||||
|  | 	if isRestart() { | ||||||
|  | 		file := os.NewFile(3, "") | ||||||
|  | 		file.Write([]byte("success")) | ||||||
|  | 		file.Close() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // isLocalhost returns true if the string looks explicitly like a localhost address. | ||||||
|  | func isLocalhost(s string) bool { | ||||||
|  | 	return s == "localhost" || s == "::1" || strings.HasPrefix(s, "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) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func Stop() error { | ||||||
|  | 	serversMu.Lock() | ||||||
|  | 	for _, s := range servers { | ||||||
|  | 		s.Stop() // TODO: error checking/reporting? | ||||||
|  | 	} | ||||||
|  | 	serversMu.Unlock() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 restarted application will use newCaddyfile as its input | ||||||
|  | // configuration. If newCaddyfile is nil, the current (existing) | ||||||
|  | // Caddyfile configuration will be used. | ||||||
|  | func Restart(newCaddyfile Input) error { | ||||||
|  | 	if newCaddyfile == nil { | ||||||
|  | 		caddyfileMu.Lock() | ||||||
|  | 		newCaddyfile = caddyfile | ||||||
|  | 		caddyfileMu.Unlock() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if runtime.GOOS == "windows" { | ||||||
|  | 		err := Stop() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		err = Start(newCaddyfile) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(os.Args) == 0 { // this should never happen, but just in case... | ||||||
|  | 		os.Args = []string{""} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Tell the child that it's a restart | ||||||
|  | 	os.Setenv("CADDY_RESTART", "true") | ||||||
|  | 
 | ||||||
|  | 	// Prepare our payload to the child process | ||||||
|  | 	cdyfileGob := caddyfileGob{ | ||||||
|  | 		ListenerFds: make(map[string]uintptr), | ||||||
|  | 		Caddyfile:   newCaddyfile.Body(), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Prepare a pipe to the fork's stdin so it can get the Caddyfile | ||||||
|  | 	rpipe, wpipe, err := os.Pipe() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Prepare a pipe that the child process will use to communicate | ||||||
|  | 	// its success or failure with us, the parent | ||||||
|  | 	sigrpipe, sigwpipe, err := os.Pipe() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Pass along current environment and file descriptors to child. | ||||||
|  | 	// Ordering here is very important: stdin, stdout, stderr, sigpipe, | ||||||
|  | 	// and then the listener file descriptors (in order). | ||||||
|  | 	fds := []uintptr{rpipe.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), sigwpipe.Fd()} | ||||||
|  | 
 | ||||||
|  | 	// Now add file descriptors of the sockets | ||||||
|  | 	serversMu.Lock() | ||||||
|  | 	for i, s := range servers { | ||||||
|  | 		fds = append(fds, s.ListenerFd()) | ||||||
|  | 		cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners | ||||||
|  | 	} | ||||||
|  | 	serversMu.Unlock() | ||||||
|  | 
 | ||||||
|  | 	// Fork the process with the current environment and file descriptors | ||||||
|  | 	execSpec := &syscall.ProcAttr{ | ||||||
|  | 		Env:   os.Environ(), | ||||||
|  | 		Files: fds, | ||||||
|  | 	} | ||||||
|  | 	pid, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Println("FORK ERR:", err, pid) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Feed it the Caddyfile | ||||||
|  | 	err = gob.NewEncoder(wpipe).Encode(cdyfileGob) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	wpipe.Close() | ||||||
|  | 
 | ||||||
|  | 	// Wait for child process to signal success or fail | ||||||
|  | 	sigwpipe.Close() // close our copy of the write end of the pipe | ||||||
|  | 	answer, err := ioutil.ReadAll(sigrpipe) | ||||||
|  | 	if err != nil || len(answer) == 0 { | ||||||
|  | 		log.Println("restart: child failed to answer; changes not applied") | ||||||
|  | 		return incompleteRestartErr | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Child process is listening now; we can stop all our servers here. | ||||||
|  | 	return Stop() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Wait blocks until all servers are stopped. | ||||||
|  | func Wait() { | ||||||
|  | 	wg.Wait() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LoadCaddyfile loads a Caddyfile in a way that prioritizes | ||||||
|  | // reading from stdin pipe; otherwise it calls loader to load | ||||||
|  | // the Caddyfile. If loader does not return a Caddyfile, the | ||||||
|  | // default one will be returned. 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) { | ||||||
|  | 	// If we are a fork, finishing the restart is highest priority; | ||||||
|  | 	// piped input is required in this case. | ||||||
|  | 	if isRestart() { | ||||||
|  | 		err := gob.NewDecoder(os.Stdin).Decode(&loadedGob) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		cdyfile = CaddyfileInput{ | ||||||
|  | 			Filepath: os.Stdin.Name(), | ||||||
|  | 			Contents: loadedGob.Caddyfile, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Otherwise, we first try to get from stdin pipe | ||||||
|  | 	if cdyfile == nil { | ||||||
|  | 		cdyfile, err = CaddyfileFromPipe(os.Stdin) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// No piped input, so try the user's loader instead | ||||||
|  | 	if cdyfile == nil && loader != nil { | ||||||
|  | 		cdyfile, err = loader() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Otherwise revert to default | ||||||
|  | 	if cdyfile == nil { | ||||||
|  | 		cdyfile = DefaultInput | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Caddyfile returns the current Caddyfile | ||||||
|  | func Caddyfile() Input { | ||||||
|  | 	caddyfileMu.Lock() | ||||||
|  | 	defer caddyfileMu.Unlock() | ||||||
|  | 	return caddyfile | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // isRestart returns whether this process is, according | ||||||
|  | // to env variables, a fork as part of a graceful restart. | ||||||
|  | func isRestart() bool { | ||||||
|  | 	return os.Getenv("CADDY_RESTART") == "true" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CaddyfileInput represents a Caddyfile as input | ||||||
|  | // and is simply a convenient way to implement | ||||||
|  | // the Input interface. | ||||||
|  | type CaddyfileInput struct { | ||||||
|  | 	Filepath string | ||||||
|  | 	Contents []byte | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Body returns c.Contents. | ||||||
|  | func (c CaddyfileInput) Body() []byte { return c.Contents } | ||||||
|  | 
 | ||||||
|  | // Path returns c.Filepath. | ||||||
|  | func (c CaddyfileInput) Path() string { return c.Filepath } | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	letsencrypt.OnRenew = func() error { return Restart(nil) } | ||||||
|  | 
 | ||||||
|  | 	// Trap signals | ||||||
|  | 	go func() { | ||||||
|  | 		// Wait for signal | ||||||
|  | 		interrupt := make(chan os.Signal, 1) | ||||||
|  | 		signal.Notify(interrupt, os.Interrupt, os.Kill) | ||||||
|  | 		<-interrupt | ||||||
|  | 
 | ||||||
|  | 		// TODO: A signal just for graceful restart (reload config) - maybe SIGUSR1 | ||||||
|  | 
 | ||||||
|  | 		// Run shutdown callbacks | ||||||
|  | 		var exitCode int | ||||||
|  | 		serversMu.Lock() | ||||||
|  | 		errs := server.ShutdownCallbacks(servers) | ||||||
|  | 		serversMu.Unlock() | ||||||
|  | 		if len(errs) > 0 { | ||||||
|  | 			for _, err := range errs { | ||||||
|  | 				log.Println(err) | ||||||
|  | 			} | ||||||
|  | 			exitCode = 1 | ||||||
|  | 		} | ||||||
|  | 		os.Exit(exitCode) | ||||||
|  | 	}() | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package config | package caddy | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @ -7,19 +7,14 @@ import ( | |||||||
| 	"net" | 	"net" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/app" | 	"github.com/mholt/caddy/caddy/letsencrypt" | ||||||
| 	"github.com/mholt/caddy/config/letsencrypt" | 	"github.com/mholt/caddy/caddy/parse" | ||||||
| 	"github.com/mholt/caddy/config/parse" | 	"github.com/mholt/caddy/caddy/setup" | ||||||
| 	"github.com/mholt/caddy/config/setup" |  | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/middleware" | ||||||
| 	"github.com/mholt/caddy/server" | 	"github.com/mholt/caddy/server" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	DefaultHost = "0.0.0.0" |  | ||||||
| 	DefaultPort = "2015" |  | ||||||
| 	DefaultRoot = "." |  | ||||||
| 
 |  | ||||||
| 	// DefaultConfigFile is the name of the configuration file that is loaded | 	// DefaultConfigFile is the name of the configuration file that is loaded | ||||||
| 	// by default if no other file is specified. | 	// by default if no other file is specified. | ||||||
| 	DefaultConfigFile = "Caddyfile" | 	DefaultConfigFile = "Caddyfile" | ||||||
| @ -56,8 +51,8 @@ func Load(filename string, input io.Reader) (Group, error) { | |||||||
| 				Root:       Root, | 				Root:       Root, | ||||||
| 				Middleware: make(map[string][]middleware.Middleware), | 				Middleware: make(map[string][]middleware.Middleware), | ||||||
| 				ConfigFile: filename, | 				ConfigFile: filename, | ||||||
| 				AppName:    app.Name, | 				AppName:    AppName, | ||||||
| 				AppVersion: app.Version, | 				AppVersion: AppVersion, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// It is crucial that directives are executed in the proper order. | 			// It is crucial that directives are executed in the proper order. | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package config | package caddy | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| @ -1,8 +1,8 @@ | |||||||
| package config | package caddy | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/mholt/caddy/config/parse" | 	"github.com/mholt/caddy/caddy/parse" | ||||||
| 	"github.com/mholt/caddy/config/setup" | 	"github.com/mholt/caddy/caddy/setup" | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/middleware" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -18,6 +18,12 @@ import ( | |||||||
| 	"github.com/xenolf/lego/acme" | 	"github.com/xenolf/lego/acme" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // OnRenew is the function that will be used to restart | ||||||
|  | // the application or the part of the application that uses | ||||||
|  | // the certificates maintained by this package. When at least | ||||||
|  | // one certificate is renewed, this function will be called. | ||||||
|  | var OnRenew func() error | ||||||
|  | 
 | ||||||
| // Activate sets up TLS for each server config in configs | // Activate sets up TLS for each server config in configs | ||||||
| // as needed. It only skips the config if the cert and key | // as needed. It only skips the config if the cert and key | ||||||
| // are already provided or if plaintext http is explicitly | // are already provided or if plaintext http is explicitly | ||||||
| @ -17,10 +17,16 @@ import ( | |||||||
| func keepCertificatesRenewed(configs []server.Config) { | func keepCertificatesRenewed(configs []server.Config) { | ||||||
| 	ticker := time.Tick(renewInterval) | 	ticker := time.Tick(renewInterval) | ||||||
| 	for range ticker { | 	for range ticker { | ||||||
| 		if errs := processCertificateRenewal(configs); len(errs) > 0 { | 		if n, errs := processCertificateRenewal(configs); len(errs) > 0 { | ||||||
| 			for _, err := range errs { | 			for _, err := range errs { | ||||||
| 				log.Printf("[ERROR] cert renewal: %v\n", err) | 				log.Printf("[ERROR] cert renewal: %v\n", err) | ||||||
| 			} | 			} | ||||||
|  | 			if n > 0 && OnRenew != nil { | ||||||
|  | 				err := OnRenew() | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Printf("[ERROR] onrenew callback: %v\n", err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -28,9 +34,11 @@ func keepCertificatesRenewed(configs []server.Config) { | |||||||
| // checkCertificateRenewal loops through all configured | // checkCertificateRenewal loops through all configured | ||||||
| // sites and looks for certificates to renew. Nothing is mutated | // sites and looks for certificates to renew. Nothing is mutated | ||||||
| // through this function. The changes happen directly on disk. | // through this function. The changes happen directly on disk. | ||||||
| func processCertificateRenewal(configs []server.Config) []error { | // It returns the number of certificates renewed and | ||||||
| 	var errs []error | func processCertificateRenewal(configs []server.Config) (int, []error) { | ||||||
| 	log.Print("[INFO] Processing certificate renewals...") | 	log.Print("[INFO] Processing certificate renewals...") | ||||||
|  | 	var errs []error | ||||||
|  | 	var n int | ||||||
| 
 | 
 | ||||||
| 	for _, cfg := range configs { | 	for _, cfg := range configs { | ||||||
| 		// Host must be TLS-enabled and have assets managed by LE | 		// Host must be TLS-enabled and have assets managed by LE | ||||||
| @ -95,11 +103,12 @@ func processCertificateRenewal(configs []server.Config) []error { | |||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) | 			saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) | ||||||
|  | 			n++ | ||||||
| 		} else if daysLeft <= 14 { | 		} else if daysLeft <= 14 { | ||||||
| 			// Warn on 14 days remaining | 			// Warn on 14 days remaining | ||||||
| 			log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 7 days remain.\n", daysLeft, cfg.Host) | 			log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 7 days remain.\n", daysLeft, cfg.Host) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return errs | 	return n, errs | ||||||
| } | } | ||||||
| @ -4,13 +4,13 @@ import ( | |||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/app" | 	"github.com/mholt/caddy/caddy/assets" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // storage is used to get file paths in a consistent, | // storage is used to get file paths in a consistent, | ||||||
| // cross-platform way for persisting Let's Encrypt assets | // cross-platform way for persisting Let's Encrypt assets | ||||||
| // on the file system. | // on the file system. | ||||||
| var storage = Storage(filepath.Join(app.DataFolder(), "letsencrypt")) | var storage = Storage(filepath.Join(assets.Path(), "letsencrypt")) | ||||||
| 
 | 
 | ||||||
| // Storage is a root directory and facilitates | // Storage is a root directory and facilitates | ||||||
| // forming file paths derived from it. | // forming file paths derived from it. | ||||||
| @ -5,7 +5,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/config/parse" | 	"github.com/mholt/caddy/caddy/parse" | ||||||
| 	"github.com/mholt/caddy/middleware" | 	"github.com/mholt/caddy/middleware" | ||||||
| 	"github.com/mholt/caddy/server" | 	"github.com/mholt/caddy/server" | ||||||
| ) | ) | ||||||
							
								
								
									
										225
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										225
									
								
								main.go
									
									
									
									
									
								
							| @ -1,24 +1,19 @@ | |||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"errors" | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"log" | 	"log" | ||||||
| 	"net" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" |  | ||||||
| 	"path" |  | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/app" | 	"github.com/mholt/caddy/caddy" | ||||||
| 	"github.com/mholt/caddy/config" | 	"github.com/mholt/caddy/caddy/letsencrypt" | ||||||
| 	"github.com/mholt/caddy/config/letsencrypt" |  | ||||||
| 	"github.com/mholt/caddy/server" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| @ -28,25 +23,33 @@ var ( | |||||||
| 	revoke  string | 	revoke  string | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	appName    = "Caddy" | ||||||
|  | 	appVersion = "0.8 beta" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| func init() { | func init() { | ||||||
| 	flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")") | 	flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") | ||||||
| 	flag.BoolVar(&app.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib | 	flag.BoolVar(&caddy.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib | ||||||
| 	flag.BoolVar(&app.Quiet, "quiet", false, "Quiet mode (no initialization output)") | 	flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") | ||||||
| 	flag.StringVar(&cpu, "cpu", "100%", "CPU cap") | 	flag.StringVar(&cpu, "cpu", "100%", "CPU cap") | ||||||
| 	flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site") | 	flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site") | ||||||
| 	flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host") | 	flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host") | ||||||
| 	flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port") | 	flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port") | ||||||
| 	flag.BoolVar(&version, "version", false, "Show version") | 	flag.BoolVar(&version, "version", false, "Show version") | ||||||
| 	flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") | 	flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") | ||||||
| 	flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions") | 	flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions") | ||||||
| 	flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") | 	flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke its certificate") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
|  | 	caddy.AppName = appName | ||||||
|  | 	caddy.AppVersion = appVersion | ||||||
|  | 
 | ||||||
| 	if version { | 	if version { | ||||||
| 		fmt.Printf("%s %s\n", app.Name, app.Version) | 		fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion) | ||||||
| 		os.Exit(0) | 		os.Exit(0) | ||||||
| 	} | 	} | ||||||
| 	if revoke != "" { | 	if revoke != "" { | ||||||
| @ -59,165 +62,103 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Set CPU cap | 	// Set CPU cap | ||||||
| 	err := app.SetCPU(cpu) | 	err := setCPU(cpu) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Load config from file | 	// Get Caddyfile input | ||||||
| 	groupings, err := loadConfigs() | 	caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Start each server with its one or more configurations | 	// Start your engines | ||||||
| 	for i, group := range groupings { | 	err = caddy.Start(caddyfile) | ||||||
| 		s, err := server.New(group.BindAddr.String(), group.Configs) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 		s.HTTP2 = app.HTTP2 // TODO: This setting is temporary |  | ||||||
| 
 |  | ||||||
| 		app.Wg.Add(1) |  | ||||||
| 		go func(s *server.Server, i int) { |  | ||||||
| 			defer app.Wg.Done() |  | ||||||
| 
 |  | ||||||
| 			if os.Getenv("CADDY_RESTART") == "true" { |  | ||||||
| 				file := os.NewFile(uintptr(3+i), "") |  | ||||||
| 				ln, err := net.FileListener(file) |  | ||||||
| 				if err != nil { |  | ||||||
| 					log.Fatal("FILE LISTENER:", err) |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				lnf, ok := ln.(server.ListenerFile) |  | ||||||
| 				if !ok { |  | ||||||
| 					log.Fatal("Listener was not a ListenerFile") |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				err = s.Serve(lnf) |  | ||||||
| 				// TODO: Better error logging... also, is it even necessary? |  | ||||||
| 				if err != nil { |  | ||||||
| 					log.Println(err) |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				err := s.ListenAndServe() |  | ||||||
| 				// TODO: Better error logging... also, is it even necessary? |  | ||||||
| 				// For example, "use of closed network connection" is normal if doing graceful shutdown... |  | ||||||
| 				if err != nil { |  | ||||||
| 					log.Println(err) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}(s, i) |  | ||||||
| 
 |  | ||||||
| 		app.ServersMutex.Lock() |  | ||||||
| 		app.Servers = append(app.Servers, s) |  | ||||||
| 		app.ServersMutex.Unlock() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Show initialization output |  | ||||||
| 	if !app.Quiet { |  | ||||||
| 		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 |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// TODO: Temporary; testing restart | 	// TODO: Temporary; testing restart | ||||||
| 	if os.Getenv("CADDY_RESTART") != "true" { | 	//if os.Getenv("CADDY_RESTART") != "true" { | ||||||
| 	go func() { | 	go func() { | ||||||
| 		time.Sleep(5 * time.Second) | 		time.Sleep(5 * time.Second) | ||||||
| 		fmt.Println("restarting") | 		fmt.Println("restarting") | ||||||
| 			log.Println("RESTART ERR:", app.Restart([]byte{})) | 		log.Println("RESTART ERR:", caddy.Restart(nil)) | ||||||
| 	}() | 	}() | ||||||
| 	} | 	//} | ||||||
| 
 | 
 | ||||||
| 	// Wait for all servers to be stopped | 	// Twiddle your thumbs | ||||||
| 	app.Wg.Wait() | 	caddy.Wait() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. | func loadCaddyfile() (caddy.Input, error) { | ||||||
| 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) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // isLocalhost returns true if the string looks explicitly like a localhost address. |  | ||||||
| func isLocalhost(s string) bool { |  | ||||||
| 	return s == "localhost" || s == "::1" || strings.HasPrefix(s, "127.") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // loadConfigs loads configuration from a file or stdin (piped). |  | ||||||
| // The configurations are grouped by bind address. |  | ||||||
| // Configuration is obtained from one of four sources, tried |  | ||||||
| // in this order: 1. -conf flag, 2. stdin, 3. command line argument 4. Caddyfile. |  | ||||||
| // If none of those are available, a default configuration is loaded. |  | ||||||
| func loadConfigs() (config.Group, error) { |  | ||||||
| 	// -conf flag | 	// -conf flag | ||||||
| 	if conf != "" { | 	if conf != "" { | ||||||
| 		file, err := os.Open(conf) | 		contents, err := ioutil.ReadFile(conf) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		defer file.Close() | 		return caddy.CaddyfileInput{ | ||||||
| 		return config.Load(path.Base(conf), file) | 			Contents: contents, | ||||||
|  | 			Filepath: conf, | ||||||
|  | 		}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// stdin | 	// command line args | ||||||
| 	fi, err := os.Stdin.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. |  | ||||||
| 		confBody, err := ioutil.ReadAll(os.Stdin) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Fatal(err) |  | ||||||
| 		} |  | ||||||
| 		if len(confBody) > 0 { |  | ||||||
| 			return config.Load("stdin", bytes.NewReader(confBody)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Command line args |  | ||||||
| 	if flag.NArg() > 0 { | 	if flag.NArg() > 0 { | ||||||
| 		confBody := ":" + config.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") | 		confBody := ":" + caddy.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") | ||||||
| 		return config.Load("args", bytes.NewBufferString(confBody)) | 		return caddy.CaddyfileInput{ | ||||||
|  | 			Contents: []byte(confBody), | ||||||
|  | 			Filepath: "args", | ||||||
|  | 		}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Caddyfile | 	// Caddyfile in cwd | ||||||
| 	file, err := os.Open(config.DefaultConfigFile) | 	contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if os.IsNotExist(err) { | 		if os.IsNotExist(err) { | ||||||
| 			return config.Default() | 			return caddy.DefaultInput, nil | ||||||
| 		} | 		} | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	defer file.Close() | 	return caddy.CaddyfileInput{ | ||||||
| 
 | 		Contents: contents, | ||||||
| 	return config.Load(config.DefaultConfigFile, file) | 		Filepath: caddy.DefaultConfigFile, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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%). | ||||||
|  | func setCPU(cpu string) error { | ||||||
|  | 	var numCPU int | ||||||
|  | 
 | ||||||
|  | 	availCPU := runtime.NumCPU() | ||||||
|  | 
 | ||||||
|  | 	if strings.HasSuffix(cpu, "%") { | ||||||
|  | 		// Percent | ||||||
|  | 		var percent float32 | ||||||
|  | 		pctStr := cpu[:len(cpu)-1] | ||||||
|  | 		pctInt, err := strconv.Atoi(pctStr) | ||||||
|  | 		if err != nil || pctInt < 1 || pctInt > 100 { | ||||||
|  | 			return errors.New("invalid CPU value: percentage must be between 1-100") | ||||||
|  | 		} | ||||||
|  | 		percent = float32(pctInt) / 100 | ||||||
|  | 		numCPU = int(float32(availCPU) * percent) | ||||||
|  | 	} else { | ||||||
|  | 		// Number | ||||||
|  | 		num, err := strconv.Atoi(cpu) | ||||||
|  | 		if err != nil || num < 1 { | ||||||
|  | 			return errors.New("invalid CPU value: provide a number or percent greater than 0") | ||||||
|  | 		} | ||||||
|  | 		numCPU = num | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if numCPU > availCPU { | ||||||
|  | 		numCPU = availCPU | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	runtime.GOMAXPROCS(numCPU) | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/mholt/caddy/config/parse" | 	"github.com/mholt/caddy/caddy/parse" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
|  | |||||||
| @ -65,6 +65,12 @@ type gracefulConn struct { | |||||||
| 
 | 
 | ||||||
| // Close closes c's underlying connection while updating the wg count. | // Close closes c's underlying connection while updating the wg count. | ||||||
| func (c gracefulConn) Close() error { | func (c gracefulConn) Close() error { | ||||||
|  | 	err := c.Conn.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	// close can fail on http2 connections (as of Oct. 2015, before http2 in std lib) | ||||||
|  | 	// so don't decrement count unless close succeeds | ||||||
| 	c.httpWg.Done() | 	c.httpWg.Done() | ||||||
| 	return c.Conn.Close() | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -59,14 +59,13 @@ func New(addr string, configs []Config) (*Server, error) { | |||||||
| 		tls:    tls, | 		tls:    tls, | ||||||
| 		vhosts: make(map[string]virtualHost), | 		vhosts: make(map[string]virtualHost), | ||||||
| 	} | 	} | ||||||
| 	s.Handler = s // TODO: this is weird | 	s.Handler = s // this is weird, but whatever | ||||||
| 
 | 
 | ||||||
| 	// We have to bound our wg with one increment | 	// We have to bound our wg with one increment | ||||||
| 	// to prevent a "race condition" that is hard-coded | 	// to prevent a "race condition" that is hard-coded | ||||||
| 	// into sync.WaitGroup.Wait() - basically, an add | 	// into sync.WaitGroup.Wait() - basically, an add | ||||||
| 	// with a positive delta must be guaranteed to | 	// with a positive delta must be guaranteed to | ||||||
| 	// occur before Wait() is called on the wg. | 	// occur before Wait() is called on the wg. | ||||||
| 	fmt.Println("+1 (new)") |  | ||||||
| 	s.httpWg.Add(1) | 	s.httpWg.Add(1) | ||||||
| 
 | 
 | ||||||
| 	// Set up each virtualhost | 	// Set up each virtualhost | ||||||
| @ -169,11 +168,6 @@ func (s *Server) setup() error { | |||||||
| // by the Go Authors. It has been modified to support multiple certificate/key pairs, | // by the Go Authors. It has been modified to support multiple certificate/key pairs, | ||||||
| // client authentication, and our custom Server type. | // client authentication, and our custom Server type. | ||||||
| func serveTLSWithSNI(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error { | func serveTLSWithSNI(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error { | ||||||
| 	addr := s.Server.Addr |  | ||||||
| 	if addr == "" { |  | ||||||
| 		addr = ":https" |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	config := cloneTLSConfig(s.TLSConfig) | 	config := cloneTLSConfig(s.TLSConfig) | ||||||
| 	if config.NextProtos == nil { | 	if config.NextProtos == nil { | ||||||
| 		config.NextProtos = []string{"http/1.1"} | 		config.NextProtos = []string{"http/1.1"} | ||||||
| @ -267,7 +261,7 @@ func (s *Server) ListenerFd() uintptr { | |||||||
| // (configuration and middleware stack) will handle the request. | // (configuration and middleware stack) will handle the request. | ||||||
| func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
| 	fmt.Println("Sleeping") | 	fmt.Println("Sleeping") | ||||||
| 	time.Sleep(5 * time.Second) | 	time.Sleep(5 * time.Second) // TODO: Temporarily making requests hang so we can test graceful restart | ||||||
| 	fmt.Println("Unblocking") | 	fmt.Println("Unblocking") | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		// In case the user doesn't enable error middleware, we still | 		// In case the user doesn't enable error middleware, we still | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user