Compare commits

..

8 Commits

Author SHA1 Message Date
Matthew Holt ec456811bb core: Don't return error on RegisterModule() and RegisterAdapter()
These functions are called at init-time, and their inputs are hard-coded
so there are no environmental or user factors that could make it fail
or succeed; the error return values are often ignored, and when they're
not, they are usually a fatal error anyway. To ensure that a programmer
mistake is not missed, we now panic instead.

Last breaking change 🤞
2020-04-13 09:48:54 -06:00
Matthew Holt 68cebb28d0 Fix some godocs 2020-04-11 09:01:40 -06:00
Matthew Holt a3bdc22234 admin: Always enforce Host header checks
With a simple heuristic for loopback addresses, we can enable this by
default without adding unnecessary inconvenience.
2020-04-10 17:31:38 -06:00
Matthew Holt d3383ced2a Update link in readme 2020-04-10 09:19:03 -06:00
Matthew Holt c024ae096d tests: Clean up redundant type declarations 2020-04-10 08:48:21 -06:00
Matthew Holt 3bee569a8a httpcaddyfile: Don't remove empty TLS conn policies (fix #3249)
Not sure why I thought that would be a good idea
2020-04-10 08:24:12 -06:00
Matthew Holt 999ab22b8c caddyhttp: Add nil check (fixes #3248 and fixes #3250) 2020-04-10 08:12:42 -06:00
Matthew Holt 9991fdc495 Update readme 2020-04-10 08:10:35 -06:00
14 changed files with 119 additions and 81 deletions
+2 -2
View File
@@ -51,7 +51,7 @@
- Can coordinate with other Caddy instances in a cluster
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/extending-caddy) lets Caddy do anything without bloat
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
@@ -81,7 +81,7 @@ _**Note:** These steps [will not embed proper version information](https://githu
Using [our builder tool](https://github.com/caddyserver/xcaddy)...
```
$ xcaddy --version CADDY_VERSION
$ xcaddy build <caddy_version>
```
...the following steps are automated:
+37 -19
View File
@@ -21,6 +21,7 @@ import (
"expvar"
"fmt"
"io"
"net"
"net/http"
"net/http/pprof"
"net/url"
@@ -78,27 +79,27 @@ type ConfigSettings struct {
// listenAddr extracts a singular listen address from ac.Listen,
// returning the network and the address of the listener.
func (admin AdminConfig) listenAddr() (string, string, error) {
func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
input := admin.Listen
if input == "" {
input = DefaultAdminListen
}
listenAddr, err := ParseNetworkAddress(input)
if err != nil {
return "", "", fmt.Errorf("parsing admin listener address: %v", err)
return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
}
if listenAddr.PortRangeSize() != 1 {
return "", "", fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
}
return listenAddr.Network, listenAddr.JoinHostPort(0), nil
return listenAddr, nil
}
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
muxWrap := adminHandler{
enforceOrigin: admin.EnforceOrigin,
allowedOrigins: admin.allowedOrigins(listenAddr),
allowedOrigins: admin.allowedOrigins(addr),
mux: http.NewServeMux(),
}
@@ -140,14 +141,30 @@ func (admin AdminConfig) newAdminHandler(listenAddr string) adminHandler {
// If admin.Origins is nil (null), the provided listen address
// will be used as the default origin. If admin.Origins is
// empty, no origins will be allowed, effectively bricking the
// endpoint, but whatever.
func (admin AdminConfig) allowedOrigins(listen string) []string {
// endpoint for non-unix-socket endpoints, but whatever.
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
uniqueOrigins := make(map[string]struct{})
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
}
if admin.Origins == nil {
uniqueOrigins[listen] = struct{}{}
if addr.isLoopback() {
if addr.IsUnixNetwork() {
// RFC 2616, Section 14.26:
// "A client MUST include a Host header field in all HTTP/1.1 request
// messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST
// be given with an empty value."
uniqueOrigins[""] = struct{}{}
} else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
}
}
if !addr.IsUnixNetwork() {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
}
}
allowed := make([]string, 0, len(uniqueOrigins))
for origin := range uniqueOrigins {
@@ -195,14 +212,14 @@ func replaceAdmin(cfg *Config) error {
}
// extract a singular listener address
netw, addr, err := adminConfig.listenAddr()
addr, err := adminConfig.listenAddr()
if err != nil {
return err
}
handler := adminConfig.newAdminHandler(addr)
ln, err := Listen(netw, addr)
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
if err != nil {
return err
}
@@ -219,7 +236,7 @@ func replaceAdmin(cfg *Config) error {
Log().Named("admin").Info(
"admin endpoint started",
zap.String("address", addr),
zap.String("address", addr.String()),
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
zap.Strings("origins", handler.allowedOrigins),
)
@@ -263,6 +280,7 @@ type adminHandler struct {
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Log().Named("admin.api").Info("received request",
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("uri", r.RequestURI),
zap.String("remote_addr", r.RemoteAddr),
zap.Reflect("headers", r.Header),
@@ -274,14 +292,14 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// be called more than once per request, for example if a request
// is rewritten (i.e. internal redirect).
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
if h.enforceOrigin {
// DNS rebinding mitigation
err := h.checkHost(r)
if err != nil {
h.handleError(w, r, err)
return
}
// DNS rebinding mitigation
err := h.checkHost(r)
if err != nil {
h.handleError(w, r, err)
return
}
if h.enforceOrigin {
// cross-site mitigation
origin, err := h.checkOrigin(r)
if err != nil {
+5 -4
View File
@@ -101,13 +101,14 @@ func JSONIndent(val interface{}) ([]byte, error) {
}
// RegisterAdapter registers a config adapter with the given name.
// This should usually be done at init-time.
func RegisterAdapter(name string, adapter Adapter) error {
// This should usually be done at init-time. It panics if the
// adapter cannot be registered successfully.
func RegisterAdapter(name string, adapter Adapter) {
if _, ok := configAdapters[name]; ok {
return fmt.Errorf("%s: already registered", name)
panic(fmt.Errorf("%s: already registered", name))
}
configAdapters[name] = adapter
return caddy.RegisterModule(adapterModule{name, adapter})
caddy.RegisterModule(adapterModule{name, adapter})
}
// GetAdapter returns the adapter with the given name,
+11 -11
View File
@@ -14,62 +14,62 @@ func TestHostsFromKeys(t *testing.T) {
}{
{
[]Address{
Address{Original: "foo", Host: "foo"},
{Original: "foo", Host: "foo"},
},
[]string{"foo"},
[]string{"foo"},
},
{
[]Address{
Address{Original: "foo", Host: "foo"},
Address{Original: "bar", Host: "bar"},
{Original: "foo", Host: "foo"},
{Original: "bar", Host: "bar"},
},
[]string{"bar", "foo"},
[]string{"bar", "foo"},
},
{
[]Address{
Address{Original: ":2015", Port: "2015"},
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{},
},
{
[]Address{
Address{Original: ":443", Port: "443"},
{Original: ":443", Port: "443"},
},
[]string{}, []string{},
},
{
[]Address{
Address{Original: "foo", Host: "foo"},
Address{Original: ":2015", Port: "2015"},
{Original: "foo", Host: "foo"},
{Original: ":2015", Port: "2015"},
},
[]string{}, []string{"foo"},
},
{
[]Address{
Address{Original: "example.com:2015", Host: "example.com", Port: "2015"},
{Original: "example.com:2015", Host: "example.com", Port: "2015"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
},
{
[]Address{
Address{Original: "example.com:80", Host: "example.com", Port: "80"},
{Original: "example.com:80", Host: "example.com", Port: "80"},
},
[]string{"example.com"},
[]string{"example.com"},
},
{
[]Address{
Address{Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
{Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
},
[]string{},
[]string{},
},
{
[]Address{
Address{Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"},
{Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"},
},
[]string{"example.com"},
[]string{"example.com:2015"},
-11
View File
@@ -569,18 +569,7 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
// consolidateConnPolicies removes empty TLS connection policies and combines
// equivalent ones for a cleaner overall output.
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
empty := new(caddytls.ConnectionPolicy)
for i := 0; i < len(cps); i++ {
// if the connection policy is empty or has
// only matchers, we can remove it entirely
empty.MatchersRaw = cps[i].MatchersRaw
if reflect.DeepEqual(empty, cps[i]) {
cps = append(cps[:i], cps[i+1:]...)
i--
continue
}
// compare it to the others
for j := 0; j < len(cps); j++ {
if j == i {
+25 -8
View File
@@ -289,14 +289,31 @@ func (na NetworkAddress) PortRangeSize() uint {
return (na.EndPort - na.StartPort) + 1
}
// String reconstructs the address string to the form expected
// by ParseNetworkAddress().
func (na NetworkAddress) String() string {
port := strconv.FormatUint(uint64(na.StartPort), 10)
if na.StartPort != na.EndPort {
port += "-" + strconv.FormatUint(uint64(na.EndPort), 10)
func (na NetworkAddress) isLoopback() bool {
if na.IsUnixNetwork() {
return true
}
return JoinNetworkAddress(na.Network, na.Host, port)
if na.Host == "localhost" {
return true
}
if ip := net.ParseIP(na.Host); ip != nil {
return ip.IsLoopback()
}
return false
}
func (na NetworkAddress) port() string {
if na.StartPort == na.EndPort {
return strconv.FormatUint(uint64(na.StartPort), 10)
}
return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort)
}
// String reconstructs the address string to the form expected
// by ParseNetworkAddress(). If the address is a unix socket,
// any non-zero port will be dropped.
func (na NetworkAddress) String() string {
return JoinNetworkAddress(na.Network, na.Host, na.port())
}
func isUnixNetwork(netw string) bool {
@@ -378,7 +395,7 @@ func JoinNetworkAddress(network, host, port string) string {
if network != "" {
a = network + "/"
}
if host != "" && port == "" {
if (host != "" && port == "") || isUnixNetwork(network) {
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
+8
View File
@@ -138,6 +138,14 @@ func TestJoinNetworkAddress(t *testing.T) {
network: "unix", host: "/foo/bar", port: "",
expect: "unix//foo/bar",
},
{
network: "unix", host: "/foo/bar", port: "0",
expect: "unix//foo/bar",
},
{
network: "unix", host: "/foo/bar", port: "1234",
expect: "unix//foo/bar",
},
{
network: "", host: "::1", port: "1234",
expect: "[::1]:1234",
+9 -10
View File
@@ -125,33 +125,32 @@ type ModuleMap map[string]json.RawMessage
// be properly recorded, this should be called in the
// init phase of runtime. Typically, the module package
// will do this as a side-effect of being imported.
// This function returns an error if the module's info
// is incomplete or invalid, or if the module is
// already registered.
func RegisterModule(instance Module) error {
// This function panics if the module's info is
// incomplete or invalid, or if the module is already
// registered.
func RegisterModule(instance Module) {
mod := instance.CaddyModule()
if mod.ID == "" {
return fmt.Errorf("module ID missing")
panic("module ID missing")
}
if mod.ID == "caddy" || mod.ID == "admin" {
return fmt.Errorf("module ID '%s' is reserved", mod.ID)
panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID))
}
if mod.New == nil {
return fmt.Errorf("missing ModuleInfo.New")
panic("missing ModuleInfo.New")
}
if val := mod.New(); val == nil {
return fmt.Errorf("ModuleInfo.New must return a non-nil module instance")
panic("ModuleInfo.New must return a non-nil module instance")
}
modulesMu.Lock()
defer modulesMu.Unlock()
if _, ok := modules[string(mod.ID)]; ok {
return fmt.Errorf("module already registered: %s", mod.ID)
panic(fmt.Sprintf("module already registered: %s", mod.ID))
}
modules[string(mod.ID)] = mod
return nil
}
// GetModule returns module information from its ID (full name).
+1 -4
View File
@@ -30,10 +30,7 @@ import (
)
func init() {
err := caddy.RegisterModule(App{})
if err != nil {
caddy.Log().Fatal(err.Error())
}
caddy.RegisterModule(App{})
}
// App is a robust, production-ready HTTP server.
+1 -4
View File
@@ -30,10 +30,7 @@ import (
func init() {
weakrand.Seed(time.Now().UnixNano())
err := caddy.RegisterModule(tlsPlaceholderWrapper{})
if err != nil {
caddy.Log().Fatal(err.Error())
}
caddy.RegisterModule(tlsPlaceholderWrapper{})
}
// RequestMatcher is a type that can match to a request.
+4
View File
@@ -44,6 +44,10 @@ func init() {
// This enables complex logic to be expressed using a comfortable,
// familiar syntax.
//
// This matcher's JSON interface is actually a string, not a struct.
// The generated docs are not correct because this type has custom
// marshaling logic.
//
// COMPATIBILITY NOTE: This module is still experimental and is not
// subject to Caddy's compatibility guarantee.
type MatchExpression struct {
+1 -1
View File
@@ -85,7 +85,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
if domain != "" {
route.MatcherSetsRaw = []caddy.ModuleMap{
caddy.ModuleMap{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),
},
}
+12 -4
View File
@@ -170,8 +170,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
repl.Set("http.response.latency", latency)
logger := accLog
if loggerName := s.Logs.getLoggerName(r.Host); loggerName != "" {
logger = logger.Named(loggerName)
if s.Logs != nil {
logger = s.Logs.wrapLogger(logger, r.Host)
}
log := logger.Info
@@ -200,8 +200,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
// prepare the error log
logger := errLog
if loggerName := s.Logs.getLoggerName(r.Host); loggerName != "" {
logger = logger.Named(loggerName)
if s.Logs != nil {
logger = s.Logs.wrapLogger(logger, r.Host)
}
// get the values that will be used to log the error
@@ -378,6 +378,14 @@ type ServerLogConfig struct {
LoggerNames map[string]string `json:"logger_names,omitempty"`
}
// wrapLogger wraps logger in a logger named according to user preferences for the given host.
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger {
if loggerName := slc.getLoggerName(host); loggerName != "" {
return logger.Named(loggerName)
}
return logger
}
func (slc ServerLogConfig) getLoggerName(host string) string {
if loggerName, ok := slc.LoggerNames[host]; ok {
return loggerName
+3 -3
View File
@@ -34,11 +34,11 @@ func init() {
// Since this handler does not write a response, the error information
// is for use by the server to know how to handle the error.
type StaticError struct {
// The recommended HTTP status code. Can be either an integer or a
// string if placeholders are needed. Optional. Default is 500.
// The error message. Optional. Default is no error message.
Error string `json:"error,omitempty"`
// The error message. Optional. Default is no error message.
// The recommended HTTP status code. Can be either an integer or a
// string if placeholders are needed. Optional. Default is 500.
StatusCode WeakString `json:"status_code,omitempty"`
}