caddy/caddyhttp/httpserver/plugin.go

533 lines
16 KiB
Go

package httpserver
import (
"flag"
"fmt"
"log"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddytls"
)
const serverType = "http"
func init() {
flag.StringVar(&HTTPPort, "http-port", HTTPPort, "Default port to use for HTTP")
flag.StringVar(&HTTPSPort, "https-port", HTTPSPort, "Default port to use for HTTPS")
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")
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, "root", hideCaddyfile)
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS })
}
// hideCaddyfile hides the source/origin Caddyfile if it is within the
// site root. This function should be run after parsing the root directive.
func hideCaddyfile(cctx caddy.Context) error {
ctx := cctx.(*httpContext)
for _, cfg := range ctx.siteConfigs {
// if no Caddyfile exists exit.
if cfg.originCaddyfile == "" {
return nil
}
absRoot, err := filepath.Abs(cfg.Root)
if err != nil {
return err
}
absOriginCaddyfile, err := filepath.Abs(cfg.originCaddyfile)
if err != nil {
return err
}
if strings.HasPrefix(absOriginCaddyfile, absRoot) {
cfg.HiddenFiles = append(cfg.HiddenFiles, strings.TrimPrefix(absOriginCaddyfile, absRoot))
}
}
return nil
}
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
}
// If default HTTP or HTTPS ports have been customized,
// make sure the ACME challenge ports match
var altHTTPPort, altTLSSNIPort string
if HTTPPort != DefaultHTTPPort {
altHTTPPort = HTTPPort
}
if HTTPSPort != DefaultHTTPSPort {
altTLSSNIPort = HTTPSPort
}
// Save the config to our master list, and key it for lookups
cfg := &SiteConfig{
Addr: addr,
Root: Root,
TLS: &caddytls.Config{
Hostname: addr.Host,
AltHTTPPort: altHTTPPort,
AltTLSSNIPort: altTLSSNIPort,
},
originCaddyfile: 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 == HTTPPort || 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 = HTTPSPort
}
}
// 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 == HTTPSPort {
scheme = "https"
} else {
scheme = "http"
}
}
s := scheme
if s != "" {
s += "://"
}
s += a.Host
if a.Port != "" &&
((scheme == "https" && a.Port != DefaultHTTPSPort) ||
(scheme == "http" && a.Port != DefaultHTTPPort)) {
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 = HTTPPort
} else if u.Scheme == "https" {
port = HTTPSPort
}
}
// 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 == HTTPSPort) || (u.Scheme == "https" && port == HTTPPort) {
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 = HTTPPort
} else if port == "https" {
u.Scheme = "https"
port = HTTPSPort
}
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",
"index",
"bind",
"maxrequestbody", // TODO: 'limits'
"timeouts",
"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 listener middleware to the stack
"proxyprotocol", // github.com/mastercactapus/caddy-proxyprotocol
// directives that add middleware to the stack
"locale", // github.com/simia-tech/caddy-locale
"log",
"rewrite",
"ext",
"gzip",
"header",
"errors",
"filter", // github.com/echocat/caddy-filter
"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",
"push",
"prometheus", // github.com/miekg/caddy-prometheus
"proxy",
"fastcgi",
"cgi", // github.com/jung-kurt/caddy-cgi
"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
}
const (
// DefaultHost is the default host.
DefaultHost = ""
// DefaultPort is the default port.
DefaultPort = "2015"
// DefaultRoot is the default root folder.
DefaultRoot = "."
// DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = "80"
// DefaultHTTPSPort is the default port for HTTPS.
DefaultHTTPSPort = "443"
)
// 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
// HTTPPort is the port to use for HTTP.
HTTPPort = DefaultHTTPPort
// HTTPSPort is the port to use for HTTPS.
HTTPSPort = DefaultHTTPSPort
)