package httpserver import ( "flag" "fmt" "log" "net" "net/url" "os" "strings" "time" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddytls" ) const serverType = "http" func init() { flag.StringVar(&Host, "host", DefaultHost, "Default host") flag.StringVar(&Port, "port", DefaultPort, "Default port") flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site") flag.DurationVar(&GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") // TODO flag.BoolVar(&HTTP2, "http2", true, "Use HTTP/2") flag.BoolVar(&QUIC, "quic", false, "Use experimental QUIC") caddy.RegisterServerType(serverType, caddy.ServerType{ Directives: func() []string { return directives }, DefaultInput: func() caddy.Input { if Port == DefaultPort && Host != "" { // by leaving the port blank in this case we give auto HTTPS // a chance to set the port to 443 for us return caddy.CaddyfileInput{ Contents: []byte(fmt.Sprintf("%s\nroot %s", Host, Root)), ServerTypeName: serverType, } } return caddy.CaddyfileInput{ Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)), ServerTypeName: serverType, } }, NewContext: newContext, }) caddy.RegisterCaddyfileLoader("short", caddy.LoaderFunc(shortCaddyfileLoader)) caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS) caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS }) } func newContext() caddy.Context { return &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)} } type httpContext struct { // keysToSiteConfigs maps an address at the top of a // server block (a "key") to its SiteConfig. Not all // SiteConfigs will be represented here, only ones // that appeared in the Caddyfile. keysToSiteConfigs map[string]*SiteConfig // siteConfigs is the master list of all site configs. siteConfigs []*SiteConfig } func (h *httpContext) saveConfig(key string, cfg *SiteConfig) { h.siteConfigs = append(h.siteConfigs, cfg) h.keysToSiteConfigs[key] = cfg } // InspectServerBlocks make sure that everything checks out before // executing directives and otherwise prepares the directives to // be parsed and executed. func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) { // For each address in each server block, make a new config for _, sb := range serverBlocks { for _, key := range sb.Keys { key = strings.ToLower(key) if _, dup := h.keysToSiteConfigs[key]; dup { return serverBlocks, fmt.Errorf("duplicate site address: %s", key) } addr, err := standardizeAddress(key) if err != nil { return serverBlocks, err } // Fill in address components from command line so that middleware // have access to the correct information during setup if addr.Host == "" && Host != DefaultHost { addr.Host = Host } if addr.Port == "" && Port != DefaultPort { addr.Port = Port } // Save the config to our master list, and key it for lookups cfg := &SiteConfig{ Addr: addr, Root: Root, TLS: &caddytls.Config{Hostname: addr.Host}, HiddenFiles: []string{sourceFile}, } h.saveConfig(key, cfg) } } // For sites that have gzip (which gets chained in // before the error handler) we should ensure that the // errors directive also appears so error pages aren't // written after the gzip writer is closed. See #616. for _, sb := range serverBlocks { _, hasGzip := sb.Tokens["gzip"] _, hasErrors := sb.Tokens["errors"] if hasGzip && !hasErrors { sb.Tokens["errors"] = []caddyfile.Token{{Text: "errors"}} } } return serverBlocks, nil } // MakeServers uses the newly-created siteConfigs to // create and return a list of server instances. func (h *httpContext) MakeServers() ([]caddy.Server, error) { // make sure TLS is disabled for explicitly-HTTP sites // (necessary when HTTP address shares a block containing tls) for _, cfg := range h.siteConfigs { if !cfg.TLS.Enabled { continue } if cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http" { cfg.TLS.Enabled = false log.Printf("[WARNING] TLS disabled for %s", cfg.Addr) } else if cfg.Addr.Scheme == "" { // set scheme to https ourselves, since TLS is enabled // and it was not explicitly set to something else. this // makes it appear as "https" when we print the list of // running sites; otherwise "http" would be assumed which // is incorrect for this site. cfg.Addr.Scheme = "https" } if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.OnDemand) { // this is vital, otherwise the function call below that // sets the listener address will use the default port // instead of 443 because it doesn't know about TLS. cfg.Addr.Port = "443" } } // we must map (group) each config to a bind address groups, err := groupSiteConfigsByListenAddr(h.siteConfigs) if err != nil { return nil, err } // then we create a server for each group var servers []caddy.Server for addr, group := range groups { s, err := NewServer(addr, group) if err != nil { return nil, err } servers = append(servers, s) } return servers, nil } // GetConfig gets the SiteConfig that corresponds to c. // If none exist (should only happen in tests), then a // new, empty one will be created. func GetConfig(c *caddy.Controller) *SiteConfig { ctx := c.Context().(*httpContext) key := strings.ToLower(c.Key) if cfg, ok := ctx.keysToSiteConfigs[key]; ok { return cfg } // we should only get here during tests because directive // actions typically skip the server blocks where we make // the configs cfg := &SiteConfig{Root: Root, TLS: new(caddytls.Config)} ctx.saveConfig(key, cfg) return cfg } // shortCaddyfileLoader loads a Caddyfile if positional arguments are // detected, or, in other words, if un-named arguments are provided to // the program. A "short Caddyfile" is one in which each argument // is a line of the Caddyfile. The default host and port are prepended // according to the Host and Port values. func shortCaddyfileLoader(serverType string) (caddy.Input, error) { if flag.NArg() > 0 && serverType == "http" { confBody := fmt.Sprintf("%s:%s\n%s", Host, Port, strings.Join(flag.Args(), "\n")) return caddy.CaddyfileInput{ Contents: []byte(confBody), Filepath: "args", ServerTypeName: serverType, }, nil } return nil, nil } // groupSiteConfigsByListenAddr groups site configs by their listen // (bind) address, so sites that use the same listener can be served // on the same server instance. The return value maps the listen // address (what you pass into net.Listen) to the list of site configs. // This function does NOT vet the configs to ensure they are compatible. func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConfig, error) { groups := make(map[string][]*SiteConfig) for _, conf := range configs { // We would add a special case here so that localhost addresses // bind to 127.0.0.1 if conf.ListenHost is not already set, which // would prevent outsiders from even connecting; but that was problematic: // https://forum.caddyserver.com/t/wildcard-virtual-domains-with-wildcard-roots/221/5?u=matt if conf.Addr.Port == "" { conf.Addr.Port = Port } addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Addr.Port)) if err != nil { return nil, err } addrstr := addr.String() groups[addrstr] = append(groups[addrstr], conf) } return groups, nil } // Address represents a site address. It contains // the original input value, and the component // parts of an address. The component parts may be // updated to the correct values as setup proceeds, // but the original value should never be changed. type Address struct { Original, Scheme, Host, Port, Path string } // String returns a human-friendly print of the address. func (a Address) String() string { if a.Host == "" && a.Port == "" { return "" } scheme := a.Scheme if scheme == "" { if a.Port == "443" { scheme = "https" } else { scheme = "http" } } s := scheme if s != "" { s += "://" } s += a.Host if a.Port != "" && ((scheme == "https" && a.Port != "443") || (scheme == "http" && a.Port != "80")) { s += ":" + a.Port } if a.Path != "" { s += a.Path } return s } // VHost returns a sensible concatenation of Host:Port/Path from a. // It's basically the a.Original but without the scheme. func (a Address) VHost() string { if idx := strings.Index(a.Original, "://"); idx > -1 { return a.Original[idx+3:] } return a.Original } // standardizeAddress parses an address string into a structured format with separate // scheme, host, port, and path portions, as well as the original input string. func standardizeAddress(str string) (Address, error) { input := str // Split input into components (prepend with // to assert host by default) if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") { str = "//" + str } u, err := url.Parse(str) if err != nil { return Address{}, err } // separate host and port host, port, err := net.SplitHostPort(u.Host) if err != nil { host, port, err = net.SplitHostPort(u.Host + ":") if err != nil { host = u.Host } } // see if we can set port based off scheme if port == "" { if u.Scheme == "http" { port = "80" } else if u.Scheme == "https" { port = "443" } } // repeated or conflicting scheme is confusing, so error if u.Scheme != "" && (port == "http" || port == "https") { return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input) } // error if scheme and port combination violate convention if (u.Scheme == "http" && port == "443") || (u.Scheme == "https" && port == "80") { return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input) } // standardize http and https ports to their respective port numbers if port == "http" { u.Scheme = "http" port = "80" } else if port == "https" { u.Scheme = "https" port = "443" } return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err } // RegisterDevDirective splices name into the list of directives // immediately before another directive. This function is ONLY // for plugin development purposes! NEVER use it for a plugin // that you are not currently building. If before is empty, // the directive will be appended to the end of the list. // // It is imperative that directives execute in the proper // order, and hard-coding the list of directives guarantees // a correct, absolute order every time. This function is // convenient when developing a plugin, but it does not // guarantee absolute ordering. Multiple plugins registering // directives with this function will lead to non- // deterministic builds and buggy software. // // Directive names must be lower-cased and unique. Any errors // here are fatal, and even successful calls print a message // to stdout as a reminder to use it only in development. func RegisterDevDirective(name, before string) { if name == "" { fmt.Println("[FATAL] Cannot register empty directive name") os.Exit(1) } if strings.ToLower(name) != name { fmt.Printf("[FATAL] %s: directive name must be lowercase\n", name) os.Exit(1) } for _, dir := range directives { if dir == name { fmt.Printf("[FATAL] %s: directive name already exists\n", name) os.Exit(1) } } if before == "" { directives = append(directives, name) } else { var found bool for i, dir := range directives { if dir == before { directives = append(directives[:i], append([]string{name}, directives[i:]...)...) found = true break } } if !found { fmt.Printf("[FATAL] %s: directive not found\n", before) os.Exit(1) } } msg := fmt.Sprintf("Registered directive '%s' ", name) if before == "" { msg += "at end of list" } else { msg += fmt.Sprintf("before '%s'", before) } fmt.Printf("[DEV NOTICE] %s\n", msg) } // directives is the list of all directives known to exist for the // http server type, including non-standard (3rd-party) directives. // The ordering of this list is important. var directives = []string{ // primitive actions that set up the fundamental vitals of each config "root", "bind", "maxrequestbody", "tls", // services/utilities, or other directives that don't necessarily inject handlers "startup", "shutdown", "realip", // github.com/captncraig/caddy-realip "git", // github.com/abiosoft/caddy-git // directives that add middleware to the stack "locale", // github.com/simia-tech/caddy-locale "log", "rewrite", "ext", "gzip", "header", "errors", "minify", // github.com/hacdias/caddy-minify "ipfilter", // github.com/pyed/ipfilter "ratelimit", // github.com/xuqingfeng/caddy-rate-limit "search", // github.com/pedronasser/caddy-search "expires", // github.com/epicagency/caddy-expires "basicauth", "redir", "status", "cors", // github.com/captncraig/cors/caddy "mime", "jwt", // github.com/BTBurke/caddy-jwt "jsonp", // github.com/pschlump/caddy-jsonp "upload", // blitznote.com/src/caddy.upload "multipass", // github.com/namsral/multipass/caddy "internal", "pprof", "expvar", "prometheus", // github.com/miekg/caddy-prometheus "proxy", "fastcgi", "websocket", "filemanager", // github.com/hacdias/caddy-filemanager "markdown", "templates", "browse", "hugo", // github.com/hacdias/caddy-hugo "mailout", // github.com/SchumacherFM/mailout "awslambda", // github.com/coopernurse/caddy-awslambda "filter", // github.com/echocat/caddy-filter } const ( // DefaultHost is the default host. DefaultHost = "" // DefaultPort is the default port. DefaultPort = "2015" // DefaultRoot is the default root folder. DefaultRoot = "." ) // These "soft defaults" are configurable by // command line flags, etc. var ( // Root is the site root Root = DefaultRoot // Host is the site host Host = DefaultHost // Port is the site port Port = DefaultPort // GracefulTimeout is the maximum duration of a graceful shutdown. GracefulTimeout time.Duration // HTTP2 indicates whether HTTP2 is enabled or not. HTTP2 bool // QUIC indicates whether QUIC is enabled or not. QUIC bool )