mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-11 12:47:57 +08:00
1dd413bd69
First, great job on the 0.9 release! It seems caddy's path lead into a bright future. Thanks also for including the locale plugin. Trying it, I've figured out, that there might be a problem with the order of the directives. In the typical use case, the result of the locale detection might be used in the `rewrite` and `log` plugin. If I'm not mistaken, it makes sense to put the `locale` directive before those.
408 lines
12 KiB
Go
408 lines
12 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mholt/caddy"
|
|
"github.com/mholt/caddy/caddyfile"
|
|
"github.com/mholt/caddy/caddytls"
|
|
)
|
|
|
|
const serverType = "http"
|
|
|
|
func init() {
|
|
flag.StringVar(&Host, "host", DefaultHost, "Default host")
|
|
flag.StringVar(&Port, "port", DefaultPort, "Default port")
|
|
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site")
|
|
flag.DurationVar(&GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown") // TODO
|
|
flag.BoolVar(&HTTP2, "http2", true, "Use HTTP/2")
|
|
flag.BoolVar(&QUIC, "quic", false, "Use experimental QUIC")
|
|
|
|
caddy.RegisterServerType(serverType, caddy.ServerType{
|
|
Directives: directives,
|
|
DefaultInput: func() caddy.Input {
|
|
if Port == DefaultPort && Host != "" {
|
|
// by leaving the port blank in this case we give auto HTTPS
|
|
// a chance to set the port to 443 for us
|
|
return caddy.CaddyfileInput{
|
|
Contents: []byte(fmt.Sprintf("%s\nroot %s", Host, Root)),
|
|
ServerTypeName: serverType,
|
|
}
|
|
}
|
|
return caddy.CaddyfileInput{
|
|
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
|
|
ServerTypeName: serverType,
|
|
}
|
|
},
|
|
NewContext: newContext,
|
|
})
|
|
caddy.RegisterCaddyfileLoader("short", caddy.LoaderFunc(shortCaddyfileLoader))
|
|
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
|
|
caddytls.RegisterConfigGetter(serverType, func(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)
|
|
if cfg, ok := ctx.keysToSiteConfigs[c.Key]; ok {
|
|
return cfg
|
|
}
|
|
// we should only get here during tests because directive
|
|
// actions typically skip the server blocks where we make
|
|
// the configs
|
|
ctx.saveConfig(c.Key, &SiteConfig{Root: Root, TLS: new(caddytls.Config)})
|
|
return GetConfig(c)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// directives is the list of all directives known to exist for the
|
|
// http server type, including non-standard (3rd-party) directives.
|
|
// The ordering of this list is important.
|
|
var directives = []string{
|
|
// primitive actions that set up the fundamental vitals of each config
|
|
"root",
|
|
"tls",
|
|
"bind",
|
|
|
|
// services/utilities, or other directives that don't necessarily inject handlers
|
|
"startup",
|
|
"shutdown",
|
|
"realip", // github.com/captncraig/caddy-realip
|
|
"git", // github.com/abiosoft/caddy-git
|
|
|
|
// directives that add middleware to the stack
|
|
"locale", // github.com/simia-tech/caddy-locale
|
|
"log",
|
|
"rewrite",
|
|
"ext",
|
|
"gzip",
|
|
"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
|
|
"header",
|
|
"redir",
|
|
"cors", // github.com/captncraig/cors/caddy
|
|
"mime",
|
|
"basicauth",
|
|
"jwt", // github.com/BTBurke/caddy-jwt
|
|
"jsonp", // github.com/pschlump/caddy-jsonp
|
|
"upload", // blitznote.com/src/caddy.upload
|
|
"internal",
|
|
"pprof",
|
|
"expvar",
|
|
"proxy",
|
|
"fastcgi",
|
|
"websocket",
|
|
"markdown",
|
|
"templates",
|
|
"browse",
|
|
"filemanager", // github.com/hacdias/caddy-filemanager
|
|
"hugo", // github.com/hacdias/caddy-hugo
|
|
"mailout", // github.com/SchumacherFM/mailout
|
|
"prometheus", // github.com/miekg/caddy-prometheus
|
|
}
|
|
|
|
const (
|
|
// DefaultHost is the default host.
|
|
DefaultHost = ""
|
|
// DefaultPort is the default port.
|
|
DefaultPort = "2015"
|
|
// DefaultRoot is the default root folder.
|
|
DefaultRoot = "."
|
|
)
|
|
|
|
// These "soft defaults" are configurable by
|
|
// command line flags, etc.
|
|
var (
|
|
// Root is the site root
|
|
Root = DefaultRoot
|
|
|
|
// Host is the site host
|
|
Host = DefaultHost
|
|
|
|
// Port is the site port
|
|
Port = DefaultPort
|
|
|
|
// GracefulTimeout is the maximum duration of a graceful shutdown.
|
|
GracefulTimeout time.Duration
|
|
|
|
// HTTP2 indicates whether HTTP2 is enabled or not.
|
|
HTTP2 bool
|
|
|
|
// QUIC indicates whether QUIC is enabled or not.
|
|
QUIC bool
|
|
)
|