mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-24 08:26:58 -04:00
Rewrote Caddy from the ground up; initial commit of 0.9 branch
These changes span work from the last ~4 months in an effort to make Caddy more extensible, reduce the coupling between its components, and lay a more robust foundation of code going forward into 1.0. A bunch of new features have been added, too, with even higher future potential. The most significant design change is an overall inversion of dependencies. Instead of the caddy package knowing about the server and the notion of middleware and config, the caddy package exposes an interface that other components plug into. This does introduce more indirection when reading the code, but every piece is very modular and pluggable. Even the HTTP server is pluggable. The caddy package has been moved to the top level, and main has been pushed into a subfolder called caddy. The actual logic of the main file has been pushed even further into caddy/caddymain/run.go so that custom builds of Caddy can be 'go get'able. The HTTPS logic was surgically separated into two parts to divide the TLS-specific code and the HTTPS-specific code. The caddytls package can now be used by any type of server that needs TLS, not just HTTP. I also added the ability to customize nearly every aspect of TLS at the site level rather than all sites sharing the same TLS configuration. Not all of this flexibility is exposed in the Caddyfile yet, but it may be in the future. Caddy can also generate self-signed certificates in memory for the convenience of a developer working on localhost who wants HTTPS. And Caddy now supports the DNS challenge, assuming at least one DNS provider is plugged in. Dozens, if not hundreds, of other minor changes swept through the code base as I literally started from an empty main function, copying over functions or files as needed, then adjusting them to fit in the new design. Most tests have been restored and adapted to the new API, but more work is needed there. A lot of what was "impossible" before is now possible, or can be made possible with minimal disruption of the code. For example, it's fairly easy to make plugins hook into another part of the code via callbacks. Plugins can do more than just be directives; we now have plugins that customize how the Caddyfile is loaded (useful when you need to get your configuration from a remote store). Site addresses no longer need be just a host and port. They can have a path, allowing you to scope a configuration to a specific path. There is no inheretance, however; each site configuration is distinct. Thanks to amazing work by Lucas Clemente, this commit adds experimental QUIC support. Turn it on using the -quic flag; your browser may have to be configured to enable it. Almost everything is here, but you will notice that most of the middle- ware are missing. After those are transferred over, we'll be ready for beta tests. I'm very excited to get this out. Thanks for everyone's help and patience these last few months. I hope you like it!!
This commit is contained in:
80
caddyhttp/httpserver/graceful.go
Normal file
80
caddyhttp/httpserver/graceful.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// TODO: Should this be a generic graceful listener available in its own package or something?
|
||||
// Also, passing in a WaitGroup is a little awkward. Why can't this listener just keep
|
||||
// the waitgroup internal to itself?
|
||||
|
||||
// newGracefulListener returns a gracefulListener that wraps l and
|
||||
// uses wg (stored in the host server) to count connections.
|
||||
func newGracefulListener(l net.Listener, wg *sync.WaitGroup) *gracefulListener {
|
||||
gl := &gracefulListener{Listener: l, stop: make(chan error), connWg: wg}
|
||||
go func() {
|
||||
<-gl.stop
|
||||
gl.Lock()
|
||||
gl.stopped = true
|
||||
gl.Unlock()
|
||||
gl.stop <- gl.Listener.Close()
|
||||
}()
|
||||
return gl
|
||||
}
|
||||
|
||||
// gracefuListener is a net.Listener which can
|
||||
// count the number of connections on it. Its
|
||||
// methods mainly wrap net.Listener to be graceful.
|
||||
type gracefulListener struct {
|
||||
net.Listener
|
||||
stop chan error
|
||||
stopped bool
|
||||
sync.Mutex // protects the stopped flag
|
||||
connWg *sync.WaitGroup // pointer to the host's wg used for counting connections
|
||||
}
|
||||
|
||||
// Accept accepts a connection.
|
||||
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
|
||||
c, err = gl.Listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c = gracefulConn{Conn: c, connWg: gl.connWg}
|
||||
gl.connWg.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
// Close immediately closes the listener.
|
||||
func (gl *gracefulListener) Close() error {
|
||||
gl.Lock()
|
||||
if gl.stopped {
|
||||
gl.Unlock()
|
||||
return syscall.EINVAL
|
||||
}
|
||||
gl.Unlock()
|
||||
gl.stop <- nil
|
||||
return <-gl.stop
|
||||
}
|
||||
|
||||
// gracefulConn represents a connection on a
|
||||
// gracefulListener so that we can keep track
|
||||
// of the number of connections, thus facilitating
|
||||
// a graceful shutdown.
|
||||
type gracefulConn struct {
|
||||
net.Conn
|
||||
connWg *sync.WaitGroup // pointer to the host server's connection waitgroup
|
||||
}
|
||||
|
||||
// Close closes c's underlying connection while updating the wg count.
|
||||
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.connWg.Done()
|
||||
return nil
|
||||
}
|
||||
154
caddyhttp/httpserver/https.go
Normal file
154
caddyhttp/httpserver/https.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func activateHTTPS() error {
|
||||
// TODO: Is this loop a bug? Should we scope this method to just a single context? (restarts...?)
|
||||
for _, ctx := range contexts {
|
||||
// pre-screen each config and earmark the ones that qualify for managed TLS
|
||||
markQualifiedForAutoHTTPS(ctx.siteConfigs)
|
||||
|
||||
// place certificates and keys on disk
|
||||
for _, c := range ctx.siteConfigs {
|
||||
err := c.TLS.ObtainCert(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update TLS configurations
|
||||
err := enableAutoHTTPS(ctx.siteConfigs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set up redirects
|
||||
ctx.siteConfigs = makePlaintextRedirects(ctx.siteConfigs)
|
||||
}
|
||||
|
||||
// renew all relevant certificates that need renewal. this is important
|
||||
// to do right away so we guarantee that renewals aren't missed, and
|
||||
// also the user can respond to any potential errors that occur.
|
||||
err := caddytls.RenewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// markQualifiedForAutoHTTPS scans each config and, if it
|
||||
// qualifies for managed TLS, it sets the Managed field of
|
||||
// the TLS config to true.
|
||||
func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
|
||||
for _, cfg := range configs {
|
||||
if caddytls.QualifiesForManagedTLS(cfg) && cfg.Addr.Scheme != "http" {
|
||||
cfg.TLS.Managed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enableAutoHTTPS configures each config to use TLS according to default settings.
|
||||
// It will only change configs that are marked as managed, and assumes that
|
||||
// certificates and keys are already on disk. If loadCertificates is true,
|
||||
// the certificates will be loaded from disk into the cache for this process
|
||||
// to use. If false, TLS will still be enabled and configured with default
|
||||
// settings, but no certificates will be parsed loaded into the cache, and
|
||||
// the returned error value will always be nil.
|
||||
func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
for _, cfg := range configs {
|
||||
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
cfg.TLS.Enabled = true
|
||||
cfg.Addr.Scheme = "https"
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.Addr.Host) {
|
||||
_, err := caddytls.CacheManagedCertificate(cfg.Addr.Host, cfg.TLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure any config values not explicitly set are set to default
|
||||
caddytls.SetDefaultTLSParams(cfg.TLS)
|
||||
|
||||
// Set default port of 443 if not explicitly set
|
||||
if cfg.Addr.Port == "" &&
|
||||
cfg.TLS.Enabled &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
|
||||
cfg.Addr.Host != "localhost" {
|
||||
cfg.Addr.Port = "443"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
|
||||
// hosts. You must pass in all configs, not just configs that qualify, since
|
||||
// we must know whether the same host already exists on port 80, and those would
|
||||
// not be in a list of configs that qualify for automatic HTTPS. This function will
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, "80") &&
|
||||
(cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// hostHasOtherPort returns true if there is another config in the list with the same
|
||||
// hostname that has port otherPort, or false otherwise. All the configs are checked
|
||||
// against the hostname of allConfigs[thisConfigIdx].
|
||||
func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort string) bool {
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == thisConfigIdx {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Addr.Host == allConfigs[thisConfigIdx].Addr.Host &&
|
||||
otherCfg.Addr.Port == otherPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on port 80. The TLS field of cfg must not be nil.
|
||||
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
redirPort := cfg.Addr.Port
|
||||
if redirPort == "443" {
|
||||
// default port is redundant
|
||||
redirPort = ""
|
||||
}
|
||||
redirMiddleware := func(next Handler) Handler {
|
||||
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
toURL := "https://" + r.Host
|
||||
if redirPort != "" {
|
||||
toURL += ":" + redirPort
|
||||
}
|
||||
toURL += r.URL.RequestURI()
|
||||
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
host := cfg.Addr.Host
|
||||
port := "80"
|
||||
addr := net.JoinHostPort(host, port)
|
||||
return &SiteConfig{
|
||||
Addr: Address{Original: addr, Host: host, Port: port},
|
||||
ListenHost: cfg.ListenHost,
|
||||
middleware: []Middleware{redirMiddleware},
|
||||
TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort},
|
||||
}
|
||||
}
|
||||
178
caddyhttp/httpserver/https_test.go
Normal file
178
caddyhttp/httpserver/https_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(&SiteConfig{
|
||||
Addr: Address{
|
||||
Host: "example.com",
|
||||
Port: "1234",
|
||||
},
|
||||
ListenHost: "93.184.216.34",
|
||||
TLS: new(caddytls.Config),
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Addr.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected {
|
||||
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Addr.Port, "80"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.middleware == nil || len(cfg.middleware) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.middleware)
|
||||
}
|
||||
|
||||
handler := cfg.middleware[0](nil)
|
||||
|
||||
// Check redirect for correctness
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://foo/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err := handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Expected status return to be 0, but was %d", status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected returned error to be nil, but was %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), "https://foo:1234/bar?q=1"; got != want {
|
||||
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
|
||||
}
|
||||
|
||||
// browsers can infer a default port from scheme, so make sure the port
|
||||
// doesn't get added in explicitly for default ports like 443 for https.
|
||||
cfg = redirPlaintextHost(&SiteConfig{Addr: Address{Host: "example.com", Port: "443"}, TLS: new(caddytls.Config)})
|
||||
handler = cfg.middleware[0](nil)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "http://foo/bar?q=1", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status, err = handler.ServeHTTP(rec, req)
|
||||
if status != 0 {
|
||||
t.Errorf("Expected status return to be 0, but was %d", status)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Expected returned error to be nil, but was %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusMovedPermanently {
|
||||
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
|
||||
}
|
||||
if got, want := rec.Header().Get("Location"), "https://foo/bar?q=1"; got != want {
|
||||
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHasOtherPort(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com", Port: "80"}},
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "80"}},
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "443"}},
|
||||
}
|
||||
|
||||
if hostHasOtherPort(configs, 0, "80") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
|
||||
}
|
||||
if hostHasOtherPort(configs, 0, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
|
||||
}
|
||||
if !hostHasOtherPort(configs, 1, "443") {
|
||||
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePlaintextRedirects(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
// Happy path = standard redirect from 80 to 443
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Host on port 80 already defined; don't change it (no redirect)
|
||||
{Addr: Address{Host: "sub1.example.com", Port: "80", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Redirect from port 80 to port 5000 in this case
|
||||
{Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Managed: true}},
|
||||
|
||||
// Can redirect from 80 to either 443 or 5001, but choose 443
|
||||
{Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{Managed: true}},
|
||||
}
|
||||
|
||||
result := makePlaintextRedirects(configs)
|
||||
expectedRedirCount := 3
|
||||
|
||||
if len(result) != len(configs)+expectedRedirCount {
|
||||
t.Errorf("Expected %d redirect(s) to be added, but got %d",
|
||||
expectedRedirCount, len(result)-len(configs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAutoHTTPS(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
enableAutoHTTPS(configs, false)
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
}
|
||||
if configs[0].Addr.Scheme != "https" {
|
||||
t.Errorf("Expected config 0 to have Addr.Scheme == \"https\", but it was \"%s\"",
|
||||
configs[0].Addr.Scheme)
|
||||
}
|
||||
if configs[1].TLS != nil && configs[1].TLS.Enabled {
|
||||
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: ""}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com"}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
markQualifiedForAutoHTTPS(configs)
|
||||
|
||||
count := 0
|
||||
for _, cfg := range configs {
|
||||
if cfg.TLS.Managed {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if count != expectedManagedCount {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
||||
156
caddyhttp/httpserver/middleware.go
Normal file
156
caddyhttp/httpserver/middleware.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initCaseSettings()
|
||||
}
|
||||
|
||||
type (
|
||||
// Middleware is the middle layer which represents the traditional
|
||||
// idea of middleware: it chains one Handler to the next by being
|
||||
// passed the next Handler in the chain.
|
||||
Middleware func(Handler) Handler
|
||||
|
||||
// Handler is like http.Handler except ServeHTTP may return a status
|
||||
// code and/or error.
|
||||
//
|
||||
// If ServeHTTP writes to the response body, it should return a status
|
||||
// code of 0. This signals to other handlers above it that the response
|
||||
// body is already written, and that they should not write to it also.
|
||||
//
|
||||
// If ServeHTTP encounters an error, it should return the error value
|
||||
// so it can be logged by designated error-handling middleware.
|
||||
//
|
||||
// If writing a response after calling another ServeHTTP method, the
|
||||
// returned status code SHOULD be used when writing the response.
|
||||
//
|
||||
// If handling errors after calling another ServeHTTP method, the
|
||||
// returned error value SHOULD be logged or handled accordingly.
|
||||
//
|
||||
// Otherwise, return values should be propagated down the middleware
|
||||
// chain by returning them unchanged.
|
||||
Handler interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// HandlerFunc is a convenience type like http.HandlerFunc, except
|
||||
// ServeHTTP returns a status code and an error. See Handler
|
||||
// documentation for more information.
|
||||
HandlerFunc func(http.ResponseWriter, *http.Request) (int, error)
|
||||
)
|
||||
|
||||
// ServeHTTP implements the Handler interface.
|
||||
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return f(w, r)
|
||||
}
|
||||
|
||||
// IndexFile looks for a file in /root/fpath/indexFile for each string
|
||||
// in indexFiles. If an index file is found, it returns the root-relative
|
||||
// path to the file and true. If no index file is found, empty string
|
||||
// and false is returned. fpath must end in a forward slash '/'
|
||||
// otherwise no index files will be tried (directory paths must end
|
||||
// in a forward slash according to HTTP).
|
||||
//
|
||||
// All paths passed into and returned from this function use '/' as the
|
||||
// path separator, just like URLs. IndexFle handles path manipulation
|
||||
// internally for systems that use different path separators.
|
||||
func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) {
|
||||
if fpath[len(fpath)-1] != '/' || root == nil {
|
||||
return "", false
|
||||
}
|
||||
for _, indexFile := range indexFiles {
|
||||
// func (http.FileSystem).Open wants all paths separated by "/",
|
||||
// regardless of operating system convention, so use
|
||||
// path.Join instead of filepath.Join
|
||||
fp := path.Join(fpath, indexFile)
|
||||
f, err := root.Open(fp)
|
||||
if err == nil {
|
||||
f.Close()
|
||||
return fp, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// SetLastModifiedHeader checks if the provided modTime is valid and if it is sets it
|
||||
// as a Last-Modified header to the ResponseWriter. If the modTime is in the future
|
||||
// the current time is used instead.
|
||||
func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
|
||||
if modTime.IsZero() || modTime.Equal(time.Unix(0, 0)) {
|
||||
// the time does not appear to be valid. Don't put it in the response
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 2616 - Section 14.29 - Last-Modified:
|
||||
// An origin server MUST NOT send a Last-Modified date which is later than the
|
||||
// server's time of message origination. In such cases, where the resource's last
|
||||
// modification would indicate some time in the future, the server MUST replace
|
||||
// that date with the message origination date.
|
||||
now := currentTime()
|
||||
if modTime.After(now) {
|
||||
modTime = now
|
||||
}
|
||||
|
||||
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
// CaseSensitivePath determines if paths should be case sensitive.
|
||||
// This is configurable via CASE_SENSITIVE_PATH environment variable.
|
||||
var CaseSensitivePath = true
|
||||
|
||||
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
|
||||
// initCaseSettings loads case sensitivity config from environment variable.
|
||||
//
|
||||
// This could have been in init, but init cannot be called from tests.
|
||||
func initCaseSettings() {
|
||||
switch os.Getenv(caseSensitivePathEnv) {
|
||||
case "0", "false":
|
||||
CaseSensitivePath = false
|
||||
default:
|
||||
CaseSensitivePath = true
|
||||
}
|
||||
}
|
||||
|
||||
// Path represents a URI path.
|
||||
type Path string
|
||||
|
||||
// Matches checks to see if other matches p.
|
||||
//
|
||||
// Path matching will probably not always be a direct
|
||||
// comparison; this method assures that paths can be
|
||||
// easily and consistently matched.
|
||||
func (p Path) Matches(other string) bool {
|
||||
if CaseSensitivePath {
|
||||
return strings.HasPrefix(string(p), other)
|
||||
}
|
||||
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other))
|
||||
}
|
||||
|
||||
// currentTime, as it is defined here, returns time.Now().
|
||||
// It's defined as a variable for mocking time in tests.
|
||||
var currentTime = func() time.Time { return time.Now() }
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// Middleware functions so that the assignment to the
|
||||
// Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// plugins can use this as a convenience.
|
||||
var EmptyNext = HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { return 0, nil })
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// plugins can use this as a convenience.
|
||||
func SameNext(next1, next2 Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
||||
58
caddyhttp/httpserver/middleware_test.go
Normal file
58
caddyhttp/httpserver/middleware_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathCaseSensitivity(t *testing.T) {
|
||||
tests := []struct {
|
||||
basePath string
|
||||
path string
|
||||
caseSensitive bool
|
||||
expected bool
|
||||
}{
|
||||
{"/", "/file", true, true},
|
||||
{"/a", "/file", true, false},
|
||||
{"/f", "/file", true, true},
|
||||
{"/f", "/File", true, false},
|
||||
{"/f", "/File", false, true},
|
||||
{"/file", "/file", true, true},
|
||||
{"/file", "/file", false, true},
|
||||
{"/files", "/file", false, false},
|
||||
{"/files", "/file", true, false},
|
||||
{"/folder", "/folder/file.txt", true, true},
|
||||
{"/folders", "/folder/file.txt", true, false},
|
||||
{"/folder", "/Folder/file.txt", false, true},
|
||||
{"/folders", "/Folder/file.txt", false, false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
CaseSensitivePath = test.caseSensitive
|
||||
valid := Path(test.path).Matches(test.basePath)
|
||||
if test.expected != valid {
|
||||
t.Errorf("Test %d: Expected %v, found %v", i, test.expected, valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathCaseSensitiveEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
envValue string
|
||||
expected bool
|
||||
}{
|
||||
{"1", true},
|
||||
{"0", false},
|
||||
{"false", false},
|
||||
{"true", true},
|
||||
{"", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
os.Setenv(caseSensitivePathEnv, test.envValue)
|
||||
initCaseSettings()
|
||||
if test.expected != CaseSensitivePath {
|
||||
t.Errorf("Test %d: Expected %v, found %v", i, test.expected, CaseSensitivePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
367
caddyhttp/httpserver/plugin.go
Normal file
367
caddyhttp/httpserver/plugin.go
Normal file
@@ -0,0 +1,367 @@
|
||||
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(key string) *caddytls.Config { return GetConfig(key).TLS })
|
||||
}
|
||||
|
||||
var contexts []*httpContext
|
||||
|
||||
func newContext() caddy.Context {
|
||||
context := &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
contexts = append(contexts, context)
|
||||
return context
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// TODO: Here we can inspect the server blocks
|
||||
// and make changes to them, like adding a directive
|
||||
// that must always be present (e.g. 'errors discard`?) -
|
||||
// totally optional; server types need not register this
|
||||
// function.
|
||||
|
||||
// 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
|
||||
}
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &SiteConfig{
|
||||
Addr: addr,
|
||||
TLS: &caddytls.Config{Hostname: addr.Host},
|
||||
HiddenFiles: []string{sourceFile},
|
||||
}
|
||||
h.siteConfigs = append(h.siteConfigs, cfg)
|
||||
h.keysToSiteConfigs[key] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
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 && (cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http") {
|
||||
cfg.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 a SiteConfig that is keyed by addrKey.
|
||||
// It creates an empty one in the latest context if
|
||||
// the key does not exist in any context, so it
|
||||
// will never return nil. If no contexts exist (which
|
||||
// should never happen except in tests), it creates a
|
||||
// new context in which to put it.
|
||||
func GetConfig(addrKey string) *SiteConfig {
|
||||
for _, context := range contexts {
|
||||
if cfg, ok := context.keysToSiteConfigs[addrKey]; ok {
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
if len(contexts) == 0 {
|
||||
// this shouldn't happen except in tests
|
||||
newContext()
|
||||
}
|
||||
cfg := new(SiteConfig)
|
||||
cfg.TLS = new(caddytls.Config)
|
||||
defaultCtx := contexts[len(contexts)-1]
|
||||
defaultCtx.siteConfigs = append(defaultCtx.siteConfigs, cfg)
|
||||
defaultCtx.keysToSiteConfigs[addrKey] = 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 {
|
||||
if caddy.IsLoopback(conf.Addr.Host) && conf.ListenHost == "" {
|
||||
// special case: one would not expect a site served
|
||||
// at loopback to be connected to from the outside.
|
||||
conf.ListenHost = conf.Addr.Host
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// AddMiddleware adds a middleware to a site's middleware stack.
|
||||
func (sc *SiteConfig) AddMiddleware(m Middleware) {
|
||||
sc.middleware = append(sc.middleware, m)
|
||||
}
|
||||
|
||||
// Address represents a site address. It contains
|
||||
// the original input value, and the component
|
||||
// parts of an address.
|
||||
type Address struct {
|
||||
Original, Scheme, Host, Port, Path string
|
||||
}
|
||||
|
||||
// String returns a human-friendly print of the address.
|
||||
func (a Address) String() string {
|
||||
scheme := a.Scheme
|
||||
if scheme == "" {
|
||||
if a.Port == "80" {
|
||||
scheme = "http"
|
||||
} else if a.Port == "443" {
|
||||
scheme = "https"
|
||||
}
|
||||
}
|
||||
s := scheme
|
||||
if s != "" {
|
||||
s += "://"
|
||||
}
|
||||
s += a.Host
|
||||
if (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, and port 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, "//") {
|
||||
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 basics of each config
|
||||
"root",
|
||||
"bind",
|
||||
"tls",
|
||||
|
||||
// these don't inject middleware handlers
|
||||
"startup",
|
||||
"shutdown",
|
||||
|
||||
// these add middleware to the stack
|
||||
"log",
|
||||
"gzip",
|
||||
"errors",
|
||||
"header",
|
||||
"rewrite",
|
||||
"redir",
|
||||
"ext",
|
||||
"mime",
|
||||
"basicauth",
|
||||
"internal",
|
||||
"pprof",
|
||||
"expvar",
|
||||
"proxy",
|
||||
"fastcgi",
|
||||
"websocket",
|
||||
"markdown",
|
||||
"templates",
|
||||
"browse",
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
92
caddyhttp/httpserver/plugin_test.go
Normal file
92
caddyhttp/httpserver/plugin_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package httpserver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
scheme, host, port, path string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "", "localhost", "", "", false},
|
||||
{`localhost:1234`, "", "localhost", "1234", "", false},
|
||||
{`localhost:`, "", "localhost", "", "", false},
|
||||
{`0.0.0.0`, "", "0.0.0.0", "", "", false},
|
||||
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false},
|
||||
{`:1234`, "", "", "1234", "", false},
|
||||
{`[::1]`, "", "::1", "", "", false},
|
||||
{`[::1]:1234`, "", "::1", "1234", "", false},
|
||||
{`:`, "", "", "", "", false},
|
||||
{`localhost:http`, "http", "localhost", "80", "", false},
|
||||
{`localhost:https`, "https", "localhost", "443", "", false},
|
||||
{`:http`, "http", "", "80", "", false},
|
||||
{`:https`, "https", "", "443", "", false},
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", "", false},
|
||||
{`https://localhost`, "https", "localhost", "443", "", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false},
|
||||
{`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false},
|
||||
{`http://[::1]`, "http", "::1", "80", "", false},
|
||||
{`http://localhost:1234`, "http", "localhost", "1234", "", false},
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
|
||||
{``, "", "", "", "", false},
|
||||
{`::1`, "", "::1", "", "", true},
|
||||
{`localhost::`, "", "localhost::", "", "", true},
|
||||
{`#$%@`, "", "", "", "", true},
|
||||
{`host/path`, "", "host", "", "/path", false},
|
||||
{`http://host/`, "http", "host", "80", "/", false},
|
||||
{`//asdf`, "", "asdf", "", "", false},
|
||||
{`:1234/asdf`, "", "", "1234", "/asdf", false},
|
||||
{`http://host/path`, "http", "host", "80", "/path", false},
|
||||
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
|
||||
{`host:80/path`, "", "host", "80", "/path", false},
|
||||
{`host:https/path`, "https", "host", "443", "/path", false},
|
||||
} {
|
||||
actual, err := standardizeAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
|
||||
}
|
||||
|
||||
if !test.shouldErr && actual.Original != test.input {
|
||||
t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original)
|
||||
}
|
||||
if actual.Scheme != test.scheme {
|
||||
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != test.host {
|
||||
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
|
||||
}
|
||||
if actual.Port != test.port {
|
||||
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
|
||||
}
|
||||
if actual.Path != test.path {
|
||||
t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressVHost(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
addr Address
|
||||
expected string
|
||||
}{
|
||||
{Address{Original: "host:1234"}, "host:1234"},
|
||||
{Address{Original: "host:1234/foo"}, "host:1234/foo"},
|
||||
{Address{Original: "host/foo"}, "host/foo"},
|
||||
{Address{Original: "http://host/foo"}, "host/foo"},
|
||||
{Address{Original: "https://host/foo"}, "host/foo"},
|
||||
} {
|
||||
actual := test.addr.VHost()
|
||||
if actual != test.expected {
|
||||
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
98
caddyhttp/httpserver/recorder.go
Normal file
98
caddyhttp/httpserver/recorder.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResponseRecorder is a type of http.ResponseWriter that captures
|
||||
// the status code written to it and also the size of the body
|
||||
// written in the response. A status code does not have
|
||||
// to be written, however, in which case 200 must be assumed.
|
||||
// It is best to have the constructor initialize this type
|
||||
// with that default status code.
|
||||
//
|
||||
// Setting the Replacer field allows middlewares to type-assert
|
||||
// the http.ResponseWriter to ResponseRecorder and set their own
|
||||
// placeholder values for logging utilities to use.
|
||||
//
|
||||
// Beware when accessing the Replacer value; it may be nil!
|
||||
type ResponseRecorder struct {
|
||||
http.ResponseWriter
|
||||
Replacer Replacer
|
||||
status int
|
||||
size int
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// NewResponseRecorder makes and returns a new responseRecorder,
|
||||
// which captures the HTTP Status code from the ResponseWriter
|
||||
// and also the length of the response body written through it.
|
||||
// Because a status is not set unless WriteHeader is called
|
||||
// explicitly, this constructor initializes with a status code
|
||||
// of 200 to cover the default case.
|
||||
func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
|
||||
return &ResponseRecorder{
|
||||
ResponseWriter: w,
|
||||
status: http.StatusOK,
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHeader records the status code and calls the
|
||||
// underlying ResponseWriter's WriteHeader method.
|
||||
func (r *ResponseRecorder) WriteHeader(status int) {
|
||||
r.status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write is a wrapper that records the size of the body
|
||||
// that gets written.
|
||||
func (r *ResponseRecorder) Write(buf []byte) (int, error) {
|
||||
n, err := r.ResponseWriter.Write(buf)
|
||||
if err == nil {
|
||||
r.size += n
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Size is a Getter to size property
|
||||
func (r *ResponseRecorder) Size() int {
|
||||
return r.size
|
||||
}
|
||||
|
||||
// Status is a Getter to status property
|
||||
func (r *ResponseRecorder) Status() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It simply wraps the underlying
|
||||
// ResponseWriter's Hijack method if there is one, or returns an error.
|
||||
func (r *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return hj.Hijack()
|
||||
}
|
||||
return nil, nil, errors.New("not a Hijacker")
|
||||
}
|
||||
|
||||
// Flush implements http.Flusher. It simply wraps the underlying
|
||||
// ResponseWriter's Flush method if there is one, or does nothing.
|
||||
func (r *ResponseRecorder) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
panic("not a Flusher") // should be recovered at the beginning of middleware stack
|
||||
}
|
||||
}
|
||||
|
||||
// CloseNotify implements http.CloseNotifier.
|
||||
// It just inherits the underlying ResponseWriter's CloseNotify method.
|
||||
func (r *ResponseRecorder) CloseNotify() <-chan bool {
|
||||
if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok {
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
panic("not a CloseNotifier")
|
||||
}
|
||||
40
caddyhttp/httpserver/recorder_test.go
Normal file
40
caddyhttp/httpserver/recorder_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewResponseRecorder(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
if !(recordRequest.ResponseWriter == w) {
|
||||
t.Fatalf("Expected Response writer in the Recording to be same as the one sent\n")
|
||||
}
|
||||
if recordRequest.status != http.StatusOK {
|
||||
t.Fatalf("Expected recorded status to be http.StatusOK (%d) , but found %d\n ", http.StatusOK, recordRequest.status)
|
||||
}
|
||||
}
|
||||
func TestWriteHeader(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
recordRequest.WriteHeader(401)
|
||||
if w.Code != 401 || recordRequest.status != 401 {
|
||||
t.Fatalf("Expected Response status to be set to 401, but found %d\n", recordRequest.status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
responseTestString := "test"
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
buf := []byte(responseTestString)
|
||||
recordRequest.Write(buf)
|
||||
if recordRequest.size != len(buf) {
|
||||
t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size)
|
||||
}
|
||||
if w.Body.String() != responseTestString {
|
||||
t.Fatalf("Expected Response Body to be %s , but found %s\n", responseTestString, w.Body.String())
|
||||
}
|
||||
}
|
||||
161
caddyhttp/httpserver/replacer.go
Normal file
161
caddyhttp/httpserver/replacer.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Replacer is a type which can replace placeholder
|
||||
// substrings in a string with actual values from a
|
||||
// http.Request and ResponseRecorder. Always use
|
||||
// NewReplacer to get one of these. Any placeholders
|
||||
// made with Set() should overwrite existing values if
|
||||
// the key is already used.
|
||||
type Replacer interface {
|
||||
Replace(string) string
|
||||
Set(key, value string)
|
||||
}
|
||||
|
||||
// replacer implements Replacer. customReplacements
|
||||
// is used to store custom replacements created with
|
||||
// Set() until the time of replacement, at which point
|
||||
// they will be used to overwrite other replacements
|
||||
// if there is a name conflict.
|
||||
type replacer struct {
|
||||
replacements map[string]string
|
||||
customReplacements map[string]string
|
||||
emptyValue string
|
||||
responseRecorder *ResponseRecorder
|
||||
}
|
||||
|
||||
// NewReplacer makes a new replacer based on r and rr which
|
||||
// are used for request and response placeholders, respectively.
|
||||
// Request placeholders are created immediately, whereas
|
||||
// response placeholders are not created until Replace()
|
||||
// is invoked. rr may be nil if it is not available.
|
||||
// emptyValue should be the string that is used in place
|
||||
// of empty string (can still be empty string).
|
||||
func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Replacer {
|
||||
rep := &replacer{
|
||||
responseRecorder: rr,
|
||||
customReplacements: make(map[string]string),
|
||||
replacements: map[string]string{
|
||||
"{method}": r.Method,
|
||||
"{scheme}": func() string {
|
||||
if r.TLS != nil {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}(),
|
||||
"{hostname}": func() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
}(),
|
||||
"{host}": r.Host,
|
||||
"{path}": r.URL.Path,
|
||||
"{path_escaped}": url.QueryEscape(r.URL.Path),
|
||||
"{query}": r.URL.RawQuery,
|
||||
"{query_escaped}": url.QueryEscape(r.URL.RawQuery),
|
||||
"{fragment}": r.URL.Fragment,
|
||||
"{proto}": r.Proto,
|
||||
"{remote}": func() string {
|
||||
if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" {
|
||||
return fwdFor
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}(),
|
||||
"{port}": func() string {
|
||||
_, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return port
|
||||
}(),
|
||||
"{uri}": r.URL.RequestURI(),
|
||||
"{uri_escaped}": url.QueryEscape(r.URL.RequestURI()),
|
||||
"{when}": time.Now().Format(timeFormat),
|
||||
"{file}": func() string {
|
||||
_, file := path.Split(r.URL.Path)
|
||||
return file
|
||||
}(),
|
||||
"{dir}": func() string {
|
||||
dir, _ := path.Split(r.URL.Path)
|
||||
return dir
|
||||
}(),
|
||||
},
|
||||
emptyValue: emptyValue,
|
||||
}
|
||||
|
||||
// Header placeholders (case-insensitive)
|
||||
for header, values := range r.Header {
|
||||
rep.replacements[headerReplacer+strings.ToLower(header)+"}"] = strings.Join(values, ",")
|
||||
}
|
||||
|
||||
return rep
|
||||
}
|
||||
|
||||
// Replace performs a replacement of values on s and returns
|
||||
// the string with the replaced values.
|
||||
func (r *replacer) Replace(s string) string {
|
||||
// Make response placeholders now
|
||||
if r.responseRecorder != nil {
|
||||
r.replacements["{status}"] = strconv.Itoa(r.responseRecorder.status)
|
||||
r.replacements["{size}"] = strconv.Itoa(r.responseRecorder.size)
|
||||
r.replacements["{latency}"] = time.Since(r.responseRecorder.start).String()
|
||||
}
|
||||
|
||||
// Include custom placeholders, overwriting existing ones if necessary
|
||||
for key, val := range r.customReplacements {
|
||||
r.replacements[key] = val
|
||||
}
|
||||
|
||||
// Header replacements - these are case-insensitive, so we can't just use strings.Replace()
|
||||
for strings.Contains(s, headerReplacer) {
|
||||
idxStart := strings.Index(s, headerReplacer)
|
||||
endOffset := idxStart + len(headerReplacer)
|
||||
idxEnd := strings.Index(s[endOffset:], "}")
|
||||
if idxEnd > -1 {
|
||||
placeholder := strings.ToLower(s[idxStart : endOffset+idxEnd+1])
|
||||
replacement := r.replacements[placeholder]
|
||||
if replacement == "" {
|
||||
replacement = r.emptyValue
|
||||
}
|
||||
s = s[:idxStart] + replacement + s[endOffset+idxEnd+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Regular replacements - these are easier because they're case-sensitive
|
||||
for placeholder, replacement := range r.replacements {
|
||||
if replacement == "" {
|
||||
replacement = r.emptyValue
|
||||
}
|
||||
s = strings.Replace(s, placeholder, replacement, -1)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Set sets key to value in the r.customReplacements map.
|
||||
func (r *replacer) Set(key, value string) {
|
||||
r.customReplacements["{"+key+"}"] = value
|
||||
}
|
||||
|
||||
const (
|
||||
timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
headerReplacer = "{>"
|
||||
)
|
||||
137
caddyhttp/httpserver/replacer_test.go
Normal file
137
caddyhttp/httpserver/replacer_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewReplacer(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
if err != nil {
|
||||
t.Fatal("Request Formation Failed\n")
|
||||
}
|
||||
rep := NewReplacer(request, recordRequest, "")
|
||||
|
||||
switch v := rep.(type) {
|
||||
case *replacer:
|
||||
if v.replacements["{host}"] != "localhost" {
|
||||
t.Error("Expected host to be localhost")
|
||||
}
|
||||
if v.replacements["{method}"] != "POST" {
|
||||
t.Error("Expected request method to be POST")
|
||||
}
|
||||
|
||||
// Response placeholders should only be set after call to Replace()
|
||||
if got, want := v.replacements["{status}"], ""; got != want {
|
||||
t.Errorf("Expected status to NOT be set before Replace() is called; was: %s", got)
|
||||
}
|
||||
rep.Replace("foobar")
|
||||
if got, want := v.replacements["{status}"], "200"; got != want {
|
||||
t.Errorf("Expected status to be %s, was: %s", want, got)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("Expected *replacer underlying Replacer type, got: %#v", rep)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
if err != nil {
|
||||
t.Fatal("Request Formation Failed\n")
|
||||
}
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to determine hostname\n")
|
||||
}
|
||||
if expected, actual := "This hostname is "+hostname, repl.Replace("This hostname is {hostname}"); expected != actual {
|
||||
t.Errorf("{hostname} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
if expected, actual := "This host is localhost.", repl.Replace("This host is {host}."); expected != actual {
|
||||
t.Errorf("{host} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := "This request method is POST.", repl.Replace("This request method is {method}."); expected != actual {
|
||||
t.Errorf("{method} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := "The response status is 200.", repl.Replace("The response status is {status}."); expected != actual {
|
||||
t.Errorf("{status} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := "The Custom header is foobarbaz.", repl.Replace("The Custom header is {>Custom}."); expected != actual {
|
||||
t.Errorf("{>Custom} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test header case-insensitivity
|
||||
if expected, actual := "The cUsToM header is foobarbaz...", repl.Replace("The cUsToM header is {>cUsToM}..."); expected != actual {
|
||||
t.Errorf("{>cUsToM} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test non-existent header/value
|
||||
if expected, actual := "The Non-Existent header is -.", repl.Replace("The Non-Existent header is {>Non-Existent}."); expected != actual {
|
||||
t.Errorf("{>Non-Existent} replacement: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test bad placeholder
|
||||
if expected, actual := "Bad {host placeholder...", repl.Replace("Bad {host placeholder..."); expected != actual {
|
||||
t.Errorf("bad placeholder: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test bad header placeholder
|
||||
if expected, actual := "Bad {>Custom placeholder", repl.Replace("Bad {>Custom placeholder"); expected != actual {
|
||||
t.Errorf("bad header placeholder: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test bad header placeholder with valid one later
|
||||
if expected, actual := "Bad -", repl.Replace("Bad {>Custom placeholder {>ShorterVal}"); expected != actual {
|
||||
t.Errorf("bad header placeholders: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test shorter header value with multiple placeholders
|
||||
if expected, actual := "Short value 1 then foobarbaz.", repl.Replace("Short value {>ShorterVal} then {>Custom}."); expected != actual {
|
||||
t.Errorf("short value: expected '%s', got '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Request Formation Failed \n")
|
||||
}
|
||||
repl := NewReplacer(request, recordRequest, "")
|
||||
|
||||
repl.Set("host", "getcaddy.com")
|
||||
repl.Set("method", "GET")
|
||||
repl.Set("status", "201")
|
||||
repl.Set("variable", "value")
|
||||
|
||||
if repl.Replace("This host is {host}") != "This host is getcaddy.com" {
|
||||
t.Error("Expected host replacement failed")
|
||||
}
|
||||
if repl.Replace("This request method is {method}") != "This request method is GET" {
|
||||
t.Error("Expected method replacement failed")
|
||||
}
|
||||
if repl.Replace("The response status is {status}") != "The response status is 201" {
|
||||
t.Error("Expected status replacement failed")
|
||||
}
|
||||
if repl.Replace("The value of variable is {variable}") != "The value of variable is value" {
|
||||
t.Error("Expected variable replacement failed")
|
||||
}
|
||||
}
|
||||
64
caddyhttp/httpserver/roller.go
Normal file
64
caddyhttp/httpserver/roller.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// LogRoller implements a type that provides a rolling logger.
|
||||
type LogRoller struct {
|
||||
Filename string
|
||||
MaxSize int
|
||||
MaxAge int
|
||||
MaxBackups int
|
||||
LocalTime bool
|
||||
}
|
||||
|
||||
// GetLogWriter returns an io.Writer that writes to a rolling logger.
|
||||
func (l LogRoller) GetLogWriter() io.Writer {
|
||||
return &lumberjack.Logger{
|
||||
Filename: l.Filename,
|
||||
MaxSize: l.MaxSize,
|
||||
MaxAge: l.MaxAge,
|
||||
MaxBackups: l.MaxBackups,
|
||||
LocalTime: l.LocalTime,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRoller parses roller contents out of c.
|
||||
func ParseRoller(c *caddy.Controller) (*LogRoller, error) {
|
||||
var size, age, keep int
|
||||
// This is kind of a hack to support nested blocks:
|
||||
// As we are already in a block: either log or errors,
|
||||
// c.nesting > 0 but, as soon as c meets a }, it thinks
|
||||
// the block is over and return false for c.NextBlock.
|
||||
for c.NextBlock() {
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value := c.Val()
|
||||
var err error
|
||||
switch what {
|
||||
case "size":
|
||||
size, err = strconv.Atoi(value)
|
||||
case "age":
|
||||
age, err = strconv.Atoi(value)
|
||||
case "keep":
|
||||
keep, err = strconv.Atoi(value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &LogRoller{
|
||||
MaxSize: size,
|
||||
MaxAge: age,
|
||||
MaxBackups: keep,
|
||||
LocalTime: true,
|
||||
}, nil
|
||||
}
|
||||
378
caddyhttp/httpserver/server.go
Normal file
378
caddyhttp/httpserver/server.go
Normal file
@@ -0,0 +1,378 @@
|
||||
// Package httpserver implements an HTTP server on top of Caddy.
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/h2quic"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// Server is the HTTP server implementation.
|
||||
type Server struct {
|
||||
Server *http.Server
|
||||
quicServer *h2quic.Server
|
||||
listener net.Listener
|
||||
listenerMu sync.Mutex
|
||||
sites []*SiteConfig
|
||||
connTimeout time.Duration // max time to wait for a connection before force stop
|
||||
connWg sync.WaitGroup // one increment per connection
|
||||
tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine
|
||||
vhosts *vhostTrie
|
||||
}
|
||||
|
||||
// ensure it satisfies the interface
|
||||
var _ caddy.GracefulServer = new(Server)
|
||||
|
||||
// NewServer creates a new Server instance that will listen on addr
|
||||
// and will serve the sites configured in group.
|
||||
func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
s := &Server{
|
||||
Server: &http.Server{
|
||||
Addr: addr,
|
||||
// TODO: Make these values configurable?
|
||||
// ReadTimeout: 2 * time.Minute,
|
||||
// WriteTimeout: 2 * time.Minute,
|
||||
// MaxHeaderBytes: 1 << 16,
|
||||
},
|
||||
vhosts: newVHostTrie(),
|
||||
sites: group,
|
||||
connTimeout: GracefulTimeout,
|
||||
}
|
||||
s.Server.Handler = s // this is weird, but whatever
|
||||
|
||||
// Disable HTTP/2 if desired
|
||||
if !HTTP2 {
|
||||
s.Server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
}
|
||||
|
||||
// Enable QUIC if desired
|
||||
if QUIC {
|
||||
s.quicServer = &h2quic.Server{Server: s.Server}
|
||||
}
|
||||
|
||||
// We have to bound our wg with one increment
|
||||
// to prevent a "race condition" that is hard-coded
|
||||
// into sync.WaitGroup.Wait() - basically, an add
|
||||
// with a positive delta must be guaranteed to
|
||||
// occur before Wait() is called on the wg.
|
||||
// In a way, this kind of acts as a safety barrier.
|
||||
s.connWg.Add(1)
|
||||
|
||||
// Set up TLS configuration
|
||||
var tlsConfigs []*caddytls.Config
|
||||
var err error
|
||||
for _, site := range group {
|
||||
tlsConfigs = append(tlsConfigs, site.TLS)
|
||||
}
|
||||
s.Server.TLSConfig, err = caddytls.MakeTLSConfig(tlsConfigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compile custom middleware for every site (enables virtual hosting)
|
||||
for _, site := range group {
|
||||
stack := Handler(staticfiles.FileServer{Root: http.Dir(site.Root), Hide: site.HiddenFiles})
|
||||
for i := len(site.middleware) - 1; i >= 0; i-- {
|
||||
stack = site.middleware[i](stack)
|
||||
}
|
||||
site.middlewareChain = stack
|
||||
s.vhosts.Insert(site.Addr.VHost(), site)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Listen creates an active listener for s that can be
|
||||
// used to serve requests.
|
||||
func (s *Server) Listen() (net.Listener, error) {
|
||||
if s.Server == nil {
|
||||
return nil, fmt.Errorf("Server field is nil")
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", s.Server.Addr)
|
||||
if err != nil {
|
||||
var succeeded bool
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows has been known to keep sockets open even after closing the listeners.
|
||||
// Tests reveal this error case easily because they call Start() then Stop()
|
||||
// in succession. TODO: Better way to handle this? And why limit this to Windows?
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
ln, err = net.Listen("tcp", s.Server.Addr)
|
||||
if err == nil {
|
||||
succeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !succeeded {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Very important to return a concrete caddy.Listener
|
||||
// implementation for graceful restarts.
|
||||
return ln.(*net.TCPListener), nil
|
||||
}
|
||||
|
||||
// Serve serves requests on ln. It blocks until ln is closed.
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
if tcpLn, ok := ln.(*net.TCPListener); ok {
|
||||
ln = tcpKeepAliveListener{TCPListener: tcpLn}
|
||||
}
|
||||
|
||||
ln = newGracefulListener(ln, &s.connWg)
|
||||
|
||||
s.listenerMu.Lock()
|
||||
s.listener = ln
|
||||
s.listenerMu.Unlock()
|
||||
|
||||
if s.Server.TLSConfig != nil {
|
||||
// Create TLS listener - note that we do not replace s.listener
|
||||
// with this TLS listener; tls.listener is unexported and does
|
||||
// not implement the File() method we need for graceful restarts
|
||||
// on POSIX systems.
|
||||
// TODO: Is this ^ still relevant anymore? Maybe we can now that it's a net.Listener...
|
||||
ln = tls.NewListener(ln, s.Server.TLSConfig)
|
||||
|
||||
// Rotate TLS session ticket keys
|
||||
s.tlsGovChan = caddytls.RotateSessionTicketKeys(s.Server.TLSConfig)
|
||||
}
|
||||
|
||||
if QUIC {
|
||||
go func() {
|
||||
err := s.quicServer.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] listening for QUIC connections: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
err := s.Server.Serve(ln)
|
||||
if QUIC {
|
||||
s.quicServer.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ServeHTTP is the entry point of all HTTP requests.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
// We absolutely need to be sure we stay alive up here,
|
||||
// even though, in theory, the errors middleware does this.
|
||||
if rec := recover(); rec != nil {
|
||||
log.Printf("[PANIC] %v", rec)
|
||||
DefaultErrorFunc(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
w.Header().Set("Server", "Caddy")
|
||||
|
||||
sanitizePath(r)
|
||||
|
||||
status, _ := s.serveHTTP(w, r)
|
||||
|
||||
// Fallback error response in case error handling wasn't chained in
|
||||
if status >= 400 {
|
||||
DefaultErrorFunc(w, r, status)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// strip out the port because it's not used in virtual
|
||||
// hosting; the port is irrelevant because each listener
|
||||
// is on a different port.
|
||||
hostname, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
hostname = r.Host
|
||||
}
|
||||
|
||||
// look up the virtualhost; if no match, serve error
|
||||
vhost, pathPrefix := s.vhosts.Match(hostname + r.URL.Path)
|
||||
|
||||
if vhost == nil {
|
||||
// check for ACME challenge even if vhost is nil;
|
||||
// could be a new host coming online soon
|
||||
if caddytls.HTTPChallengeHandler(w, r, caddytls.DefaultHTTPAlternatePort) {
|
||||
return 0, nil
|
||||
}
|
||||
// otherwise, log the error and write a message to the client
|
||||
remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteHost = r.RemoteAddr
|
||||
}
|
||||
writeTextResponse(w, http.StatusNotFound, "No such site at "+s.Server.Addr)
|
||||
log.Printf("[INFO] %s - No such site at %s (Remote: %s, Referer: %s)",
|
||||
hostname, s.Server.Addr, remoteHost, r.Header.Get("Referer"))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// we still check for ACME challenge if the vhost exists,
|
||||
// because we must apply its HTTP challenge config settings
|
||||
if s.proxyHTTPChallenge(vhost, w, r) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// trim the path portion of the site address from the beginning of
|
||||
// the URL path, so a request to example.com/foo/blog on the site
|
||||
// defined as example.com/foo appears as /blog instead of /foo/blog.
|
||||
if pathPrefix != "/" {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, pathPrefix)
|
||||
if !strings.HasPrefix(r.URL.Path, "/") {
|
||||
r.URL.Path = "/" + r.URL.Path
|
||||
}
|
||||
}
|
||||
|
||||
return vhost.middlewareChain.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// proxyHTTPChallenge solves the ACME HTTP challenge if r is the HTTP
|
||||
// request for the challenge. If it is, and if the request has been
|
||||
// fulfilled (response written), true is returned; false otherwise.
|
||||
// If you don't have a vhost, just call the challenge handler directly.
|
||||
func (s *Server) proxyHTTPChallenge(vhost *SiteConfig, w http.ResponseWriter, r *http.Request) bool {
|
||||
if vhost.Addr.Port != caddytls.HTTPChallengePort {
|
||||
return false
|
||||
}
|
||||
if vhost.TLS != nil && vhost.TLS.Manual {
|
||||
return false
|
||||
}
|
||||
altPort := caddytls.DefaultHTTPAlternatePort
|
||||
if vhost.TLS != nil && vhost.TLS.AltHTTPPort != "" {
|
||||
altPort = vhost.TLS.AltHTTPPort
|
||||
}
|
||||
return caddytls.HTTPChallengeHandler(w, r, altPort)
|
||||
}
|
||||
|
||||
// Address returns the address s was assigned to listen on.
|
||||
func (s *Server) Address() string {
|
||||
return s.Server.Addr
|
||||
}
|
||||
|
||||
// Stop stops s gracefully (or forcefully after timeout) and
|
||||
// closes its listener.
|
||||
func (s *Server) Stop() (err error) {
|
||||
s.Server.SetKeepAlivesEnabled(false)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// force connections to close after timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.connWg.Done() // decrement our initial increment used as a barrier
|
||||
s.connWg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait for remaining connections to finish or
|
||||
// force them all to close after timeout
|
||||
select {
|
||||
case <-time.After(s.connTimeout):
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
|
||||
// Close the listener now; this stops the server without delay
|
||||
s.listenerMu.Lock()
|
||||
if s.listener != nil {
|
||||
err = s.listener.Close()
|
||||
}
|
||||
s.listenerMu.Unlock()
|
||||
|
||||
// Closing this signals any TLS governor goroutines to exit
|
||||
if s.tlsGovChan != nil {
|
||||
close(s.tlsGovChan)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// sanitizePath collapses any ./ ../ /// madness
|
||||
// which helps prevent path traversal attacks.
|
||||
// Note to middleware: use URL.RawPath If you need
|
||||
// the "original" URL.Path value.
|
||||
func sanitizePath(r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
return
|
||||
}
|
||||
cleanedPath := path.Clean(r.URL.Path)
|
||||
if cleanedPath == "." {
|
||||
r.URL.Path = "/"
|
||||
} else {
|
||||
if !strings.HasPrefix(cleanedPath, "/") {
|
||||
cleanedPath = "/" + cleanedPath
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/") && !strings.HasSuffix(cleanedPath, "/") {
|
||||
cleanedPath = cleanedPath + "/"
|
||||
}
|
||||
r.URL.Path = cleanedPath
|
||||
}
|
||||
}
|
||||
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming caddy.Quiet == false.
|
||||
func (s *Server) OnStartupComplete() {
|
||||
if caddy.Quiet {
|
||||
return
|
||||
}
|
||||
for _, site := range s.sites {
|
||||
output := site.Addr.String()
|
||||
if caddy.IsLocalhost(s.Address()) && !caddy.IsLocalhost(site.Addr.Host) {
|
||||
output += " (only accessible on this machine)"
|
||||
}
|
||||
fmt.Println(output)
|
||||
}
|
||||
}
|
||||
|
||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
// go away.
|
||||
//
|
||||
// Borrowed from the Go standard library.
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
// Accept accepts the connection with a keep-alive enabled.
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
// File implements caddy.Listener; it returns the underlying file of the listener.
|
||||
func (ln tcpKeepAliveListener) File() (*os.File, error) {
|
||||
return ln.TCPListener.File()
|
||||
}
|
||||
|
||||
// DefaultErrorFunc responds to an HTTP request with a simple description
|
||||
// of the specified HTTP status code.
|
||||
func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) {
|
||||
writeTextResponse(w, status, fmt.Sprintf("%d %s", status, http.StatusText(status)))
|
||||
}
|
||||
|
||||
// writeTextResponse writes body with code status to w. The body will
|
||||
// be interpreted as plain text.
|
||||
func writeTextResponse(w http.ResponseWriter, status int, body string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(body))
|
||||
}
|
||||
15
caddyhttp/httpserver/server_test.go
Normal file
15
caddyhttp/httpserver/server_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddress(t *testing.T) {
|
||||
addr := "127.0.0.1:9005"
|
||||
srv := &Server{Server: &http.Server{Addr: addr}}
|
||||
|
||||
if got, want := srv.Address(), addr; got != want {
|
||||
t.Errorf("Expected '%s' but got '%s'", want, got)
|
||||
}
|
||||
}
|
||||
53
caddyhttp/httpserver/siteconfig.go
Normal file
53
caddyhttp/httpserver/siteconfig.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package httpserver
|
||||
|
||||
import "github.com/mholt/caddy/caddytls"
|
||||
|
||||
// SiteConfig contains information about a site
|
||||
// (also known as a virtual host).
|
||||
type SiteConfig struct {
|
||||
// The address of the site
|
||||
Addr Address
|
||||
|
||||
// The hostname to bind listener to;
|
||||
// defaults to Addr.Host
|
||||
ListenHost string
|
||||
|
||||
// TLS configuration
|
||||
TLS *caddytls.Config
|
||||
|
||||
// Uncompiled middleware stack
|
||||
middleware []Middleware
|
||||
|
||||
// Compiled middleware stack
|
||||
middlewareChain Handler
|
||||
|
||||
// Directory from which to serve files
|
||||
Root string
|
||||
|
||||
// A list of files to hide (for example, the
|
||||
// source Caddyfile). TODO: Enforcing this
|
||||
// should be centralized, for example, a
|
||||
// standardized way of loading files from disk
|
||||
// for a request.
|
||||
HiddenFiles []string
|
||||
}
|
||||
|
||||
// TLSConfig returns s.TLS.
|
||||
func (s SiteConfig) TLSConfig() *caddytls.Config {
|
||||
return s.TLS
|
||||
}
|
||||
|
||||
// Host returns s.Addr.Host.
|
||||
func (s SiteConfig) Host() string {
|
||||
return s.Addr.Host
|
||||
}
|
||||
|
||||
// Port returns s.Addr.Port.
|
||||
func (s SiteConfig) Port() string {
|
||||
return s.Addr.Port
|
||||
}
|
||||
|
||||
// Middleware returns s.middleware (useful for tests).
|
||||
func (s SiteConfig) Middleware() []Middleware {
|
||||
return s.middleware
|
||||
}
|
||||
139
caddyhttp/httpserver/vhosttrie.go
Normal file
139
caddyhttp/httpserver/vhosttrie.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// vhostTrie facilitates virtual hosting. It matches
|
||||
// requests first by hostname (with support for
|
||||
// wildcards as TLS certificates support them), then
|
||||
// by longest matching path.
|
||||
type vhostTrie struct {
|
||||
edges map[string]*vhostTrie
|
||||
site *SiteConfig // also known as a virtual host
|
||||
path string // the path portion of the key for this node
|
||||
}
|
||||
|
||||
// newVHostTrie returns a new vhostTrie.
|
||||
func newVHostTrie() *vhostTrie {
|
||||
return &vhostTrie{edges: make(map[string]*vhostTrie)}
|
||||
}
|
||||
|
||||
// Insert adds stack to t keyed by key. The key should be
|
||||
// a valid "host/path" combination (or just host).
|
||||
func (t *vhostTrie) Insert(key string, site *SiteConfig) {
|
||||
host, path := t.splitHostPath(key)
|
||||
if _, ok := t.edges[host]; !ok {
|
||||
t.edges[host] = newVHostTrie()
|
||||
}
|
||||
t.edges[host].insertPath(path, path, site)
|
||||
}
|
||||
|
||||
// insertPath expects t to be a host node (not a root node),
|
||||
// and inserts site into the t according to remainingPath.
|
||||
func (t *vhostTrie) insertPath(remainingPath, originalPath string, site *SiteConfig) {
|
||||
if remainingPath == "" {
|
||||
t.site = site
|
||||
t.path = originalPath
|
||||
return
|
||||
}
|
||||
ch := string(remainingPath[0])
|
||||
if _, ok := t.edges[ch]; !ok {
|
||||
t.edges[ch] = newVHostTrie()
|
||||
}
|
||||
t.edges[ch].insertPath(remainingPath[1:], originalPath, site)
|
||||
}
|
||||
|
||||
// Match returns the virtual host (site) in v with
|
||||
// the closest match to key. If there was a match,
|
||||
// it returns the SiteConfig and the path portion of
|
||||
// the key used to make the match. The matched path
|
||||
// would be a prefix of the path portion of the
|
||||
// key, if not the whole path portion of the key.
|
||||
// If there is no match, nil and empty string will
|
||||
// be returned.
|
||||
//
|
||||
// A typical key will be in the form "host" or "host/path".
|
||||
func (t *vhostTrie) Match(key string) (*SiteConfig, string) {
|
||||
host, path := t.splitHostPath(key)
|
||||
// try the given host, then, if no match, try wildcard hosts
|
||||
branch := t.matchHost(host)
|
||||
if branch == nil {
|
||||
branch = t.matchHost("0.0.0.0")
|
||||
}
|
||||
if branch == nil {
|
||||
branch = t.matchHost("")
|
||||
}
|
||||
if branch == nil {
|
||||
return nil, ""
|
||||
}
|
||||
node := branch.matchPath(path)
|
||||
if node == nil {
|
||||
return nil, ""
|
||||
}
|
||||
return node.site, node.path
|
||||
}
|
||||
|
||||
// matchHost returns the vhostTrie matching host. The matching
|
||||
// algorithm is the same as used to match certificates to host
|
||||
// with SNI during TLS handshakes. In other words, it supports,
|
||||
// to some degree, the use of wildcard (*) characters.
|
||||
func (t *vhostTrie) matchHost(host string) *vhostTrie {
|
||||
// try exact match
|
||||
if subtree, ok := t.edges[host]; ok {
|
||||
return subtree
|
||||
}
|
||||
|
||||
// then try replacing labels in the host
|
||||
// with wildcards until we get a match
|
||||
labels := strings.Split(host, ".")
|
||||
for i := range labels {
|
||||
labels[i] = "*"
|
||||
candidate := strings.Join(labels, ".")
|
||||
if subtree, ok := t.edges[candidate]; ok {
|
||||
return subtree
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchPath traverses t until it finds the longest key matching
|
||||
// remainingPath, and returns its node.
|
||||
func (t *vhostTrie) matchPath(remainingPath string) *vhostTrie {
|
||||
var longestMatch *vhostTrie
|
||||
for len(remainingPath) > 0 {
|
||||
ch := string(remainingPath[0])
|
||||
next, ok := t.edges[ch]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if next.site != nil {
|
||||
longestMatch = next
|
||||
}
|
||||
t = next
|
||||
remainingPath = remainingPath[1:]
|
||||
}
|
||||
return longestMatch
|
||||
}
|
||||
|
||||
// splitHostPath separates host from path in key.
|
||||
func (t *vhostTrie) splitHostPath(key string) (host, path string) {
|
||||
parts := strings.SplitN(key, "/", 2)
|
||||
host, path = strings.ToLower(parts[0]), "/"
|
||||
if len(parts) > 1 {
|
||||
path += parts[1]
|
||||
}
|
||||
// strip out the port (if present) from the host, since
|
||||
// each port has its own socket, and each socket has its
|
||||
// own listener, and each listener has its own server
|
||||
// instance, and each server instance has its own vhosts.
|
||||
// removing the port is a simple way to standardize so
|
||||
// when requests come in, we can be sure to get a match.
|
||||
hostname, _, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
host = hostname
|
||||
}
|
||||
return
|
||||
}
|
||||
141
caddyhttp/httpserver/vhosttrie_test.go
Normal file
141
caddyhttp/httpserver/vhosttrie_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVHostTrie(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"example",
|
||||
"example.com",
|
||||
"*.example.com",
|
||||
"example.com/foo",
|
||||
"example.com/foo/bar",
|
||||
"*.example.com/test",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"not-in-trie.com", false, "", "/"},
|
||||
{"example", true, "example", "/"},
|
||||
{"example.com", true, "example.com", "/"},
|
||||
{"example.com/test", true, "example.com", "/"},
|
||||
{"example.com/foo", true, "example.com/foo", "/foo"},
|
||||
{"example.com/foo/", true, "example.com/foo", "/foo"},
|
||||
{"EXAMPLE.COM/foo", true, "example.com/foo", "/foo"},
|
||||
{"EXAMPLE.COM/Foo", true, "example.com", "/"},
|
||||
{"example.com/foo/bar", true, "example.com/foo/bar", "/foo/bar"},
|
||||
{"example.com/foo/bar/baz", true, "example.com/foo/bar", "/foo/bar"},
|
||||
{"example.com/foo/other", true, "example.com/foo", "/foo"},
|
||||
{"foo.example.com", true, "*.example.com", "/"},
|
||||
{"foo.example.com/else", true, "*.example.com", "/"},
|
||||
}, false)
|
||||
}
|
||||
|
||||
func TestVHostTrieWildcard1(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"example.com",
|
||||
"",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"not-in-trie.com", true, "", "/"},
|
||||
{"example.com", true, "example.com", "/"},
|
||||
{"example.com/foo", true, "example.com", "/"},
|
||||
{"not-in-trie.com/asdf", true, "", "/"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func TestVHostTrieWildcard2(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"0.0.0.0/asdf",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"example.com/asdf/foo", true, "0.0.0.0/asdf", "/asdf"},
|
||||
{"example.com/foo", false, "", "/"},
|
||||
{"host/asdf", true, "0.0.0.0/asdf", "/asdf"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func TestVHostTrieWildcard3(t *testing.T) {
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"*/foo",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"example.com/foo", true, "*/foo", "/foo"},
|
||||
{"example.com", false, "", "/"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func TestVHostTriePort(t *testing.T) {
|
||||
// Make sure port is stripped out
|
||||
trie := newVHostTrie()
|
||||
populateTestTrie(trie, []string{
|
||||
"example.com:1234",
|
||||
})
|
||||
assertTestTrie(t, trie, []vhostTrieTest{
|
||||
{"example.com/foo", true, "example.com:1234", "/"},
|
||||
}, true)
|
||||
}
|
||||
|
||||
func populateTestTrie(trie *vhostTrie, keys []string) {
|
||||
for _, key := range keys {
|
||||
// we wrap this in a func, passing in the key, otherwise the
|
||||
// handler always writes the last key to the response, even
|
||||
// if the handler is actually from one of the earlier keys.
|
||||
func(key string) {
|
||||
site := &SiteConfig{
|
||||
middlewareChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Write([]byte(key))
|
||||
return 0, nil
|
||||
}),
|
||||
}
|
||||
trie.Insert(key, site)
|
||||
}(key)
|
||||
}
|
||||
}
|
||||
|
||||
type vhostTrieTest struct {
|
||||
query string
|
||||
expectMatch bool
|
||||
expectedKey string
|
||||
matchedPrefix string // the path portion of a key that is expected to be matched
|
||||
}
|
||||
|
||||
func assertTestTrie(t *testing.T, trie *vhostTrie, tests []vhostTrieTest, hasWildcardHosts bool) {
|
||||
for i, test := range tests {
|
||||
site, pathPrefix := trie.Match(test.query)
|
||||
|
||||
if !test.expectMatch {
|
||||
if site != nil {
|
||||
// If not expecting a value, then just make sure we didn't get one
|
||||
t.Errorf("Test %d: Expected no matches, but got %v", i, site)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, we must assert we got a value
|
||||
if site == nil {
|
||||
t.Errorf("Test %d: Expected non-nil return value, but got: %v", i, site)
|
||||
continue
|
||||
}
|
||||
|
||||
// And it must be the correct value
|
||||
resp := httptest.NewRecorder()
|
||||
site.middlewareChain.ServeHTTP(resp, nil)
|
||||
actualHandlerKey := resp.Body.String()
|
||||
if actualHandlerKey != test.expectedKey {
|
||||
t.Errorf("Test %d: Expected match '%s' but matched '%s'",
|
||||
i, test.expectedKey, actualHandlerKey)
|
||||
}
|
||||
|
||||
// The path prefix must also be correct
|
||||
if test.matchedPrefix != pathPrefix {
|
||||
t.Errorf("Test %d: Expected matched path prefix to be '%s', got '%s'",
|
||||
i, test.matchedPrefix, pathPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user