14 changed files with 994 additions and 258 deletions
@ -1,119 +1,83 @@ |
|||||
package caddyhttp |
package caddyhttp |
||||
|
|
||||
import ( |
import ( |
||||
|
"fmt" |
||||
"net" |
"net" |
||||
"net/http" |
"net/http" |
||||
"os" |
"path" |
||||
"strings" |
"strings" |
||||
|
|
||||
"bitbucket.org/lightcodelabs/caddy2" |
"bitbucket.org/lightcodelabs/caddy2" |
||||
) |
) |
||||
|
|
||||
// Replacer can replace values in strings based
|
// TODO: A simple way to format or escape or encode each value would be nice
|
||||
// on a request and/or response writer. The zero
|
// ... TODO: Should we just use templates? :-/ yeesh...
|
||||
// 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 |
|
||||
} |
|
||||
|
|
||||
// Replace replaces placeholders in input with the value. If
|
func newReplacer(req *http.Request, w http.ResponseWriter) caddy2.Replacer { |
||||
// the value is empty string, the placeholder is substituted
|
repl := caddy2.NewReplacer() |
||||
// 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 { |
httpVars := func() map[string]string { |
||||
for key, val := range mapping { |
m := make(map[string]string) |
||||
if val == "" { |
if req != nil { |
||||
val = empty |
m["http.request.host"] = func() string { |
||||
} |
host, _, err := net.SplitHostPort(req.Host) |
||||
input = strings.ReplaceAll(input, phOpen+key+phClose, val) |
|
||||
} |
|
||||
return input |
|
||||
} |
|
||||
|
|
||||
func (r *Replacer) defaults() map[string]string { |
|
||||
m := map[string]string{ |
|
||||
"system.hostname": func() string { |
|
||||
// OK if there is an error; just return empty string
|
|
||||
name, _ := os.Hostname() |
|
||||
return name |
|
||||
}(), |
|
||||
} |
|
||||
|
|
||||
if r.req != nil { |
|
||||
m["request.host"] = func() string { |
|
||||
host, _, err := net.SplitHostPort(r.req.Host) |
|
||||
if err != nil { |
if err != nil { |
||||
return r.req.Host // OK; there probably was no port
|
return req.Host // OK; there probably was no port
|
||||
} |
} |
||||
return host |
return host |
||||
}() |
}() |
||||
m["request.hostport"] = r.req.Host // may include both host and port
|
m["http.request.hostport"] = req.Host // may include both host and port
|
||||
m["request.method"] = r.req.Method |
m["http.request.method"] = req.Method |
||||
m["request.port"] = func() string { |
m["http.request.port"] = func() string { |
||||
// if there is no port, there will be an error; in
|
// if there is no port, there will be an error; in
|
||||
// that case, port is the empty string anyway
|
// that case, port is the empty string anyway
|
||||
_, port, _ := net.SplitHostPort(r.req.Host) |
_, port, _ := net.SplitHostPort(req.Host) |
||||
return port |
return port |
||||
}() |
}() |
||||
m["request.scheme"] = func() string { |
m["http.request.scheme"] = func() string { |
||||
if r.req.TLS != nil { |
if req.TLS != nil { |
||||
return "https" |
return "https" |
||||
} |
} |
||||
return "http" |
return "http" |
||||
}() |
}() |
||||
m["request.uri"] = r.req.URL.RequestURI() |
m["http.request.uri"] = req.URL.RequestURI() |
||||
m["request.uri.path"] = r.req.URL.Path |
m["http.request.uri.path"] = req.URL.Path |
||||
|
m["http.request.uri.path.file"] = func() string { |
||||
|
_, file := path.Split(req.URL.Path) |
||||
|
return file |
||||
|
}() |
||||
|
m["http.request.uri.path.dir"] = func() string { |
||||
|
dir, _ := path.Split(req.URL.Path) |
||||
|
return dir |
||||
|
}() |
||||
|
|
||||
for field, vals := range r.req.Header { |
for field, vals := range req.Header { |
||||
m["request.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
m["http.request.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
||||
} |
} |
||||
for _, cookie := range r.req.Cookies() { |
for _, cookie := range req.Cookies() { |
||||
m["request.cookie."+cookie.Name] = cookie.Value |
m["http.request.cookie."+cookie.Name] = cookie.Value |
||||
} |
} |
||||
for param, vals := range r.req.URL.Query() { |
for param, vals := range req.URL.Query() { |
||||
m["request.uri.query."+param] = strings.Join(vals, ",") |
m["http.request.uri.query."+param] = strings.Join(vals, ",") |
||||
|
} |
||||
|
|
||||
|
hostLabels := strings.Split(req.Host, ".") |
||||
|
for i, label := range hostLabels { |
||||
|
key := fmt.Sprintf("http.request.host.labels.%d", len(hostLabels)-i-1) |
||||
|
m[key] = label |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
if r.resp != nil { |
if w != nil { |
||||
for field, vals := range r.resp.Header() { |
for field, vals := range w.Header() { |
||||
m["response.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
m["http.response.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
return m |
return m |
||||
} |
} |
||||
|
|
||||
const phOpen, phClose = "{", "}" |
repl.Map(httpVars) |
||||
|
|
||||
// ReplacerCtxKey is the context key for the request's replacer.
|
return repl |
||||
const ReplacerCtxKey caddy2.CtxKey = "replacer" |
} |
||||
|
@ -0,0 +1,91 @@ |
|||||
|
package caddyhttp |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"log" |
||||
|
"net/http" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
"bitbucket.org/lightcodelabs/caddy2/modules/caddytls" |
||||
|
) |
||||
|
|
||||
|
// 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"` |
||||
|
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"` |
||||
|
MaxRehandles int `json:"max_rehandles"` |
||||
|
|
||||
|
tlsApp *caddytls.TLS |
||||
|
} |
||||
|
|
||||
|
// ServeHTTP is the entry point for all HTTP requests.
|
||||
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
|
if s.tlsApp.HandleHTTPChallenge(w, r) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// set up the replacer
|
||||
|
repl := newReplacer(r, w) |
||||
|
ctx := context.WithValue(r.Context(), caddy2.ReplacerCtxKey, repl) |
||||
|
ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this
|
||||
|
r = r.WithContext(ctx) |
||||
|
|
||||
|
// build and execute the main handler chain
|
||||
|
stack := s.Routes.BuildCompositeRoute(w, r) |
||||
|
err := s.executeCompositeRoute(w, r, stack) |
||||
|
if err != nil { |
||||
|
// add the error value to the request context so
|
||||
|
// it can be accessed by error handlers
|
||||
|
c := context.WithValue(r.Context(), ErrorCtxKey, err) |
||||
|
r = r.WithContext(c) |
||||
|
// TODO: add error values to Replacer
|
||||
|
|
||||
|
if len(s.Errors.Routes) == 0 { |
||||
|
// TODO: implement a default error handler?
|
||||
|
log.Printf("[ERROR] %s", err) |
||||
|
} else { |
||||
|
errStack := s.Errors.Routes.BuildCompositeRoute(w, r) |
||||
|
err := s.executeCompositeRoute(w, r, errStack) |
||||
|
if err != nil { |
||||
|
// TODO: what should we do if the error handler has an error?
|
||||
|
log.Printf("[ERROR] handling error: %v", err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// executeCompositeRoute executes stack with w and r. This function handles
|
||||
|
// the special ErrRehandle error value, which reprocesses requests through
|
||||
|
// the stack again. Any error value returned from this function would be an
|
||||
|
// actual error that needs to be handled.
|
||||
|
func (s *Server) executeCompositeRoute(w http.ResponseWriter, r *http.Request, stack Handler) error { |
||||
|
var err error |
||||
|
for i := -1; i <= s.MaxRehandles; i++ { |
||||
|
// we started the counter at -1 because we
|
||||
|
// always want to run this at least once
|
||||
|
err = stack.ServeHTTP(w, r) |
||||
|
if err != ErrRehandle { |
||||
|
break |
||||
|
} |
||||
|
if i >= s.MaxRehandles-1 { |
||||
|
return fmt.Errorf("too many rehandles") |
||||
|
} |
||||
|
} |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
type httpErrorConfig struct { |
||||
|
Routes RouteList `json:"routes"` |
||||
|
// TODO: some way to configure the logging of errors, probably? standardize
|
||||
|
// the logging configuration first.
|
||||
|
} |
||||
|
|
||||
|
// TableCtxKey is the context key for the request's variable table.
|
||||
|
const TableCtxKey caddy2.CtxKey = "table" |
@ -0,0 +1,205 @@ |
|||||
|
package staticfiles |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
) |
||||
|
|
||||
|
// Browse configures directory browsing.
|
||||
|
type Browse struct { |
||||
|
} |
||||
|
|
||||
|
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
|
// If so, control is handed over to ServeListing.
|
||||
|
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) error { |
||||
|
// TODO: convert this handler
|
||||
|
return nil |
||||
|
|
||||
|
// // Browse works on existing directories; delegate everything else
|
||||
|
// requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path)
|
||||
|
// if err != nil {
|
||||
|
// switch {
|
||||
|
// case os.IsPermission(err):
|
||||
|
// return http.StatusForbidden, err
|
||||
|
// case os.IsExist(err):
|
||||
|
// return http.StatusNotFound, err
|
||||
|
// default:
|
||||
|
// return b.Next.ServeHTTP(w, r)
|
||||
|
// }
|
||||
|
// }
|
||||
|
// defer requestedFilepath.Close()
|
||||
|
|
||||
|
// info, err := requestedFilepath.Stat()
|
||||
|
// if err != nil {
|
||||
|
// switch {
|
||||
|
// case os.IsPermission(err):
|
||||
|
// return http.StatusForbidden, err
|
||||
|
// case os.IsExist(err):
|
||||
|
// return http.StatusGone, err
|
||||
|
// default:
|
||||
|
// return b.Next.ServeHTTP(w, r)
|
||||
|
// }
|
||||
|
// }
|
||||
|
// if !info.IsDir() {
|
||||
|
// return b.Next.ServeHTTP(w, r)
|
||||
|
// }
|
||||
|
|
||||
|
// // Do not reply to anything else because it might be nonsensical
|
||||
|
// switch r.Method {
|
||||
|
// case http.MethodGet, http.MethodHead:
|
||||
|
// // proceed, noop
|
||||
|
// case "PROPFIND", http.MethodOptions:
|
||||
|
// return http.StatusNotImplemented, nil
|
||||
|
// default:
|
||||
|
// return b.Next.ServeHTTP(w, r)
|
||||
|
// }
|
||||
|
|
||||
|
// // Browsing navigation gets messed up if browsing a directory
|
||||
|
// // that doesn't end in "/" (which it should, anyway)
|
||||
|
// u := *r.URL
|
||||
|
// if u.Path == "" {
|
||||
|
// u.Path = "/"
|
||||
|
// }
|
||||
|
// if u.Path[len(u.Path)-1] != '/' {
|
||||
|
// u.Path += "/"
|
||||
|
// http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
|
||||
|
// return http.StatusMovedPermanently, nil
|
||||
|
// }
|
||||
|
|
||||
|
// return b.ServeListing(w, r, requestedFilepath, bc)
|
||||
|
} |
||||
|
|
||||
|
// func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) {
|
||||
|
// files, err := requestedFilepath.Readdir(-1)
|
||||
|
// if err != nil {
|
||||
|
// return nil, false, err
|
||||
|
// }
|
||||
|
|
||||
|
// // Determine if user can browse up another folder
|
||||
|
// var canGoUp bool
|
||||
|
// curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
|
||||
|
// for _, other := range b.Configs {
|
||||
|
// if strings.HasPrefix(curPathDir, other.PathScope) {
|
||||
|
// canGoUp = true
|
||||
|
// break
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// // Assemble listing of directory contents
|
||||
|
// listing, hasIndex := directoryListing(files, canGoUp, urlPath, config)
|
||||
|
|
||||
|
// return &listing, hasIndex, nil
|
||||
|
// }
|
||||
|
|
||||
|
// // handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||
|
// // and reads 'limit' if given. The latter is 0 if not given.
|
||||
|
// //
|
||||
|
// // This sets Cookies.
|
||||
|
// func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
|
||||
|
// sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
|
||||
|
|
||||
|
// // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
|
||||
|
// switch sort {
|
||||
|
// case "":
|
||||
|
// sort = sortByNameDirFirst
|
||||
|
// if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
|
||||
|
// sort = sortCookie.Value
|
||||
|
// }
|
||||
|
// case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||
|
// http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
|
||||
|
// }
|
||||
|
|
||||
|
// switch order {
|
||||
|
// case "":
|
||||
|
// order = "asc"
|
||||
|
// if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
||||
|
// order = orderCookie.Value
|
||||
|
// }
|
||||
|
// case "asc", "desc":
|
||||
|
// http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
|
||||
|
// }
|
||||
|
|
||||
|
// if limitQuery != "" {
|
||||
|
// limit, err = strconv.Atoi(limitQuery)
|
||||
|
// if err != nil { // if the 'limit' query can't be interpreted as a number, return err
|
||||
|
// return
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
// return
|
||||
|
// }
|
||||
|
|
||||
|
// // ServeListing returns a formatted view of 'requestedFilepath' contents'.
|
||||
|
// func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
|
||||
|
// listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc)
|
||||
|
// if err != nil {
|
||||
|
// switch {
|
||||
|
// case os.IsPermission(err):
|
||||
|
// return http.StatusForbidden, err
|
||||
|
// case os.IsExist(err):
|
||||
|
// return http.StatusGone, err
|
||||
|
// default:
|
||||
|
// return http.StatusInternalServerError, err
|
||||
|
// }
|
||||
|
// }
|
||||
|
// if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
|
||||
|
// return b.Next.ServeHTTP(w, r)
|
||||
|
// }
|
||||
|
// listing.Context = httpserver.Context{
|
||||
|
// Root: bc.Fs.Root,
|
||||
|
// Req: r,
|
||||
|
// URL: r.URL,
|
||||
|
// }
|
||||
|
// listing.User = bc.Variables
|
||||
|
|
||||
|
// // Copy the query values into the Listing struct
|
||||
|
// var limit int
|
||||
|
// listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
|
||||
|
// if err != nil {
|
||||
|
// return http.StatusBadRequest, err
|
||||
|
// }
|
||||
|
|
||||
|
// listing.applySort()
|
||||
|
|
||||
|
// if limit > 0 && limit <= len(listing.Items) {
|
||||
|
// listing.Items = listing.Items[:limit]
|
||||
|
// listing.ItemsLimitedTo = limit
|
||||
|
// }
|
||||
|
|
||||
|
// var buf *bytes.Buffer
|
||||
|
// acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
|
||||
|
// switch {
|
||||
|
// case strings.Contains(acceptHeader, "application/json"):
|
||||
|
// if buf, err = b.formatAsJSON(listing, bc); err != nil {
|
||||
|
// return http.StatusInternalServerError, err
|
||||
|
// }
|
||||
|
// w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
|
||||
|
// default: // There's no 'application/json' in the 'Accept' header; browse normally
|
||||
|
// if buf, err = b.formatAsHTML(listing, bc); err != nil {
|
||||
|
// return http.StatusInternalServerError, err
|
||||
|
// }
|
||||
|
// w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
|
||||
|
// }
|
||||
|
|
||||
|
// _, _ = buf.WriteTo(w)
|
||||
|
|
||||
|
// return http.StatusOK, nil
|
||||
|
// }
|
||||
|
|
||||
|
// func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||
|
// marsh, err := json.Marshal(listing.Items)
|
||||
|
// if err != nil {
|
||||
|
// return nil, err
|
||||
|
// }
|
||||
|
|
||||
|
// buf := new(bytes.Buffer)
|
||||
|
// _, err = buf.Write(marsh)
|
||||
|
// return buf, err
|
||||
|
// }
|
||||
|
|
||||
|
// func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||
|
// buf := new(bytes.Buffer)
|
||||
|
// err := bc.Template.Execute(buf, listing)
|
||||
|
// return buf, err
|
||||
|
// }
|
@ -0,0 +1,54 @@ |
|||||
|
package staticfiles |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
"bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "http.matchers.file", |
||||
|
New: func() (interface{}, error) { return new(FileMatcher), nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TODO: Not sure how to do this well; we'd need the ability to
|
||||
|
// hide files, etc...
|
||||
|
// TODO: Also consider a feature to match directory that
|
||||
|
// contains a certain filename (use filepath.Glob), useful
|
||||
|
// if wanting to map directory-URI requests where the dir
|
||||
|
// has index.php to PHP backends, for example (although this
|
||||
|
// can effectively be done with rehandling already)
|
||||
|
type FileMatcher struct { |
||||
|
Root string `json:"root"` |
||||
|
Path string `json:"path"` |
||||
|
Flags []string `json:"flags"` |
||||
|
} |
||||
|
|
||||
|
func (m FileMatcher) Match(r *http.Request) bool { |
||||
|
// TODO: sanitize path
|
||||
|
fullPath := filepath.Join(m.Root, m.Path) |
||||
|
var match bool |
||||
|
if len(m.Flags) > 0 { |
||||
|
match = true |
||||
|
fi, err := os.Stat(fullPath) |
||||
|
for _, f := range m.Flags { |
||||
|
switch f { |
||||
|
case "EXIST": |
||||
|
match = match && os.IsNotExist(err) |
||||
|
case "DIR": |
||||
|
match = match && err == nil && fi.IsDir() |
||||
|
default: |
||||
|
match = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return match |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil) |
@ -0,0 +1,41 @@ |
|||||
|
package caddyhttp |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
|
||||
|
"bitbucket.org/lightcodelabs/caddy2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "http.middleware.table", |
||||
|
New: func() (interface{}, error) { return new(tableMiddleware), nil }, |
||||
|
}) |
||||
|
|
||||
|
caddy2.RegisterModule(caddy2.Module{ |
||||
|
Name: "http.matchers.table", |
||||
|
New: func() (interface{}, error) { return new(tableMatcher), nil }, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
type tableMiddleware struct { |
||||
|
} |
||||
|
|
||||
|
func (t tableMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { |
||||
|
// tbl := r.Context().Value(TableCtxKey).(map[string]interface{})
|
||||
|
|
||||
|
// TODO: implement this...
|
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
type tableMatcher struct { |
||||
|
} |
||||
|
|
||||
|
func (m tableMatcher) Match(r *http.Request) bool { |
||||
|
return false // TODO: implement
|
||||
|
} |
||||
|
|
||||
|
// Interface guards
|
||||
|
var _ MiddlewareHandler = (*tableMiddleware)(nil) |
||||
|
var _ RequestMatcher = (*tableMatcher)(nil) |
@ -0,0 +1,104 @@ |
|||||
|
package caddy2 |
||||
|
|
||||
|
import ( |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"runtime" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
// Replacer can replace values in strings.
|
||||
|
type Replacer interface { |
||||
|
Set(variable, value string) |
||||
|
Delete(variable string) |
||||
|
Map(func() map[string]string) |
||||
|
ReplaceAll(input, empty string) string |
||||
|
} |
||||
|
|
||||
|
// NewReplacer returns a new Replacer.
|
||||
|
func NewReplacer() Replacer { |
||||
|
rep := &replacer{ |
||||
|
static: make(map[string]string), |
||||
|
} |
||||
|
rep.providers = []ReplacementsFunc{ |
||||
|
defaultReplacements, |
||||
|
func() map[string]string { return rep.static }, |
||||
|
} |
||||
|
return rep |
||||
|
} |
||||
|
|
||||
|
type replacer struct { |
||||
|
providers []ReplacementsFunc |
||||
|
static map[string]string |
||||
|
} |
||||
|
|
||||
|
// Map augments the map of replacements with those returned
|
||||
|
// by the given replacements function. The function is only
|
||||
|
// executed at replace-time.
|
||||
|
func (r *replacer) Map(replacements func() map[string]string) { |
||||
|
r.providers = append(r.providers, replacements) |
||||
|
} |
||||
|
|
||||
|
// Set sets a custom variable to a static value.
|
||||
|
func (r *replacer) Set(variable, value string) { |
||||
|
r.static[variable] = value |
||||
|
} |
||||
|
|
||||
|
// Delete removes a variable with a static value
|
||||
|
// that was created using Set.
|
||||
|
func (r *replacer) Delete(variable string) { |
||||
|
delete(r.static, variable) |
||||
|
} |
||||
|
|
||||
|
// ReplaceAll replaces placeholders in input with their values.
|
||||
|
// Values that are empty string will be substituted with the
|
||||
|
// empty parameter.
|
||||
|
func (r *replacer) ReplaceAll(input, empty string) string { |
||||
|
if !strings.Contains(input, phOpen) { |
||||
|
return input |
||||
|
} |
||||
|
for _, replacements := range r.providers { |
||||
|
for key, val := range replacements() { |
||||
|
if val == "" { |
||||
|
val = empty |
||||
|
} |
||||
|
input = strings.ReplaceAll(input, phOpen+key+phClose, val) |
||||
|
} |
||||
|
} |
||||
|
return input |
||||
|
} |
||||
|
|
||||
|
// ReplacementsFunc is a function that returns replacements,
|
||||
|
// which is variable names mapped to their values. The
|
||||
|
// function will be evaluated only at replace-time to ensure
|
||||
|
// the most current values are mapped.
|
||||
|
type ReplacementsFunc func() map[string]string |
||||
|
|
||||
|
var defaultReplacements = func() map[string]string { |
||||
|
m := map[string]string{ |
||||
|
"system.hostname": func() string { |
||||
|
// OK if there is an error; just return empty string
|
||||
|
name, _ := os.Hostname() |
||||
|
return name |
||||
|
}(), |
||||
|
"system.slash": string(filepath.Separator), |
||||
|
"system.os": runtime.GOOS, |
||||
|
"system.arch": runtime.GOARCH, |
||||
|
} |
||||
|
|
||||
|
// add environment variables
|
||||
|
for _, keyval := range os.Environ() { |
||||
|
parts := strings.SplitN(keyval, "=", 2) |
||||
|
if len(parts) != 2 { |
||||
|
continue |
||||
|
} |
||||
|
m["env."+strings.ToUpper(parts[0])] = parts[1] |
||||
|
} |
||||
|
|
||||
|
return m |
||||
|
} |
||||
|
|
||||
|
// ReplacerCtxKey is the context key for a replacer.
|
||||
|
const ReplacerCtxKey CtxKey = "replacer" |
||||
|
|
||||
|
const phOpen, phClose = "{", "}" |
Loading…
Reference in new issue