From 2eb35933271d5b2ef1ca29aaa8f00f6748c56424 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 4 May 2019 13:21:20 -0600 Subject: Begin implementing HTTP replacer and static responder --- modules/caddyhttp/caddyhttp.go | 25 ++++++---- modules/caddyhttp/matchers.go | 14 +++--- modules/caddyhttp/replacer.go | 75 ++++++++++++++++++++++++++++++ modules/caddyhttp/staticresp/staticresp.go | 57 +++++++++++++++++++++++ 4 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 modules/caddyhttp/replacer.go create mode 100644 modules/caddyhttp/staticresp/staticresp.go (limited to 'modules') diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index de62b79..0731fea 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -167,14 +167,15 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error { var defaultALPN = []string{"h2", "http/1.1"} type httpServerConfig 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"` - Errors httpErrorConfig `json:"errors"` - TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"` - DisableAutoHTTPS bool `json:"disable_auto_https"` + 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"` + Errors httpErrorConfig `json:"errors"` + TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"` + DisableAutoHTTPS bool `json:"disable_auto_https"` + DisableAutoHTTPSRedir bool `json:"disable_auto_https_redir"` tlsApp *caddytls.TLS } @@ -190,6 +191,12 @@ func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // set up the replacer + repl := &Replacer{req: r, resp: w, custom: make(map[string]string)} + ctx := context.WithValue(r.Context(), ReplacerCtxKey, repl) + r = r.WithContext(ctx) + + // build and execute the main middleware chain stack := s.Routes.buildMiddlewareChain(w, r) err := executeMiddlewareChain(w, r, stack) if err != nil { @@ -329,5 +336,7 @@ func (mrw middlewareResponseWriter) Write(b []byte) (int, error) { return mrw.ResponseWriterWrapper.Write(b) } +const ReplacerCtxKey caddy2.CtxKey = "replacer" + // Interface guards var _ HTTPInterfaces = middlewareResponseWriter{} diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 21cc19f..731832b 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -139,11 +139,11 @@ func (m matchHeader) Match(r *http.Request) bool { // Interface guards var ( - _ RouteMatcher = matchHost{} - _ RouteMatcher = matchPath{} - _ RouteMatcher = matchMethod{} - _ RouteMatcher = matchQuery{} - _ RouteMatcher = matchHeader{} - _ RouteMatcher = new(matchProtocol) - _ RouteMatcher = new(matchScript) + _ RouteMatcher = (*matchHost)(nil) + _ RouteMatcher = (*matchPath)(nil) + _ RouteMatcher = (*matchMethod)(nil) + _ RouteMatcher = (*matchQuery)(nil) + _ RouteMatcher = (*matchHeader)(nil) + _ RouteMatcher = (*matchProtocol)(nil) + _ RouteMatcher = (*matchScript)(nil) ) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go new file mode 100644 index 0000000..6a2ecd1 --- /dev/null +++ b/modules/caddyhttp/replacer.go @@ -0,0 +1,75 @@ +package caddyhttp + +import ( + "net/http" + "strings" +) + +type Replacer struct { + req *http.Request + resp http.ResponseWriter + custom map[string]string +} + +// Map sets a custom variable mapping to a value. +func (r *Replacer) Map(variable, value string) { + r.custom[variable] = value +} + +// Replace replaces placeholders in input with the value. If +// the value is empty string, the placeholder is substituted +// with the value empty. +func (r *Replacer) Replace(input, empty string) string { + if !strings.Contains(input, phOpen) { + return input + } + + input = r.replaceAll(input, empty, r.defaults()) + input = r.replaceAll(input, empty, r.custom) + + return input +} + +func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) string { + for key, val := range mapping { + if val == "" { + val = empty + } + input = strings.ReplaceAll(input, phOpen+key+phClose, val) + } + return input +} + +func (r *Replacer) defaults() map[string]string { + m := map[string]string{ + "host": r.req.Host, + "method": r.req.Method, + "scheme": func() string { + if r.req.TLS != nil { + return "https" + } + return "http" + }(), + "uri": r.req.URL.RequestURI(), + } + + 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 = "{", "}" diff --git a/modules/caddyhttp/staticresp/staticresp.go b/modules/caddyhttp/staticresp/staticresp.go new file mode 100644 index 0000000..e169133 --- /dev/null +++ b/modules/caddyhttp/staticresp/staticresp.go @@ -0,0 +1,57 @@ +package staticresp + +import ( + "fmt" + "net/http" + + "bitbucket.org/lightcodelabs/caddy2" + "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" +) + +func init() { + caddy2.RegisterModule(caddy2.Module{ + Name: "http.responders.static", + New: func() (interface{}, error) { return new(Static), nil }, + }) +} + +// Static implements a simple responder for static responses. +type Static struct { + StatusCode int `json:"status_code"` + Headers map[string][]string `json:"headers"` + Body string `json:"body"` + Close bool `json:"close"` +} + +func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { + repl := r.Context().Value(caddyhttp.ReplacerCtxKey).(*caddyhttp.Replacer) + + // close the connection + r.Close = s.Close + + // set all headers, with replacements + for field, vals := range s.Headers { + field = repl.Replace(field, "") + for i := range vals { + vals[i] = repl.Replace(vals[i], "") + } + w.Header()[field] = vals + } + + // write the headers with a status code + statusCode := s.StatusCode + if statusCode == 0 { + statusCode = http.StatusOK + } + w.WriteHeader(statusCode) + + // write the response body, with replacements + if s.Body != "" { + fmt.Fprint(w, repl.Replace(s.Body, "")) + } + + return nil +} + +// Interface guard +var _ caddyhttp.Handler = (*Static)(nil) -- cgit v1.2.3