Rename and export some types, other minor changes

This commit is contained in:
Matthew Holt 2019-05-14 14:14:05 -06:00
parent 8ae0d6a509
commit f9d93ead4e
7 changed files with 151 additions and 106 deletions

View file

@ -22,29 +22,31 @@ func init() {
err := caddy2.RegisterModule(caddy2.Module{
Name: "http",
New: func() (interface{}, error) { return new(httpModuleConfig), nil },
New: func() (interface{}, error) { return new(App), nil },
})
if err != nil {
log.Fatal(err)
}
}
type httpModuleConfig struct {
HTTPPort int `json:"http_port"`
HTTPSPort int `json:"https_port"`
GracePeriod caddy2.Duration `json:"grace_period"`
Servers map[string]*httpServerConfig `json:"servers"`
// App is the HTTP app for Caddy.
type App struct {
HTTPPort int `json:"http_port"`
HTTPSPort int `json:"https_port"`
GracePeriod caddy2.Duration `json:"grace_period"`
Servers map[string]*Server `json:"servers"`
servers []*http.Server
}
func (hc *httpModuleConfig) Provision() error {
// Provision sets up the app.
func (hc *App) Provision() error {
for _, srv := range hc.Servers {
err := srv.Routes.setup()
err := srv.Routes.Provision()
if err != nil {
return fmt.Errorf("setting up server routes: %v", err)
}
err = srv.Errors.Routes.setup()
err = srv.Errors.Routes.Provision()
if err != nil {
return fmt.Errorf("setting up server error handling routes: %v", err)
}
@ -53,7 +55,8 @@ func (hc *httpModuleConfig) Provision() error {
return nil
}
func (hc *httpModuleConfig) Validate() error {
// Validate ensures the app's configuration is valid.
func (hc *App) Validate() error {
// each server must use distinct listener addresses
lnAddrs := make(map[string]string)
for srvName, srv := range hc.Servers {
@ -74,7 +77,8 @@ func (hc *httpModuleConfig) Validate() error {
return nil
}
func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
// Start runs the app. It sets up automatic HTTPS if enabled.
func (hc *App) Start(handle caddy2.Handle) error {
err := hc.automaticHTTPS(handle)
if err != nil {
return fmt.Errorf("enabling automatic HTTPS: %v", err)
@ -129,7 +133,7 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
}
// Stop gracefully shuts down the HTTP server.
func (hc *httpModuleConfig) Stop() error {
func (hc *App) Stop() error {
ctx := context.Background()
if hc.GracePeriod > 0 {
var cancel context.CancelFunc
@ -145,7 +149,7 @@ func (hc *httpModuleConfig) Stop() error {
return nil
}
func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
func (hc *App) automaticHTTPS(handle caddy2.Handle) error {
tlsAppIface, err := handle.App("tls")
if err != nil {
return fmt.Errorf("getting tls app: %v", err)
@ -153,7 +157,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
tlsApp := tlsAppIface.(*caddytls.TLS)
lnAddrMap := make(map[string]struct{})
var redirRoutes routeList
var redirRoutes RouteList
for srvName, srv := range hc.Servers {
srv.tlsApp = tlsApp
@ -222,7 +226,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
}
redirTo += "{request.uri}"
redirRoutes = append(redirRoutes, serverRoute{
redirRoutes = append(redirRoutes, ServerRoute{
matchers: []RouteMatcher{
matchProtocol("http"),
matchHost(domains),
@ -255,7 +259,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
}
lnAddrs = append(lnAddrs, addr)
}
hc.Servers["auto_https_redirects"] = &httpServerConfig{
hc.Servers["auto_https_redirects"] = &Server{
Listen: lnAddrs,
Routes: redirRoutes,
DisableAutoHTTPS: true,
@ -265,7 +269,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
return nil
}
func (hc *httpModuleConfig) listenerTaken(network, address string) bool {
func (hc *App) listenerTaken(network, address string) bool {
for _, srv := range hc.Servers {
for _, addr := range srv.Listen {
netw, addrs, err := parseListenAddr(addr)
@ -284,12 +288,13 @@ func (hc *httpModuleConfig) listenerTaken(network, address string) bool {
var defaultALPN = []string{"h2", "http/1.1"}
type httpServerConfig struct {
// Server is an HTTP server.
type Server struct {
Listen []string `json:"listen"`
ReadTimeout caddy2.Duration `json:"read_timeout"`
ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
Routes routeList `json:"routes"`
Routes RouteList `json:"routes"`
Errors httpErrorConfig `json:"errors"`
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"`
DisableAutoHTTPS bool `json:"disable_auto_https"`
@ -299,23 +304,24 @@ type httpServerConfig struct {
}
type httpErrorConfig struct {
Routes routeList `json:"routes"`
// TODO: some way to configure the logging of errors, probably? standardize the logging configuration first.
Routes RouteList `json:"routes"`
// TODO: some way to configure the logging of errors, probably? standardize
// the logging configuration first.
}
// ServeHTTP is the entry point for all HTTP requests.
func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.tlsApp.HandleHTTPChallenge(w, r) {
return
}
// set up the replacer
repl := &Replacer{req: r, resp: w, custom: make(map[string]string)}
repl := NewReplacer(r, w)
ctx := context.WithValue(r.Context(), ReplacerCtxKey, repl)
r = r.WithContext(ctx)
// build and execute the main middleware chain
stack := s.Routes.buildMiddlewareChain(w, r)
stack := s.Routes.BuildHandlerChain(w, r)
err := executeMiddlewareChain(w, r, stack)
if err != nil {
// add the error value to the request context so
@ -328,7 +334,7 @@ func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO: implement a default error handler?
log.Printf("[ERROR] %s", err)
} else {
errStack := s.Errors.Routes.buildMiddlewareChain(w, r)
errStack := s.Errors.Routes.BuildHandlerChain(w, r)
err := executeMiddlewareChain(w, r, errStack)
if err != nil {
// TODO: what should we do if the error handler has an error?
@ -411,6 +417,7 @@ func parseListenAddr(a string) (network string, addrs []string, err error) {
if err != nil {
return
}
host = NewReplacer(nil, nil).Replace(host, "")
ports := strings.SplitN(port, "-", 2)
if len(ports) == 1 {
ports = append(ports, ports[0])
@ -474,9 +481,6 @@ func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
return mrw.ResponseWriterWrapper.Write(b)
}
// ReplacerCtxKey is the context key for the request's replacer.
const ReplacerCtxKey caddy2.CtxKey = "replacer"
const (
// DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = 80

View file

@ -1,6 +1,7 @@
package caddyhttp
import (
"os"
"reflect"
"testing"
)
@ -114,6 +115,11 @@ func TestJoinListenerAddr(t *testing.T) {
}
func TestParseListenerAddr(t *testing.T) {
hostname, err := os.Hostname()
if err != nil {
t.Fatalf("Cannot ascertain system hostname: %v", err)
}
for i, tc := range []struct {
input string
expectNetwork string
@ -170,6 +176,11 @@ func TestParseListenerAddr(t *testing.T) {
expectNetwork: "tcp",
expectAddrs: []string{"localhost:0"},
},
{
input: "{system.hostname}:0",
expectNetwork: "tcp",
expectAddrs: []string{hostname + ":0"},
},
} {
actualNetwork, actualAddrs, err := parseListenAddr(tc.input)
if tc.expectErr && err == nil {

View file

@ -205,7 +205,7 @@ func TestPathREMatcher(t *testing.T) {
// set up the fake request and its Replacer
req := &http.Request{URL: &url.URL{Path: tc.input}}
repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)}
repl := NewReplacer(req, httptest.NewRecorder())
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
req = req.WithContext(ctx)
@ -322,7 +322,7 @@ func TestHeaderREMatcher(t *testing.T) {
// set up the fake request and its Replacer
req := &http.Request{Header: tc.input, URL: new(url.URL)}
repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)}
repl := NewReplacer(req, httptest.NewRecorder())
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
req = req.WithContext(ctx)

View file

@ -5,18 +5,31 @@ import (
"net/http"
"os"
"strings"
"bitbucket.org/lightcodelabs/caddy2"
)
// Replacer can replace values in strings based
// on a request and/or response writer. The zero
// Replacer is not valid; it must be initialized
// within this package.
// Replacer is not valid; use NewReplacer() to
// initialize one.
type Replacer struct {
req *http.Request
resp http.ResponseWriter
custom map[string]string
}
// NewReplacer makes a new Replacer, initializing all necessary
// fields. The request and response writer are optional, but
// necessary for most replacements to work.
func NewReplacer(req *http.Request, rw http.ResponseWriter) *Replacer {
return &Replacer{
req: req,
resp: rw,
custom: make(map[string]string),
}
}
// Map sets a custom variable mapping to a value.
func (r *Replacer) Map(variable, value string) {
r.custom[variable] = value
@ -48,28 +61,6 @@ func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) st
func (r *Replacer) defaults() map[string]string {
m := map[string]string{
"request.host": func() string {
host, _, err := net.SplitHostPort(r.req.Host)
if err != nil {
return r.req.Host // OK; there probably was no port
}
return host
}(),
"request.hostport": r.req.Host, // may include both host and port
"request.method": r.req.Method,
"request.port": func() string {
// if there is no port, there will be an error; in
// that case, port is the empty string anyway
_, port, _ := net.SplitHostPort(r.req.Host)
return port
}(),
"request.scheme": func() string {
if r.req.TLS != nil {
return "https"
}
return "http"
}(),
"request.uri": r.req.URL.RequestURI(),
"system.hostname": func() string {
// OK if there is an error; just return empty string
name, _ := os.Hostname()
@ -77,22 +68,51 @@ func (r *Replacer) defaults() map[string]string {
}(),
}
// TODO: why should header fields, cookies, and query params get special treatment like this?
// maybe they should be scoped by words like "request.header." just like everything else.
for field, vals := range r.req.Header {
m[">"+strings.ToLower(field)] = strings.Join(vals, ",")
}
for field, vals := range r.resp.Header() {
m["<"+strings.ToLower(field)] = strings.Join(vals, ",")
}
for _, cookie := range r.req.Cookies() {
m["~"+cookie.Name] = cookie.Value
}
for param, vals := range r.req.URL.Query() {
m["?"+param] = strings.Join(vals, ",")
if r.req != nil {
m["request.host"] = func() string {
host, _, err := net.SplitHostPort(r.req.Host)
if err != nil {
return r.req.Host // OK; there probably was no port
}
return host
}()
m["request.hostport"] = r.req.Host // may include both host and port
m["request.method"] = r.req.Method
m["request.port"] = func() string {
// if there is no port, there will be an error; in
// that case, port is the empty string anyway
_, port, _ := net.SplitHostPort(r.req.Host)
return port
}()
m["request.scheme"] = func() string {
if r.req.TLS != nil {
return "https"
}
return "http"
}()
m["request.uri"] = r.req.URL.RequestURI()
m["request.uri.path"] = r.req.URL.Path
// TODO: why should header fields, cookies, and query params get special treatment like this?
// maybe they should be scoped by words like "request.header." just like everything else.
for field, vals := range r.req.Header {
m[">"+strings.ToLower(field)] = strings.Join(vals, ",")
}
for field, vals := range r.resp.Header() {
m["<"+strings.ToLower(field)] = strings.Join(vals, ",")
}
for _, cookie := range r.req.Cookies() {
m["~"+cookie.Name] = cookie.Value
}
for param, vals := range r.req.URL.Query() {
m["?"+param] = strings.Join(vals, ",")
}
}
return m
}
const phOpen, phClose = "{", "}"
// ReplacerCtxKey is the context key for the request's replacer.
const ReplacerCtxKey caddy2.CtxKey = "replacer"

View file

@ -8,7 +8,10 @@ import (
"bitbucket.org/lightcodelabs/caddy2"
)
type serverRoute struct {
// ServerRoute represents a set of matching rules,
// middlewares, and a responder for handling HTTP
// requests.
type ServerRoute struct {
Matchers map[string]json.RawMessage `json:"match"`
Apply []json.RawMessage `json:"apply"`
Respond json.RawMessage `json:"respond"`
@ -21,9 +24,49 @@ type serverRoute struct {
responder Handler
}
type routeList []serverRoute
// RouteList is a list of server routes that can
// create a middleware chain.
type RouteList []ServerRoute
func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Request) Handler {
// Provision sets up all the routes by loading the modules.
func (routes RouteList) Provision() error {
for i, route := range routes {
// matchers
for modName, rawMsg := range route.Matchers {
val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
}
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
// middleware
for j, rawMsg := range route.Apply {
mid, err := caddy2.LoadModuleInline("middleware", "http.middleware", rawMsg)
if err != nil {
return fmt.Errorf("loading middleware module in position %d: %v", j, err)
}
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
}
routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
// responder
if route.Respond != nil {
resp, err := caddy2.LoadModuleInline("responder", "http.responders", route.Respond)
if err != nil {
return fmt.Errorf("loading responder module: %v", err)
}
routes[i].responder = resp.(Handler)
}
routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
}
return nil
}
// BuildHandlerChain creates a chain of handlers by
// applying all the matching routes.
func (routes RouteList) BuildHandlerChain(w http.ResponseWriter, r *http.Request) Handler {
if len(routes) == 0 {
return emptyHandler
}
@ -68,38 +111,3 @@ routeLoop:
return stack
}
func (routes routeList) setup() error {
for i, route := range routes {
// matchers
for modName, rawMsg := range route.Matchers {
val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
}
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
// middleware
for j, rawMsg := range route.Apply {
mid, err := caddy2.LoadModuleInline("middleware", "http.middleware", rawMsg)
if err != nil {
return fmt.Errorf("loading middleware module in position %d: %v", j, err)
}
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
}
routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
// responder
if route.Respond != nil {
resp, err := caddy2.LoadModuleInline("responder", "http.responders", route.Respond)
if err != nil {
return fmt.Errorf("loading responder module: %v", err)
}
routes[i].responder = resp.(Handler)
}
routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
}
return nil
}

View file

@ -15,7 +15,6 @@ func init() {
}
// Static implements a simple responder for static responses.
// It is Caddy's default responder. TODO: Or is it?
type Static struct {
StatusCode int `json:"status_code"`
Headers http.Header `json:"headers"`

View file

@ -85,6 +85,9 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(handle caddy2.Handle) error {
NextProtos: cp.ALPN,
PreferServerCipherSuites: true,
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
// TODO: Must fix https://github.com/mholt/caddy/issues/2588
// (allow customizing the selection of a very specific certificate
// based on the ClientHelloInfo)
cfgTpl, err := tlsApp.getConfigForName(hello.ServerName)
if err != nil {
return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err)