Matthew Holt
6 years ago
14 changed files with 994 additions and 258 deletions
@ -1,119 +1,83 @@ |
|||
package caddyhttp |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net" |
|||
"net/http" |
|||
"os" |
|||
"path" |
|||
"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; 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
|
|||
// 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{ |
|||
"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 { |
|||
return r.req.Host // OK; there probably was no port
|
|||
// TODO: A simple way to format or escape or encode each value would be nice
|
|||
// ... TODO: Should we just use templates? :-/ yeesh...
|
|||
|
|||
func newReplacer(req *http.Request, w http.ResponseWriter) caddy2.Replacer { |
|||
repl := caddy2.NewReplacer() |
|||
|
|||
httpVars := func() map[string]string { |
|||
m := make(map[string]string) |
|||
if req != nil { |
|||
m["http.request.host"] = func() string { |
|||
host, _, err := net.SplitHostPort(req.Host) |
|||
if err != nil { |
|||
return req.Host // OK; there probably was no port
|
|||
} |
|||
return host |
|||
}() |
|||
m["http.request.hostport"] = req.Host // may include both host and port
|
|||
m["http.request.method"] = req.Method |
|||
m["http.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(req.Host) |
|||
return port |
|||
}() |
|||
m["http.request.scheme"] = func() string { |
|||
if req.TLS != nil { |
|||
return "https" |
|||
} |
|||
return "http" |
|||
}() |
|||
m["http.request.uri"] = req.URL.RequestURI() |
|||
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 req.Header { |
|||
m["http.request.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
|||
} |
|||
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" |
|||
for _, cookie := range req.Cookies() { |
|||
m["http.request.cookie."+cookie.Name] = cookie.Value |
|||
} |
|||
for param, vals := range req.URL.Query() { |
|||
m["http.request.uri.query."+param] = strings.Join(vals, ",") |
|||
} |
|||
return "http" |
|||
}() |
|||
m["request.uri"] = r.req.URL.RequestURI() |
|||
m["request.uri.path"] = r.req.URL.Path |
|||
|
|||
for field, vals := range r.req.Header { |
|||
m["request.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
|||
} |
|||
for _, cookie := range r.req.Cookies() { |
|||
m["request.cookie."+cookie.Name] = cookie.Value |
|||
} |
|||
for param, vals := range r.req.URL.Query() { |
|||
m["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 { |
|||
for field, vals := range r.resp.Header() { |
|||
m["response.header."+strings.ToLower(field)] = strings.Join(vals, ",") |
|||
if w != nil { |
|||
for field, vals := range w.Header() { |
|||
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.
|
|||
const ReplacerCtxKey caddy2.CtxKey = "replacer" |
|||
return repl |
|||
} |
|||
|
@ -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