mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-22 20:25:23 +08:00
Implement most of static file server; refactor and improve Replacer
This commit is contained in:
parent
1a20fe330e
commit
fec7fa8bfd
3
caddy.go
3
caddy.go
|
@ -34,7 +34,7 @@ func Run(newCfg *Config) error {
|
||||||
// modules - essentially our new config's
|
// modules - essentially our new config's
|
||||||
// execution environment; be sure that
|
// execution environment; be sure that
|
||||||
// cleanup occurs when we return if there
|
// cleanup occurs when we return if there
|
||||||
// was an error; otherwise, it will get
|
// was an error; if no error, it will get
|
||||||
// cleaned up on next config cycle
|
// cleaned up on next config cycle
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -139,7 +139,6 @@ type Config struct {
|
||||||
StorageRaw json.RawMessage `json:"storage"`
|
StorageRaw json.RawMessage `json:"storage"`
|
||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
|
|
||||||
TestVal string `json:"testval"`
|
|
||||||
AppsRaw map[string]json.RawMessage `json:"apps"`
|
AppsRaw map[string]json.RawMessage `json:"apps"`
|
||||||
|
|
||||||
// apps stores the decoded Apps values,
|
// apps stores the decoded Apps values,
|
||||||
|
|
|
@ -45,7 +45,13 @@ type App struct {
|
||||||
func (app *App) Provision(ctx caddy2.Context) error {
|
func (app *App) Provision(ctx caddy2.Context) error {
|
||||||
app.ctx = ctx
|
app.ctx = ctx
|
||||||
|
|
||||||
|
repl := caddy2.NewReplacer()
|
||||||
|
|
||||||
for _, srv := range app.Servers {
|
for _, srv := range app.Servers {
|
||||||
|
// TODO: Test this function to ensure these replacements are performed
|
||||||
|
for i := range srv.Listen {
|
||||||
|
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
|
||||||
|
}
|
||||||
err := srv.Routes.Provision(ctx)
|
err := srv.Routes.Provision(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("setting up server routes: %v", err)
|
return fmt.Errorf("setting up server routes: %v", err)
|
||||||
|
@ -78,6 +84,13 @@ func (app *App) Validate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// each server's max rehandle value must be valid
|
||||||
|
for srvName, srv := range app.Servers {
|
||||||
|
if srv.MaxRehandles < 0 {
|
||||||
|
return fmt.Errorf("%s: invalid max_rehandles value: %d", srvName, srv.MaxRehandles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +244,7 @@ func (app *App) automaticHTTPS() error {
|
||||||
redirTo += "{request.uri}"
|
redirTo += "{request.uri}"
|
||||||
|
|
||||||
redirRoutes = append(redirRoutes, ServerRoute{
|
redirRoutes = append(redirRoutes, ServerRoute{
|
||||||
matchers: []RouteMatcher{
|
matchers: []RequestMatcher{
|
||||||
matchProtocol("http"),
|
matchProtocol("http"),
|
||||||
matchHost(domains),
|
matchHost(domains),
|
||||||
},
|
},
|
||||||
|
@ -292,84 +305,9 @@ func (app *App) listenerTaken(network, address string) bool {
|
||||||
|
|
||||||
var defaultALPN = []string{"h2", "http/1.1"}
|
var defaultALPN = []string{"h2", "http/1.1"}
|
||||||
|
|
||||||
// Server is an HTTP server.
|
// RequestMatcher is a type that can match to a request.
|
||||||
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"`
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpErrorConfig struct {
|
|
||||||
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 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(), ReplacerCtxKey, repl)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
// build and execute the main middleware chain
|
|
||||||
stack := s.Routes.BuildHandlerChain(w, r)
|
|
||||||
err := executeMiddlewareChain(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.BuildHandlerChain(w, r)
|
|
||||||
err := executeMiddlewareChain(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// executeMiddlewareChain 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 executeMiddlewareChain(w http.ResponseWriter, r *http.Request, stack Handler) error {
|
|
||||||
const maxRehandles = 3
|
|
||||||
var err error
|
|
||||||
for i := 0; i < maxRehandles; i++ {
|
|
||||||
err = stack.ServeHTTP(w, r)
|
|
||||||
if err != ErrRehandle {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if i == maxRehandles-1 {
|
|
||||||
return fmt.Errorf("too many rehandles")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouteMatcher is a type that can match to a request.
|
|
||||||
// A route matcher MUST NOT modify the request.
|
// A route matcher MUST NOT modify the request.
|
||||||
type RouteMatcher interface {
|
type RequestMatcher interface {
|
||||||
Match(*http.Request) bool
|
Match(*http.Request) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,7 +359,6 @@ func parseListenAddr(a string) (network string, addrs []string, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
host = NewReplacer(nil, nil).Replace(host, "")
|
|
||||||
ports := strings.SplitN(port, "-", 2)
|
ports := strings.SplitN(port, "-", 2)
|
||||||
if len(ports) == 1 {
|
if len(ports) == 1 {
|
||||||
ports = append(ports, ports[0])
|
ports = append(ports, ports[0])
|
||||||
|
@ -466,25 +403,6 @@ func joinListenAddr(network, host, port string) string {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
type middlewareResponseWriter struct {
|
|
||||||
*ResponseWriterWrapper
|
|
||||||
allowWrites bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
|
|
||||||
if !mrw.allowWrites {
|
|
||||||
panic("WriteHeader: middleware cannot write to the response")
|
|
||||||
}
|
|
||||||
mrw.ResponseWriterWrapper.WriteHeader(statusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
|
|
||||||
if !mrw.allowWrites {
|
|
||||||
panic("Write: middleware cannot write to the response")
|
|
||||||
}
|
|
||||||
return mrw.ResponseWriterWrapper.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultHTTPPort is the default port for HTTP.
|
// DefaultHTTPPort is the default port for HTTP.
|
||||||
DefaultHTTPPort = 80
|
DefaultHTTPPort = 80
|
||||||
|
@ -493,6 +411,5 @@ const (
|
||||||
DefaultHTTPSPort = 443
|
DefaultHTTPSPort = 443
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface guards
|
// Interface guard
|
||||||
var _ HTTPInterfaces = middlewareResponseWriter{}
|
|
||||||
var _ caddy2.App = (*App)(nil)
|
var _ caddy2.App = (*App)(nil)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -115,11 +114,6 @@ func TestJoinListenerAddr(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseListenerAddr(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 {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
expectNetwork string
|
expectNetwork string
|
||||||
|
@ -176,11 +170,6 @@ func TestParseListenerAddr(t *testing.T) {
|
||||||
expectNetwork: "tcp",
|
expectNetwork: "tcp",
|
||||||
expectAddrs: []string{"localhost:0"},
|
expectAddrs: []string{"localhost:0"},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
input: "{system.hostname}:0",
|
|
||||||
expectNetwork: "tcp",
|
|
||||||
expectAddrs: []string{hostname + ":0"},
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
actualNetwork, actualAddrs, err := parseListenAddr(tc.input)
|
actualNetwork, actualAddrs, err := parseListenAddr(tc.input)
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectErr && err == nil {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -23,7 +25,8 @@ type (
|
||||||
matchHeader http.Header
|
matchHeader http.Header
|
||||||
matchHeaderRE map[string]*matchRegexp
|
matchHeaderRE map[string]*matchRegexp
|
||||||
matchProtocol string
|
matchProtocol string
|
||||||
matchStarlark string
|
matchStarlarkExpr string
|
||||||
|
matchTable string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -60,8 +63,8 @@ func init() {
|
||||||
New: func() (interface{}, error) { return new(matchProtocol), nil },
|
New: func() (interface{}, error) { return new(matchProtocol), nil },
|
||||||
})
|
})
|
||||||
caddy2.RegisterModule(caddy2.Module{
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
Name: "http.matchers.caddyscript",
|
Name: "http.matchers.starlark_expr",
|
||||||
New: func() (interface{}, error) { return new(matchStarlark), nil },
|
New: func() (interface{}, error) { return new(matchStarlarkExpr), nil },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +94,17 @@ outer:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m matchPath) Match(r *http.Request) bool {
|
func (m matchPath) Match(r *http.Request) bool {
|
||||||
for _, path := range m {
|
for _, matchPath := range m {
|
||||||
if strings.HasPrefix(r.URL.Path, path) {
|
compare := r.URL.Path
|
||||||
|
if strings.HasPrefix(matchPath, "*") {
|
||||||
|
compare = path.Base(compare)
|
||||||
|
}
|
||||||
|
// can ignore error here because we can't handle it anyway
|
||||||
|
matches, _ := filepath.Match(matchPath, compare)
|
||||||
|
if matches {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(r.URL.Path, matchPath) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +112,7 @@ func (m matchPath) Match(r *http.Request) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m matchPathRE) Match(r *http.Request) bool {
|
func (m matchPathRE) Match(r *http.Request) bool {
|
||||||
repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
|
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
|
||||||
return m.match(r.URL.Path, repl, "path_regexp")
|
return m.match(r.URL.Path, repl, "path_regexp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +159,7 @@ func (m matchHeader) Match(r *http.Request) bool {
|
||||||
|
|
||||||
func (m matchHeaderRE) Match(r *http.Request) bool {
|
func (m matchHeaderRE) Match(r *http.Request) bool {
|
||||||
for field, rm := range m {
|
for field, rm := range m {
|
||||||
repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
|
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
|
||||||
match := rm.match(r.Header.Get(field), repl, "header_regexp")
|
match := rm.match(r.Header.Get(field), repl, "header_regexp")
|
||||||
if !match {
|
if !match {
|
||||||
return false
|
return false
|
||||||
|
@ -188,7 +200,7 @@ func (m matchProtocol) Match(r *http.Request) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m matchStarlark) Match(r *http.Request) bool {
|
func (m matchStarlarkExpr) Match(r *http.Request) bool {
|
||||||
input := string(m)
|
input := string(m)
|
||||||
thread := new(starlark.Thread)
|
thread := new(starlark.Thread)
|
||||||
env := caddyscript.MatcherEnv(r)
|
env := caddyscript.MatcherEnv(r)
|
||||||
|
@ -225,7 +237,7 @@ func (mre *matchRegexp) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mre *matchRegexp) match(input string, repl *Replacer, scope string) bool {
|
func (mre *matchRegexp) match(input string, repl caddy2.Replacer, scope string) bool {
|
||||||
matches := mre.compiled.FindStringSubmatch(input)
|
matches := mre.compiled.FindStringSubmatch(input)
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
return false
|
return false
|
||||||
|
@ -234,14 +246,14 @@ func (mre *matchRegexp) match(input string, repl *Replacer, scope string) bool {
|
||||||
// save all capture groups, first by index
|
// save all capture groups, first by index
|
||||||
for i, match := range matches {
|
for i, match := range matches {
|
||||||
key := fmt.Sprintf("matchers.%s.%s.%d", scope, mre.Name, i)
|
key := fmt.Sprintf("matchers.%s.%s.%d", scope, mre.Name, i)
|
||||||
repl.Map(key, match)
|
repl.Set(key, match)
|
||||||
}
|
}
|
||||||
|
|
||||||
// then by name
|
// then by name
|
||||||
for i, name := range mre.compiled.SubexpNames() {
|
for i, name := range mre.compiled.SubexpNames() {
|
||||||
if i != 0 && name != "" {
|
if i != 0 && name != "" {
|
||||||
key := fmt.Sprintf("matchers.%s.%s.%s", scope, mre.Name, name)
|
key := fmt.Sprintf("matchers.%s.%s.%s", scope, mre.Name, name)
|
||||||
repl.Map(key, matches[i])
|
repl.Set(key, matches[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,13 +264,13 @@ var wordRE = regexp.MustCompile(`\w+`)
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ RouteMatcher = (*matchHost)(nil)
|
_ RequestMatcher = (*matchHost)(nil)
|
||||||
_ RouteMatcher = (*matchPath)(nil)
|
_ RequestMatcher = (*matchPath)(nil)
|
||||||
_ RouteMatcher = (*matchPathRE)(nil)
|
_ RequestMatcher = (*matchPathRE)(nil)
|
||||||
_ RouteMatcher = (*matchMethod)(nil)
|
_ RequestMatcher = (*matchMethod)(nil)
|
||||||
_ RouteMatcher = (*matchQuery)(nil)
|
_ RequestMatcher = (*matchQuery)(nil)
|
||||||
_ RouteMatcher = (*matchHeader)(nil)
|
_ RequestMatcher = (*matchHeader)(nil)
|
||||||
_ RouteMatcher = (*matchHeaderRE)(nil)
|
_ RequestMatcher = (*matchHeaderRE)(nil)
|
||||||
_ RouteMatcher = (*matchProtocol)(nil)
|
_ RequestMatcher = (*matchProtocol)(nil)
|
||||||
_ RouteMatcher = (*matchStarlark)(nil)
|
_ RequestMatcher = (*matchStarlarkExpr)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHostMatcher(t *testing.T) {
|
func TestHostMatcher(t *testing.T) {
|
||||||
|
@ -131,6 +133,26 @@ func TestPathMatcher(t *testing.T) {
|
||||||
input: "/other/",
|
input: "/other/",
|
||||||
expect: true,
|
expect: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: matchPath{"*.ext"},
|
||||||
|
input: "foo.ext",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: matchPath{"*.ext"},
|
||||||
|
input: "/foo/bar.ext",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: matchPath{"/foo/*/baz"},
|
||||||
|
input: "/foo/bar/baz",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: matchPath{"/foo/*/baz/bam"},
|
||||||
|
input: "/foo/bar/bam",
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
req := &http.Request{URL: &url.URL{Path: tc.input}}
|
req := &http.Request{URL: &url.URL{Path: tc.input}}
|
||||||
actual := tc.match.Match(req)
|
actual := tc.match.Match(req)
|
||||||
|
@ -205,8 +227,8 @@ func TestPathREMatcher(t *testing.T) {
|
||||||
|
|
||||||
// set up the fake request and its Replacer
|
// set up the fake request and its Replacer
|
||||||
req := &http.Request{URL: &url.URL{Path: tc.input}}
|
req := &http.Request{URL: &url.URL{Path: tc.input}}
|
||||||
repl := NewReplacer(req, httptest.NewRecorder())
|
repl := newReplacer(req, httptest.NewRecorder())
|
||||||
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
|
ctx := context.WithValue(req.Context(), caddy2.ReplacerCtxKey, repl)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
actual := tc.match.Match(req)
|
actual := tc.match.Match(req)
|
||||||
|
@ -218,7 +240,7 @@ func TestPathREMatcher(t *testing.T) {
|
||||||
|
|
||||||
for key, expectVal := range tc.expectRepl {
|
for key, expectVal := range tc.expectRepl {
|
||||||
placeholder := fmt.Sprintf("{matchers.path_regexp.%s}", key)
|
placeholder := fmt.Sprintf("{matchers.path_regexp.%s}", key)
|
||||||
actualVal := repl.Replace(placeholder, "<empty>")
|
actualVal := repl.ReplaceAll(placeholder, "<empty>")
|
||||||
if actualVal != expectVal {
|
if actualVal != expectVal {
|
||||||
t.Errorf("Test %d [%v]: Expected placeholder {matchers.path_regexp.%s} to be '%s' but got '%s'",
|
t.Errorf("Test %d [%v]: Expected placeholder {matchers.path_regexp.%s} to be '%s' but got '%s'",
|
||||||
i, tc.match.Pattern, key, expectVal, actualVal)
|
i, tc.match.Pattern, key, expectVal, actualVal)
|
||||||
|
@ -322,8 +344,8 @@ func TestHeaderREMatcher(t *testing.T) {
|
||||||
|
|
||||||
// set up the fake request and its Replacer
|
// set up the fake request and its Replacer
|
||||||
req := &http.Request{Header: tc.input, URL: new(url.URL)}
|
req := &http.Request{Header: tc.input, URL: new(url.URL)}
|
||||||
repl := NewReplacer(req, httptest.NewRecorder())
|
repl := newReplacer(req, httptest.NewRecorder())
|
||||||
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
|
ctx := context.WithValue(req.Context(), caddy2.ReplacerCtxKey, repl)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
actual := tc.match.Match(req)
|
actual := tc.match.Match(req)
|
||||||
|
@ -335,7 +357,7 @@ func TestHeaderREMatcher(t *testing.T) {
|
||||||
|
|
||||||
for key, expectVal := range tc.expectRepl {
|
for key, expectVal := range tc.expectRepl {
|
||||||
placeholder := fmt.Sprintf("{matchers.header_regexp.%s}", key)
|
placeholder := fmt.Sprintf("{matchers.header_regexp.%s}", key)
|
||||||
actualVal := repl.Replace(placeholder, "<empty>")
|
actualVal := repl.ReplaceAll(placeholder, "<empty>")
|
||||||
if actualVal != expectVal {
|
if actualVal != expectVal {
|
||||||
t.Errorf("Test %d [%v]: Expected placeholder {matchers.header_regexp.%s} to be '%s' but got '%s'",
|
t.Errorf("Test %d [%v]: Expected placeholder {matchers.header_regexp.%s} to be '%s' but got '%s'",
|
||||||
i, tc.match, key, expectVal, actualVal)
|
i, tc.match, key, expectVal, actualVal)
|
||||||
|
|
|
@ -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
|
func newReplacer(req *http.Request, w http.ResponseWriter) caddy2.Replacer {
|
||||||
// fields. The request and response writer are optional, but
|
repl := caddy2.NewReplacer()
|
||||||
// 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.
|
httpVars := func() map[string]string {
|
||||||
func (r *Replacer) Map(variable, value string) {
|
m := make(map[string]string)
|
||||||
r.custom[variable] = value
|
if req != nil {
|
||||||
}
|
m["http.request.host"] = func() string {
|
||||||
|
host, _, err := net.SplitHostPort(req.Host)
|
||||||
// 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 {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
repl.Map(httpVars)
|
||||||
|
|
||||||
|
return repl
|
||||||
}
|
}
|
||||||
|
|
||||||
const phOpen, phClose = "{", "}"
|
|
||||||
|
|
||||||
// ReplacerCtxKey is the context key for the request's replacer.
|
|
||||||
const ReplacerCtxKey caddy2.CtxKey = "replacer"
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
// middlewares, and a responder for handling HTTP
|
// middlewares, and a responder for handling HTTP
|
||||||
// requests.
|
// requests.
|
||||||
type ServerRoute struct {
|
type ServerRoute struct {
|
||||||
|
Group string `json:"group"`
|
||||||
Matchers map[string]json.RawMessage `json:"match"`
|
Matchers map[string]json.RawMessage `json:"match"`
|
||||||
Apply []json.RawMessage `json:"apply"`
|
Apply []json.RawMessage `json:"apply"`
|
||||||
Respond json.RawMessage `json:"respond"`
|
Respond json.RawMessage `json:"respond"`
|
||||||
|
@ -19,7 +20,7 @@ type ServerRoute struct {
|
||||||
Terminal bool `json:"terminal"`
|
Terminal bool `json:"terminal"`
|
||||||
|
|
||||||
// decoded values
|
// decoded values
|
||||||
matchers []RouteMatcher
|
matchers []RequestMatcher
|
||||||
middleware []MiddlewareHandler
|
middleware []MiddlewareHandler
|
||||||
responder Handler
|
responder Handler
|
||||||
}
|
}
|
||||||
|
@ -37,7 +38,7 @@ func (routes RouteList) Provision(ctx caddy2.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
|
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
|
||||||
}
|
}
|
||||||
routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
|
routes[i].matchers = append(routes[i].matchers, val.(RequestMatcher))
|
||||||
}
|
}
|
||||||
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
|
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
|
||||||
|
@ -64,9 +65,9 @@ func (routes RouteList) Provision(ctx caddy2.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildHandlerChain creates a chain of handlers by
|
// BuildCompositeRoute creates a chain of handlers by
|
||||||
// applying all the matching routes.
|
// applying all the matching routes.
|
||||||
func (routes RouteList) BuildHandlerChain(w http.ResponseWriter, r *http.Request) Handler {
|
func (routes RouteList) BuildCompositeRoute(w http.ResponseWriter, r *http.Request) Handler {
|
||||||
if len(routes) == 0 {
|
if len(routes) == 0 {
|
||||||
return emptyHandler
|
return emptyHandler
|
||||||
}
|
}
|
||||||
|
@ -74,17 +75,39 @@ func (routes RouteList) BuildHandlerChain(w http.ResponseWriter, r *http.Request
|
||||||
var mid []Middleware
|
var mid []Middleware
|
||||||
var responder Handler
|
var responder Handler
|
||||||
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
|
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
|
||||||
|
groups := make(map[string]struct{})
|
||||||
|
|
||||||
routeLoop:
|
routeLoop:
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
|
// see if route matches
|
||||||
for _, m := range route.matchers {
|
for _, m := range route.matchers {
|
||||||
if !m.Match(r) {
|
if !m.Match(r) {
|
||||||
continue routeLoop
|
continue routeLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if route is part of a group, ensure only
|
||||||
|
// the first matching route in the group is
|
||||||
|
// applied
|
||||||
|
if route.Group != "" {
|
||||||
|
_, ok := groups[route.Group]
|
||||||
|
if ok {
|
||||||
|
// this group has already been satisfied
|
||||||
|
// by a matching route
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// this matching route satisfies the group
|
||||||
|
groups[route.Group] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply the rest of the route
|
||||||
for _, m := range route.middleware {
|
for _, m := range route.middleware {
|
||||||
mid = append(mid, func(next HandlerFunc) HandlerFunc {
|
mid = append(mid, func(next HandlerFunc) HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
// TODO: This is where request tracing could be implemented; also
|
||||||
|
// see below to trace the responder as well
|
||||||
|
// TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...)
|
||||||
|
// TODO: see what the std lib gives us in terms of stack trracing too
|
||||||
return m.ServeHTTP(mrw, r, next)
|
return m.ServeHTTP(mrw, r, next)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -111,3 +134,25 @@ routeLoop:
|
||||||
|
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type middlewareResponseWriter struct {
|
||||||
|
*ResponseWriterWrapper
|
||||||
|
allowWrites bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
if !mrw.allowWrites {
|
||||||
|
panic("WriteHeader: middleware cannot write to the response")
|
||||||
|
}
|
||||||
|
mrw.ResponseWriterWrapper.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !mrw.allowWrites {
|
||||||
|
panic("Write: middleware cannot write to the response")
|
||||||
|
}
|
||||||
|
return mrw.ResponseWriterWrapper.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ HTTPInterfaces = middlewareResponseWriter{}
|
||||||
|
|
91
modules/caddyhttp/server.go
Normal file
91
modules/caddyhttp/server.go
Normal file
|
@ -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"
|
205
modules/caddyhttp/staticfiles/browse.go
Normal file
205
modules/caddyhttp/staticfiles/browse.go
Normal file
|
@ -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
|
||||||
|
// }
|
54
modules/caddyhttp/staticfiles/matcher.go
Normal file
54
modules/caddyhttp/staticfiles/matcher.go
Normal file
|
@ -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)
|
|
@ -1,7 +1,15 @@
|
||||||
package staticfiles
|
package staticfiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
weakrand "math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"bitbucket.org/lightcodelabs/caddy2"
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
"bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
|
"bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
|
||||||
|
@ -16,13 +24,298 @@ func init() {
|
||||||
|
|
||||||
// StaticFiles implements a static file server responder for Caddy.
|
// StaticFiles implements a static file server responder for Caddy.
|
||||||
type StaticFiles struct {
|
type StaticFiles struct {
|
||||||
Root string
|
Root string `json:"root"` // default is current directory
|
||||||
|
IndexNames []string `json:"index_names"`
|
||||||
|
Files []string `json:"files"` // all relative to the root; default is request URI path
|
||||||
|
SelectionPolicy string `json:"selection_policy"`
|
||||||
|
Fallback caddyhttp.RouteList `json:"fallback"`
|
||||||
|
Browse *Browse `json:"browse"`
|
||||||
|
Hide []string `json:"hide"`
|
||||||
|
Rehandle bool `json:"rehandle"` // issue a rehandle (internal redirect) if request is rewritten
|
||||||
|
// TODO: Etag
|
||||||
|
// TODO: Content negotiation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sf StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
// Provision sets up the static files responder.
|
||||||
http.FileServer(http.Dir(sf.Root)).ServeHTTP(w, r)
|
func (sf *StaticFiles) Provision(ctx caddy2.Context) error {
|
||||||
|
if sf.Fallback != nil {
|
||||||
|
err := sf.Fallback.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting up fallback routes: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sf.IndexNames == nil {
|
||||||
|
sf.IndexNames = defaultIndexNames
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate ensures that sf has a valid configuration.
|
||||||
|
func (sf *StaticFiles) Validate() error {
|
||||||
|
switch sf.SelectionPolicy {
|
||||||
|
case "",
|
||||||
|
"first_existing",
|
||||||
|
"largest_size",
|
||||||
|
"smallest_size",
|
||||||
|
"most_recently_modified":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown selection policy %s", sf.SelectionPolicy)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
// TODO: Prevent directory traversal, see https://play.golang.org/p/oh77BiVQFti
|
||||||
|
|
||||||
|
// http.FileServer(http.Dir(sf.Directory)).ServeHTTP(w, r)
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
// TODO: Still needed?
|
||||||
|
// // Prevent absolute path access on Windows, e.g.: http://localhost:5000/C:\Windows\notepad.exe
|
||||||
|
// // TODO: does stdlib http.Dir handle this? see first check of http.Dir.Open()...
|
||||||
|
// if runtime.GOOS == "windows" && len(reqPath) > 0 && filepath.IsAbs(reqPath[1:]) {
|
||||||
|
// return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("request path was absolute"))
|
||||||
|
// }
|
||||||
|
|
||||||
|
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
|
||||||
|
|
||||||
|
// map the request to a filename
|
||||||
|
pathBefore := r.URL.Path
|
||||||
|
filename := sf.selectFile(r, repl)
|
||||||
|
if filename == "" {
|
||||||
|
// no files worked, so resort to fallback
|
||||||
|
if sf.Fallback != nil {
|
||||||
|
fallback := sf.Fallback.BuildCompositeRoute(w, r)
|
||||||
|
return fallback.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return caddyhttp.Error(http.StatusNotFound, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the ultimate destination has changed, submit
|
||||||
|
// this request for a rehandling (internal redirect)
|
||||||
|
// if configured to do so
|
||||||
|
// TODO: double check this against https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/
|
||||||
|
if r.URL.Path != pathBefore && sf.Rehandle {
|
||||||
|
return caddyhttp.ErrRehandle
|
||||||
|
}
|
||||||
|
|
||||||
|
// get information about the file
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return caddyhttp.Error(http.StatusNotFound, err)
|
||||||
|
} else if os.IsPermission(err) {
|
||||||
|
return caddyhttp.Error(http.StatusForbidden, err)
|
||||||
|
}
|
||||||
|
// TODO: treat this as resource exhaustion like with os.Open? Or unnecessary here?
|
||||||
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the request mapped to a directory, see if
|
||||||
|
// there is an index file we can serve
|
||||||
|
if info.IsDir() && len(sf.IndexNames) > 0 {
|
||||||
|
filesToHide := sf.transformHidePaths(repl)
|
||||||
|
|
||||||
|
for _, indexPage := range sf.IndexNames {
|
||||||
|
indexPath := path.Join(filename, indexPage)
|
||||||
|
if fileIsHidden(indexPath, filesToHide) {
|
||||||
|
// pretend this file doesn't exist
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
indexInfo, err := os.Stat(indexPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// we found an index file that might work,
|
||||||
|
// so rewrite the request path and, if
|
||||||
|
// configured, do an internal redirect
|
||||||
|
// TODO: I don't know if the logic for rewriting
|
||||||
|
// the URL here is the right logic
|
||||||
|
r.URL.Path = path.Join(r.URL.Path, indexPage)
|
||||||
|
if sf.Rehandle {
|
||||||
|
return caddyhttp.ErrRehandle
|
||||||
|
}
|
||||||
|
|
||||||
|
info = indexInfo
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if still referencing a directory, delegate
|
||||||
|
// to browse or return an error
|
||||||
|
if info.IsDir() {
|
||||||
|
if sf.Browse != nil {
|
||||||
|
return sf.Browse.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return caddyhttp.Error(http.StatusNotFound, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the file
|
||||||
|
file, err := os.Open(info.Name())
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return caddyhttp.Error(http.StatusNotFound, err)
|
||||||
|
} else if os.IsPermission(err) {
|
||||||
|
return caddyhttp.Error(http.StatusForbidden, err)
|
||||||
|
}
|
||||||
|
// maybe the server is under load and ran out of file descriptors?
|
||||||
|
// have client wait arbitrary seconds to help prevent a stampede
|
||||||
|
backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
|
||||||
|
w.Header().Set("Retry-After", strconv.Itoa(backoff))
|
||||||
|
return caddyhttp.Error(http.StatusServiceUnavailable, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// TODO: Right now we return an invalid response if the
|
||||||
|
// request is for a directory and there is no index file
|
||||||
|
// or dir browsing; we should return a 404 I think...
|
||||||
|
|
||||||
|
// TODO: Etag?
|
||||||
|
|
||||||
|
// TODO: content negotiation? (brotli sidecar files, etc...)
|
||||||
|
|
||||||
|
// let the standard library do what it does best; note, however,
|
||||||
|
// that errors generated by ServeContent are written immediately
|
||||||
|
// to the response, so we cannot handle them (but errors here are rare)
|
||||||
|
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *StaticFiles) transformHidePaths(repl caddy2.Replacer) []string {
|
||||||
|
hide := make([]string, len(sf.Hide))
|
||||||
|
for i := range sf.Hide {
|
||||||
|
hide[i] = repl.ReplaceAll(sf.Hide[i], "")
|
||||||
|
}
|
||||||
|
return hide
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string {
|
||||||
|
root := repl.ReplaceAll(sf.Root, "")
|
||||||
|
if root == "" {
|
||||||
|
root = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
if sf.Files == nil {
|
||||||
|
return filepath.Join(root, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sf.SelectionPolicy {
|
||||||
|
// TODO: Make these policy names constants
|
||||||
|
case "", "first_existing":
|
||||||
|
filesToHide := sf.transformHidePaths(repl)
|
||||||
|
for _, f := range sf.Files {
|
||||||
|
suffix := repl.ReplaceAll(f, "")
|
||||||
|
// TODO: sanitize path
|
||||||
|
fullpath := filepath.Join(root, suffix)
|
||||||
|
if !fileIsHidden(fullpath, filesToHide) && fileExists(fullpath) {
|
||||||
|
r.URL.Path = suffix
|
||||||
|
return fullpath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "largest_size":
|
||||||
|
var largestSize int64
|
||||||
|
var largestFilename string
|
||||||
|
var largestSuffix string
|
||||||
|
for _, f := range sf.Files {
|
||||||
|
suffix := repl.ReplaceAll(f, "")
|
||||||
|
// TODO: sanitize path
|
||||||
|
fullpath := filepath.Join(root, suffix)
|
||||||
|
info, err := os.Stat(fullpath)
|
||||||
|
if err == nil && info.Size() > largestSize {
|
||||||
|
largestSize = info.Size()
|
||||||
|
largestFilename = fullpath
|
||||||
|
largestSuffix = suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.URL.Path = largestSuffix
|
||||||
|
return largestFilename
|
||||||
|
|
||||||
|
case "smallest_size":
|
||||||
|
var smallestSize int64
|
||||||
|
var smallestFilename string
|
||||||
|
var smallestSuffix string
|
||||||
|
for _, f := range sf.Files {
|
||||||
|
suffix := repl.ReplaceAll(f, "")
|
||||||
|
// TODO: sanitize path
|
||||||
|
fullpath := filepath.Join(root, suffix)
|
||||||
|
info, err := os.Stat(fullpath)
|
||||||
|
if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
|
||||||
|
smallestSize = info.Size()
|
||||||
|
smallestFilename = fullpath
|
||||||
|
smallestSuffix = suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.URL.Path = smallestSuffix
|
||||||
|
return smallestFilename
|
||||||
|
|
||||||
|
case "most_recently_modified":
|
||||||
|
var recentDate time.Time
|
||||||
|
var recentFilename string
|
||||||
|
var recentSuffix string
|
||||||
|
for _, f := range sf.Files {
|
||||||
|
suffix := repl.ReplaceAll(f, "")
|
||||||
|
// TODO: sanitize path
|
||||||
|
fullpath := filepath.Join(root, suffix)
|
||||||
|
info, err := os.Stat(fullpath)
|
||||||
|
if err == nil &&
|
||||||
|
(recentDate.IsZero() || info.ModTime().After(recentDate)) {
|
||||||
|
recentDate = info.ModTime()
|
||||||
|
recentFilename = fullpath
|
||||||
|
recentSuffix = suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.URL.Path = recentSuffix
|
||||||
|
return recentFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileExists returns true if file exists.
|
||||||
|
func fileExists(file string) bool {
|
||||||
|
_, err := os.Stat(file)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileIsHidden(filename string, hide []string) bool {
|
||||||
|
nameOnly := filepath.Base(filename)
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
// see if file is hidden
|
||||||
|
for _, h := range hide {
|
||||||
|
// assuming h is a glob/shell-like pattern,
|
||||||
|
// use it to compare the whole file path;
|
||||||
|
// but if there is no separator in h, then
|
||||||
|
// just compare against the file's name
|
||||||
|
compare := filename
|
||||||
|
if !strings.Contains(h, sep) {
|
||||||
|
compare = nameOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden, err := filepath.Match(h, compare)
|
||||||
|
if err != nil {
|
||||||
|
// malformed pattern; fallback by checking prefix
|
||||||
|
if strings.HasPrefix(filename, h) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hidden {
|
||||||
|
// file name or path matches hide pattern
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultIndexNames = []string{"index.html"}
|
||||||
|
|
||||||
|
const minBackoff, maxBackoff = 2, 5
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ caddyhttp.Handler = (*StaticFiles)(nil)
|
var _ caddyhttp.Handler = (*StaticFiles)(nil)
|
||||||
|
|
|
@ -23,16 +23,16 @@ type Static struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
|
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
|
||||||
|
|
||||||
// close the connection after responding
|
// close the connection after responding
|
||||||
r.Close = s.Close
|
r.Close = s.Close
|
||||||
|
|
||||||
// set all headers, with replacements
|
// set all headers, with replacements
|
||||||
for field, vals := range s.Headers {
|
for field, vals := range s.Headers {
|
||||||
field = repl.Replace(field, "")
|
field = repl.ReplaceAll(field, "")
|
||||||
for i := range vals {
|
for i := range vals {
|
||||||
vals[i] = repl.Replace(vals[i], "")
|
vals[i] = repl.ReplaceAll(vals[i], "")
|
||||||
}
|
}
|
||||||
w.Header()[field] = vals
|
w.Header()[field] = vals
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
// write the response body, with replacements
|
// write the response body, with replacements
|
||||||
if s.Body != "" {
|
if s.Body != "" {
|
||||||
fmt.Fprint(w, repl.Replace(s.Body, ""))
|
fmt.Fprint(w, repl.ReplaceAll(s.Body, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
41
modules/caddyhttp/table.go
Normal file
41
modules/caddyhttp/table.go
Normal file
|
@ -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)
|
104
replacer.go
Normal file
104
replacer.go
Normal file
|
@ -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
Block a user