@marcel91200 / caddy Mirror · read-only / commits / 6621406f
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

Very basic middleware and route matching functionality

Matthew Holt committed Mar 31, 2019 at 20:41 UTC 6621406fa8b44826477ba7cbe2ff6c5462048f8e
9 files changed +454 -25
caddy.go
+7 -8
@@ -17,11 +17,7 @@ func Start(cfg Config) error {
17 cfg.runners = make(map[string]Runner)
18
19 for modName, rawMsg := range cfg.Modules {
20 - mod, ok := modules[modName]
21 - if !ok {
22 - return fmt.Errorf("unrecognized module: %s", modName)
23 - }
24 - val, err := LoadModule(mod, rawMsg)
20 + val, err := LoadModule(modName, rawMsg)
21 if err != nil {
22 return fmt.Errorf("loading module '%s': %v", modName, err)
23 }
@@ -68,14 +64,17 @@ type Config struct {
64 type Duration time.Duration
65
66 // UnmarshalJSON satisfies json.Unmarshaler.
71 -func (d *Duration) UnmarshalJSON(b []byte) (err error) {
67 +func (d *Duration) UnmarshalJSON(b []byte) error {
68 dd, err := time.ParseDuration(strings.Trim(string(b), `"`))
69 + if err != nil {
70 + return err
71 + }
72 cd := Duration(dd)
73 d = &cd
75 - return
74 + return nil
75 }
76
77 // MarshalJSON satisfies json.Marshaler.
79 -func (d Duration) MarshalJSON() (b []byte, err error) {
78 +func (d Duration) MarshalJSON() ([]byte, error) {
79 return []byte(fmt.Sprintf(`"%s"`, time.Duration(d).String())), nil
80 }
cmd/caddy2/main.go
+2
@@ -9,6 +9,8 @@ import (
9
10 // this is where modules get plugged in
11 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
12 + _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
13 + _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
14 _ "bitbucket.org/lightcodelabs/dynamicconfig"
15 )
16
listeners.go
+3 -3
@@ -8,8 +8,8 @@ import (
8 )
9
10 // Listen returns a listener suitable for use in a Caddy module.
11 -func Listen(proto, addr string) (net.Listener, error) {
12 - lnKey := proto + "/" + addr
11 +func Listen(network, addr string) (net.Listener, error) {
12 + lnKey := network + "/" + addr
13
14 listenersMu.Lock()
15 defer listenersMu.Unlock()
@@ -20,7 +20,7 @@ func Listen(proto, addr string) (net.Listener, error) {
20 }
21
22 // or, create new one and save it
23 - ln, err := net.Listen(proto, addr)
23 + ln, err := net.Listen(network, addr)
24 if err != nil {
25 return nil, err
26 }
modules.go
+39 -3
@@ -43,7 +43,7 @@ func GetModule(name string) (Module, error) {
43
44 // GetModules returns all modules in the given scope/namespace.
45 // For example, a scope of "foo" returns modules named "foo.bar",
46 -// "foo.lor", but not "bar", "foo.bar.lor", etc. An empty scope
46 +// "foo.lee", but not "bar", "foo.bar.lee", etc. An empty scope
47 // returns top-level modules, for example "foo" or "bar". Partial
48 // scopes are not matched (i.e. scope "foo.ba" does not match
49 // name "foo.bar").
@@ -111,9 +111,16 @@ func Modules() []string {
111 // value, it is converted to one so that it is unmarshaled
112 // into the underlying concrete type. If mod.New is nil, an
113 // error is returned.
114 -func LoadModule(mod Module, rawMsg json.RawMessage) (interface{}, error) {
114 +func LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
115 + modulesMu.Lock()
116 + mod, ok := modules[name]
117 + modulesMu.Unlock()
118 + if !ok {
119 + return nil, fmt.Errorf("unknown module: %s", name)
120 + }
121 +
122 if mod.New == nil {
116 - return nil, fmt.Errorf("no constructor")
123 + return nil, fmt.Errorf("module '%s' has no constructor", mod.Name)
124 }
125
126 val, err := mod.New()
@@ -134,6 +141,35 @@ func LoadModule(mod Module, rawMsg json.RawMessage) (interface{}, error) {
141 return val, nil
142 }
143
144 +// LoadModuleInlineName loads a module from a JSON raw message which
145 +// decodes to a map[string]interface{}, and where one of the keys is
146 +// "_module", which indicates the module name and which be found in
147 +// the given scope.
148 +//
149 +// This allows modules to be decoded into their concrete types and
150 +// used when their names cannot be the unique key in a map, such as
151 +// when there are multiple instances in the map or it appears in an
152 +// array (where there are no custom keys).
153 +func LoadModuleInlineName(moduleScope string, raw json.RawMessage) (interface{}, error) {
154 + var tmp map[string]interface{}
155 + err := json.Unmarshal(raw, &tmp)
156 + if err != nil {
157 + return nil, err
158 + }
159 +
160 + moduleName, ok := tmp["_module"].(string)
161 + if !ok || moduleName == "" {
162 + return nil, fmt.Errorf("module name not specified")
163 + }
164 +
165 + val, err := LoadModule(moduleScope+"."+moduleName, raw)
166 + if err != nil {
167 + return nil, fmt.Errorf("loading module '%s': %v", moduleName, err)
168 + }
169 +
170 + return val, nil
171 +}
172 +
173 var (
174 modules = make(map[string]Module)
175 modulesMu sync.Mutex
modules/caddyhttp/caddyhttp.go
+168 -11
@@ -2,6 +2,7 @@ package caddyhttp
2
3 import (
4 "context"
5 + "encoding/json"
6 "fmt"
7 "log"
8 "net"
@@ -30,23 +31,56 @@ type httpModuleConfig struct {
31 }
32
33 func (hc *httpModuleConfig) Run() error {
33 - // fmt.Printf("RUNNING: %#v\n", hc)
34 + // TODO: Either prevent overlapping listeners on different servers, or combine them into one
35 +
36 + // TODO: A way to loop requests back through, so have them start the matching over again, but keeping any mutations
37
38 for _, srv := range hc.Servers {
39 + // set up the routes
40 + for i, route := range srv.Routes {
41 + // matchers
42 + for modName, rawMsg := range route.Matchers {
43 + val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
44 + if err != nil {
45 + return fmt.Errorf("loading matcher module '%s': %v", modName, err)
46 + }
47 + srv.Routes[i].matchers = append(srv.Routes[i].matchers, val.(RouteMatcher))
48 + }
49 +
50 + // middleware
51 + for j, rawMsg := range route.Apply {
52 + mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg)
53 + if err != nil {
54 + return fmt.Errorf("loading middleware module in position %d: %v", j, err)
55 + }
56 + srv.Routes[i].middleware = append(srv.Routes[i].middleware, mid.(MiddlewareHandler))
57 + }
58 +
59 + // responder
60 + if route.Respond != nil {
61 + resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond)
62 + if err != nil {
63 + return fmt.Errorf("loading responder module: %v", err)
64 + }
65 + srv.Routes[i].responder = resp.(Handler)
66 + }
67 + }
68 +
69 s := &http.Server{
70 ReadTimeout: time.Duration(srv.ReadTimeout),
71 ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
72 + Handler: srv,
73 }
74
75 for _, lnAddr := range srv.Listen {
42 - proto, addrs, err := parseListenAddr(lnAddr)
76 + network, addrs, err := parseListenAddr(lnAddr)
77 if err != nil {
78 return fmt.Errorf("parsing listen address '%s': %v", lnAddr, err)
79 }
80 for _, addr := range addrs {
47 - ln, err := caddy2.Listen(proto, addr)
81 + ln, err := caddy2.Listen(network, addr)
82 if err != nil {
49 - return fmt.Errorf("%s: listening on %s: %v", proto, addr, err)
83 + return fmt.Errorf("%s: listening on %s: %v", network, addr, err)
84 }
85 go s.Serve(ln)
86 hc.servers = append(hc.servers, s)
@@ -67,10 +101,117 @@ func (hc *httpModuleConfig) Cancel() error {
101 return nil
102 }
103
70 -func parseListenAddr(a string) (proto string, addrs []string, err error) {
71 - proto = "tcp"
104 +type httpServerConfig struct {
105 + Listen []string `json:"listen"`
106 + ReadTimeout caddy2.Duration `json:"read_timeout"`
107 + ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
108 + HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
109 + Routes []serverRoute `json:"routes"`
110 +}
111 +
112 +func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
113 + var mid []Middleware // TODO: see about using make() for performance reasons
114 + var responder Handler
115 + mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
116 +
117 + for _, route := range s.Routes {
118 + matched := len(route.matchers) == 0
119 + for _, m := range route.matchers {
120 + if m.Match(r) {
121 + matched = true
122 + break
123 + }
124 + }
125 + if !matched {
126 + continue
127 + }
128 + for _, m := range route.middleware {
129 + mid = append(mid, func(next HandlerFunc) HandlerFunc {
130 + return func(w http.ResponseWriter, r *http.Request) error {
131 + return m.ServeHTTP(mrw, r, next)
132 + }
133 + })
134 + }
135 + if responder == nil {
136 + responder = route.responder
137 + }
138 + }
139 +
140 + // build the middleware stack, with the responder at the end
141 + stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
142 + if responder == nil {
143 + return nil
144 + }
145 + mrw.allowWrites = true
146 + return responder.ServeHTTP(w, r)
147 + })
148 + for i := len(mid) - 1; i >= 0; i-- {
149 + stack = mid[i](stack)
150 + }
151 +
152 + err := stack.ServeHTTP(w, r)
153 + if err != nil {
154 + // TODO: error handling
155 + log.Printf("[ERROR] TODO: error handling: %v", err)
156 + }
157 +}
158 +
159 +type serverRoute struct {
160 + Matchers map[string]json.RawMessage `json:"match"`
161 + Apply []json.RawMessage `json:"apply"`
162 + Respond json.RawMessage `json:"respond"`
163 +
164 + // decoded values
165 + matchers []RouteMatcher
166 + middleware []MiddlewareHandler
167 + responder Handler
168 +}
169 +
170 +// RouteMatcher is a type that can match to a request.
171 +// A route matcher MUST NOT modify the request.
172 +type RouteMatcher interface {
173 + Match(*http.Request) bool
174 +}
175 +
176 +// Middleware chains one Handler to the next by being passed
177 +// the next Handler in the chain.
178 +type Middleware func(HandlerFunc) HandlerFunc
179 +
180 +// MiddlewareHandler is a Handler that includes a reference
181 +// to the next middleware handler in the chain. Middleware
182 +// handlers MUST NOT call Write() or WriteHeader() on the
183 +// response writer; doing so will panic. See Handler godoc
184 +// for more information.
185 +type MiddlewareHandler interface {
186 + ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
187 +}
188 +
189 +// Handler is like http.Handler except ServeHTTP may return an error.
190 +//
191 +// Middleware and responder handlers both implement this method.
192 +// Middleware must not call Write or WriteHeader on the ResponseWriter;
193 +// doing so will cause a panic. Responders should write to the response
194 +// if there was not an error.
195 +//
196 +// If any handler encounters an error, it should be returned for proper
197 +// handling. Return values should be propagated down the middleware chain
198 +// by returning it unchanged.
199 +type Handler interface {
200 + ServeHTTP(http.ResponseWriter, *http.Request) error
201 +}
202 +
203 +// HandlerFunc is a convenience type like http.HandlerFunc.
204 +type HandlerFunc func(http.ResponseWriter, *http.Request) error
205 +
206 +// ServeHTTP implements the Handler interface.
207 +func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
208 + return f(w, r)
209 +}
210 +
211 +func parseListenAddr(a string) (network string, addrs []string, err error) {
212 + network = "tcp"
213 if idx := strings.Index(a, "/"); idx >= 0 {
73 - proto = strings.ToLower(strings.TrimSpace(a[:idx]))
214 + network = strings.ToLower(strings.TrimSpace(a[:idx]))
215 a = a[idx+1:]
216 }
217 var host, port string
@@ -101,8 +242,24 @@ func parseListenAddr(a string) (proto string, addrs []string, err error) {
242 return
243 }
244
104 -type httpServerConfig struct {
105 - Listen []string `json:"listen"`
106 - ReadTimeout caddy2.Duration `json:"read_timeout"`
107 - ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
245 +type middlewareResponseWriter struct {
246 + *ResponseWriterWrapper
247 + allowWrites bool
248 }
249 +
250 +func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
251 + if !mrw.allowWrites {
252 + panic("WriteHeader: middleware cannot write to the response")
253 + }
254 + mrw.ResponseWriterWrapper.WriteHeader(statusCode)
255 +}
256 +
257 +func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
258 + if !mrw.allowWrites {
259 + panic("Write: middleware cannot write to the response")
260 + }
261 + return mrw.ResponseWriterWrapper.Write(b)
262 +}
263 +
264 +// Interface guards
265 +var _ HTTPInterfaces = middlewareResponseWriter{}
modules/caddyhttp/caddylog/log.go new
+37
@@ -0,0 +1,37 @@
1 +package caddylog
2 +
3 +import (
4 + "log"
5 + "net/http"
6 + "time"
7 +
8 + "bitbucket.org/lightcodelabs/caddy2"
9 + "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
10 +)
11 +
12 +func init() {
13 + caddy2.RegisterModule(caddy2.Module{
14 + Name: "http.middleware.log",
15 + New: func() (interface{}, error) { return &Log{}, nil },
16 + })
17 +}
18 +
19 +// Log implements a simple logging middleware.
20 +type Log struct {
21 + Filename string
22 +}
23 +
24 +func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
25 + start := time.Now()
26 +
27 + if err := next.ServeHTTP(w, r); err != nil {
28 + return err
29 + }
30 +
31 + log.Println("latency:", time.Now().Sub(start))
32 +
33 + return nil
34 +}
35 +
36 +// Interface guard
37 +var _ caddyhttp.MiddlewareHandler = &Log{}
modules/caddyhttp/matchers.go new
+102
@@ -0,0 +1,102 @@
1 +package caddyhttp
2 +
3 +import (
4 + "net/http"
5 + "strings"
6 +
7 + "bitbucket.org/lightcodelabs/caddy2"
8 +)
9 +
10 +func init() {
11 + caddy2.RegisterModule(caddy2.Module{
12 + Name: "http.matchers.host",
13 + New: func() (interface{}, error) { return matchHost{}, nil },
14 + })
15 + caddy2.RegisterModule(caddy2.Module{
16 + Name: "http.matchers.path",
17 + New: func() (interface{}, error) { return matchPath{}, nil },
18 + })
19 + caddy2.RegisterModule(caddy2.Module{
20 + Name: "http.matchers.method",
21 + New: func() (interface{}, error) { return matchMethod{}, nil },
22 + })
23 + caddy2.RegisterModule(caddy2.Module{
24 + Name: "http.matchers.query",
25 + New: func() (interface{}, error) { return matchQuery{}, nil },
26 + })
27 + caddy2.RegisterModule(caddy2.Module{
28 + Name: "http.matchers.header",
29 + New: func() (interface{}, error) { return matchHeader{}, nil },
30 + })
31 +}
32 +
33 +// TODO: Matchers should probably support regex of some sort... performance trade-offs?
34 +
35 +type (
36 + matchHost []string
37 + matchPath []string
38 + matchMethod []string
39 + matchQuery map[string][]string
40 + matchHeader map[string][]string
41 +)
42 +
43 +func (m matchHost) Match(r *http.Request) bool {
44 + for _, host := range m {
45 + if r.Host == host {
46 + return true
47 + }
48 + }
49 + return false
50 +}
51 +
52 +func (m matchPath) Match(r *http.Request) bool {
53 + for _, path := range m {
54 + if strings.HasPrefix(r.URL.Path, path) {
55 + return true
56 + }
57 + }
58 + return false
59 +}
60 +
61 +func (m matchMethod) Match(r *http.Request) bool {
62 + for _, method := range m {
63 + if r.Method == method {
64 + return true
65 + }
66 + }
67 + return false
68 +}
69 +
70 +func (m matchQuery) Match(r *http.Request) bool {
71 + for param, vals := range m {
72 + paramVal := r.URL.Query().Get(param)
73 + for _, v := range vals {
74 + if paramVal == v {
75 + return true
76 + }
77 + }
78 + }
79 + return false
80 +}
81 +
82 +func (m matchHeader) Match(r *http.Request) bool {
83 + for field, vals := range m {
84 + fieldVals := r.Header[field]
85 + for _, fieldVal := range fieldVals {
86 + for _, v := range vals {
87 + if fieldVal == v {
88 + return true
89 + }
90 + }
91 + }
92 + }
93 + return false
94 +}
95 +
96 +var (
97 + _ RouteMatcher = matchHost{}
98 + _ RouteMatcher = matchPath{}
99 + _ RouteMatcher = matchMethod{}
100 + _ RouteMatcher = matchQuery{}
101 + _ RouteMatcher = matchHeader{}
102 +)
modules/caddyhttp/responsewriter.go new
+68
@@ -0,0 +1,68 @@
1 +package caddyhttp
2 +
3 +import (
4 + "bufio"
5 + "fmt"
6 + "net"
7 + "net/http"
8 +)
9 +
10 +// ResponseWriterWrapper wraps an underlying ResponseWriter and
11 +// promotes its Pusher/Flusher/CloseNotifier/Hijacker methods
12 +// as well. To use this type, embed a pointer to it within your
13 +// own struct type that implements the http.ResponseWriter
14 +// interface, then call methods on the embedded value. You can
15 +// make sure your type wraps correctly by asserting that it
16 +// implements the HTTPInterfaces interface.
17 +type ResponseWriterWrapper struct {
18 + http.ResponseWriter
19 +}
20 +
21 +// Hijack implements http.Hijacker. It simply calls the underlying
22 +// ResponseWriter's Hijack method if there is one, or returns an error.
23 +func (rww *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
24 + if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
25 + return hj.Hijack()
26 + }
27 + return nil, nil, fmt.Errorf("not a hijacker")
28 +}
29 +
30 +// Flush implements http.Flusher. It simply calls the underlying
31 +// ResponseWriter's Flush method if there is one, or panics.
32 +func (rww *ResponseWriterWrapper) Flush() {
33 + if f, ok := rww.ResponseWriter.(http.Flusher); ok {
34 + f.Flush()
35 + } else {
36 + panic("not a flusher")
37 + }
38 +}
39 +
40 +// CloseNotify implements http.CloseNotifier. It simply calls the underlying
41 +// ResponseWriter's CloseNotify method if there is one, or panics.
42 +func (rww *ResponseWriterWrapper) CloseNotify() <-chan bool {
43 + if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
44 + return cn.CloseNotify()
45 + }
46 + panic("not a close notifier")
47 +}
48 +
49 +// Push implements http.Pusher. It simply calls the underlying
50 +// ResponseWriter's Push method if there is one, or returns an error.
51 +func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) error {
52 + if pusher, hasPusher := rww.ResponseWriter.(http.Pusher); hasPusher {
53 + return pusher.Push(target, opts)
54 + }
55 + return fmt.Errorf("not a pusher")
56 +}
57 +
58 +// HTTPInterfaces mix all the interfaces that middleware ResponseWriters need to support.
59 +type HTTPInterfaces interface {
60 + http.ResponseWriter
61 + http.Pusher
62 + http.Flusher
63 + http.CloseNotifier
64 + http.Hijacker
65 +}
66 +
67 +// Interface guards
68 +var _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
modules/caddyhttp/staticfiles/staticfiles.go new
+28
@@ -0,0 +1,28 @@
1 +package staticfiles
2 +
3 +import (
4 + "net/http"
5 +
6 + "bitbucket.org/lightcodelabs/caddy2"
7 + "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
8 +)
9 +
10 +func init() {
11 + caddy2.RegisterModule(caddy2.Module{
12 + Name: "http.responders.static_files",
13 + New: func() (interface{}, error) { return &StaticFiles{}, nil },
14 + })
15 +}
16 +
17 +// StaticFiles implements a static file server responder for Caddy.
18 +type StaticFiles struct {
19 + Root string
20 +}
21 +
22 +func (sf StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
23 + http.FileServer(http.Dir(sf.Root)).ServeHTTP(w, r)
24 + return nil
25 +}
26 +
27 +// Interface guard
28 +var _ caddyhttp.Handler = StaticFiles{}