diff options
-rw-r--r-- | README.gmi | 12 | ||||
-rw-r--r-- | README.md | 12 | ||||
-rw-r--r-- | contrib/cgi/gemini.go | 4 | ||||
-rw-r--r-- | contrib/cgi/gopher.go | 4 | ||||
-rw-r--r-- | contrib/fs/dir_test.go | 4 | ||||
-rw-r--r-- | contrib/fs/file_test.go | 2 | ||||
-rw-r--r-- | contrib/fs/gemini.go | 12 | ||||
-rw-r--r-- | contrib/fs/gopher.go | 12 | ||||
-rw-r--r-- | contrib/sharedhost/replacement.go | 6 | ||||
-rw-r--r-- | contrib/sharedhost/replacement_test.go | 6 | ||||
-rw-r--r-- | contrib/tlsauth/auth_test.go | 20 | ||||
-rw-r--r-- | contrib/tlsauth/gemini.go | 12 | ||||
-rw-r--r-- | contrib/tlsauth/gemini_test.go | 20 | ||||
-rw-r--r-- | examples/cowsay/main.go | 4 | ||||
-rw-r--r-- | examples/inspectls/main.go | 4 | ||||
-rw-r--r-- | finger/serve.go | 2 | ||||
-rw-r--r-- | finger/system.go | 4 | ||||
-rw-r--r-- | gemini/roundtrip_test.go | 8 | ||||
-rw-r--r-- | gemini/serve.go | 8 | ||||
-rw-r--r-- | gopher/serve.go | 2 | ||||
-rw-r--r-- | handler.go | 34 | ||||
-rw-r--r-- | handler_test.go | 22 | ||||
-rw-r--r-- | logging/middleware.go | 6 | ||||
-rw-r--r-- | logging/middleware_test.go | 6 | ||||
-rw-r--r-- | router.go | 4 | ||||
-rw-r--r-- | router_test.go | 16 |
26 files changed, 140 insertions, 106 deletions
@@ -13,7 +13,7 @@ Gus is carefully structured as composable building blocks. The top-level package * a "Server" interface type * a "Handler" abstraction * a "Middleware" abstraction -* some useful Handler wrappers: filtering, falling through a list of handlers +* some useful Handler wrappers: a router, request filtering, falling through a list of handlers ## Protocols @@ -39,6 +39,16 @@ The gus/logging package provides everything you need to get a good basic start t * A request-logging middleware with common diagnostics (time, duration, url, status codes, response body lengths) * A simple constructor of useful default loggers at various levels. They output colorful logfmt lines to stdout. +## Routing + +The router in the gus package supports slash-delimited path pattern strings. In the segments of these patterns: + +* A "/:wildcard/" segment matches anything in that position, and captures the value as a route parameter. Or if the paramter name is omitted like "/:/", it matches anything in a single segment without capturing a paramter. +* A "/*remainder" segment is only allowed at the end and matches the rest of the path, capturing it into the paramter name. Or again, omitting a parameter name like "/*" simple matches any path suffix. +* Any other segment in the pattern must match the corresponding segment of a request exactly. + +Router also supports maintaining a list of middlewares at the router level, mounting sub-routers under a pattern, looking up the matching handler for any request, and of course acting as a Handler itself. + ## gus/contrib/* This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do. @@ -14,7 +14,7 @@ Gus is carefully structured as composable building blocks. The top-level package * a "Server" interface type * a "Handler" abstraction * a "Middleware" abstraction -* some useful Handler wrappers: filtering, falling through a list of handlers +* some useful Handler wrappers: a router, request filtering, falling through a list of handlers ## Protocols @@ -42,6 +42,16 @@ The gus/logging package provides everything you need to get a good basic start t * A request-logging middleware with common diagnostics (time, duration, url, status codes, response body lengths) * A simple constructor of useful default loggers at various levels. They output colorful logfmt lines to stdout. +## Routing + +The router in the gus package supports slash-delimited path pattern strings. In the segments of these patterns: + +* A "/:wildcard/" segment matches anything in that position, and captures the value as a route parameter. Or if the paramter name is omitted like "/:/", it matches anything in a single segment without capturing a paramter. +* A "/*remainder" segment is only allowed at the end and matches the rest of the path, capturing it into the paramter name. Or again, omitting a parameter name like "/*" simple matches any path suffix. +* Any other segment in the pattern must match the corresponding segment of a request exactly. + +Router also supports maintaining a list of middlewares at the router level, mounting sub-routers under a pattern, looking up the matching handler for any request, and of course acting as a Handler itself. + ## gus/contrib/* This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do. diff --git a/contrib/cgi/gemini.go b/contrib/cgi/gemini.go index 8302e7e..1587037 100644 --- a/contrib/cgi/gemini.go +++ b/contrib/cgi/gemini.go @@ -17,7 +17,7 @@ import ( // the URI path. func GeminiCGIDirectory(pathRoot, fsRoot string) gus.Handler { fsRoot = strings.TrimRight(fsRoot, "/") - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, pathRoot) { return nil } @@ -43,5 +43,5 @@ func GeminiCGIDirectory(pathRoot, fsRoot string) gus.Handler { return gemini.Failure(err) } return response - } + }) } diff --git a/contrib/cgi/gopher.go b/contrib/cgi/gopher.go index 29bfdba..4378eb7 100644 --- a/contrib/cgi/gopher.go +++ b/contrib/cgi/gopher.go @@ -17,7 +17,7 @@ import ( // the URI path. func GopherCGIDirectory(pathRoot, fsRoot string) gus.Handler { fsRoot = strings.TrimRight(fsRoot, "/") - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, pathRoot) { return nil } @@ -41,5 +41,5 @@ func GopherCGIDirectory(pathRoot, fsRoot string) gus.Handler { } return gopher.File(0, stdout) - } + }) } diff --git a/contrib/fs/dir_test.go b/contrib/fs/dir_test.go index 6109a3c..9c6770d 100644 --- a/contrib/fs/dir_test.go +++ b/contrib/fs/dir_test.go @@ -47,7 +47,7 @@ func TestDirectoryDefault(t *testing.T) { require.Nil(t, err) request := &gus.Request{URL: u} - response := handler(context.Background(), request) + response := handler.Handle(context.Background(), request) if response == nil { assert.Equal(t, test.status, gemini.StatusNotFound) @@ -108,7 +108,7 @@ func TestDirectoryListing(t *testing.T) { require.Nil(t, err) request := &gus.Request{URL: u} - response := handler(context.Background(), request) + response := handler.Handle(context.Background(), request) if response == nil { assert.Equal(t, test.status, gemini.StatusNotFound) diff --git a/contrib/fs/file_test.go b/contrib/fs/file_test.go index f97b66b..3949b83 100644 --- a/contrib/fs/file_test.go +++ b/contrib/fs/file_test.go @@ -58,7 +58,7 @@ func TestFileHandler(t *testing.T) { require.Nil(t, err) request := &gus.Request{URL: u} - response := handler(context.Background(), request) + response := handler.Handle(context.Background(), request) if response == nil { assert.Equal(t, test.status, gemini.StatusNotFound) diff --git a/contrib/fs/gemini.go b/contrib/fs/gemini.go index 4193493..15677f1 100644 --- a/contrib/fs/gemini.go +++ b/contrib/fs/gemini.go @@ -14,7 +14,7 @@ import ( // // It only serves responses for paths which do not correspond to directories on disk. func GeminiFileHandler(fileSystem fs.FS) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { filepath, file, err := ResolveFile(request, fileSystem) if err != nil { return gemini.Failure(err) @@ -25,7 +25,7 @@ func GeminiFileHandler(fileSystem fs.FS) gus.Handler { } return gemini.Success(mediaType(filepath), file) - } + }) } // GeminiDirectoryDefault serves up default files for directory path requests. @@ -42,7 +42,7 @@ func GeminiFileHandler(fileSystem fs.FS) gus.Handler { // It requires that files from the provided fs.FS implement fs.ReadDirFile. If they // don't, it will produce nil responses for any directory paths. func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { dirpath, dir, response := handleDirGemini(request, fileSystem) if response != nil { return response @@ -61,7 +61,7 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { } return gemini.Success(mediaType(filepath), file) - } + }) } // GeminiDirectoryListing produces a listing of the contents of any requested directories. @@ -78,7 +78,7 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { // The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The // template is then processed with RenderDirectoryListing. func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { dirpath, dir, response := handleDirGemini(request, fileSystem) if response != nil { return response @@ -97,7 +97,7 @@ func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) gus.H } return gemini.Success("text/gemini", body) - } + }) } // DefaultGeminiDirectoryList is a template which renders a reasonable gemtext dir list. diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go index 7b0d8bd..f63785c 100644 --- a/contrib/fs/gopher.go +++ b/contrib/fs/gopher.go @@ -16,7 +16,7 @@ import ( // // It only serves responses for paths which correspond to files, not directories. func GopherFileHandler(fileSystem fs.FS) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { filepath, file, err := ResolveFile(request, fileSystem) if err != nil { return gopher.Error(err).Response() @@ -27,7 +27,7 @@ func GopherFileHandler(fileSystem fs.FS) gus.Handler { } return gopher.File(GuessGopherItemType(filepath), file) - } + }) } // GopherDirectoryDefault serves up default files for directory path requests. @@ -40,7 +40,7 @@ func GopherFileHandler(fileSystem fs.FS) gus.Handler { // It requires that files from the provided fs.FS implement fs.ReadDirFile. If // they don't, it will produce nil responses for all directory paths. func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { dirpath, dir, err := ResolveDirectory(request, fileSystem) if err != nil { return gopher.Error(err).Response() @@ -59,7 +59,7 @@ func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { } return gopher.File(gopher.MenuType, file) - } + }) } // GopherDirectoryListing produces a listing of the contents of any requested directories. @@ -72,7 +72,7 @@ func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { // A template may be nil, in which case DefaultGopherDirectoryList is used instead. The // template is then processed with RenderDirectoryListing. func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { dirpath, dir, err := ResolveDirectory(request, fileSystem) if err != nil { return gopher.Error(err).Response() @@ -91,7 +91,7 @@ func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) gus.Handle } return gopher.File(gopher.MenuType, body) - } + }) } // GopherTemplateFunctions is a map for templates providing useful functions for gophermaps. diff --git a/contrib/sharedhost/replacement.go b/contrib/sharedhost/replacement.go index 1fb2a0d..9267530 100644 --- a/contrib/sharedhost/replacement.go +++ b/contrib/sharedhost/replacement.go @@ -19,14 +19,14 @@ import ( // "users/", "domain.com/~jim/index.gmi" maps to "domain.com/users/jim/index.gmi". func ReplaceTilde(replacement string) gus.Middleware { return func(inner gus.Handler) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { if len(request.Path) > 1 && request.Path[0] == '/' && request.Path[1] == '~' { request = cloneRequest(request) request.Path = "/" + replacement + request.Path[2:] } - return inner(ctx, request) - } + return inner.Handle(ctx, request) + }) } } diff --git a/contrib/sharedhost/replacement_test.go b/contrib/sharedhost/replacement_test.go index cab80bb..67c3754 100644 --- a/contrib/sharedhost/replacement_test.go +++ b/contrib/sharedhost/replacement_test.go @@ -43,12 +43,12 @@ func TestReplaceTilde(t *testing.T) { replacer := sharedhost.ReplaceTilde(test.replacement) request := &gus.Request{URL: u} - handler := replacer(func(_ context.Context, request *gus.Request) *gus.Response { + handler := replacer(gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { assert.Equal(t, test.replacedPath, request.Path) return &gus.Response{} - }) + })) - handler(context.Background(), request) + handler.Handle(context.Background(), request) // original request was unmodified assert.Equal(t, originalPath, request.Path) diff --git a/contrib/tlsauth/auth_test.go b/contrib/tlsauth/auth_test.go index 30b63f5..3cbc106 100644 --- a/contrib/tlsauth/auth_test.go +++ b/contrib/tlsauth/auth_test.go @@ -24,7 +24,7 @@ func TestIdentify(t *testing.T) { server, client, clientCert := setup(t, "testdata/server.crt", "testdata/server.key", "testdata/client1.crt", "testdata/client1.key", - func(_ context.Context, request *gus.Request) *gus.Response { + gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { invoked = true ident := tlsauth.Identity(request) @@ -33,7 +33,7 @@ func TestIdentify(t *testing.T) { } return nil - }, + }), ) leafCert, err := x509.ParseCertificate(clientCert.Certificate[0]) require.Nil(t, err) @@ -51,15 +51,15 @@ func TestRequiredAuth(t *testing.T) { invoked1 := false invoked2 := false - handler1 := func(_ context.Context, request *gus.Request) *gus.Response { + handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { invoked1 = true return gemini.Success("", &bytes.Buffer{}) - } + }) - handler2 := func(_ context.Context, request *gus.Request) *gus.Response { + handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { invoked2 = true return gemini.Success("", &bytes.Buffer{}) - } + }) authMiddleware := gus.Filter(tlsauth.RequiredAuth(tlsauth.Allow), nil) @@ -94,19 +94,19 @@ func TestOptionalAuth(t *testing.T) { invoked1 := false invoked2 := false - handler1 := func(_ context.Context, request *gus.Request) *gus.Response { + handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, "/one") { return nil } invoked1 = true return gemini.Success("", &bytes.Buffer{}) - } + }) - handler2 := func(_ context.Context, request *gus.Request) *gus.Response { + handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { invoked2 = true return gemini.Success("", &bytes.Buffer{}) - } + }) mw := gus.Filter(tlsauth.OptionalAuth(tlsauth.Reject), nil) handler := gus.FallthroughHandler(mw(handler1), mw(handler2)) diff --git a/contrib/tlsauth/gemini.go b/contrib/tlsauth/gemini.go index 0db89de..40bee9e 100644 --- a/contrib/tlsauth/gemini.go +++ b/contrib/tlsauth/gemini.go @@ -14,7 +14,7 @@ import ( // not pass the approver it will be rejected with "62 certificate invalid". func GeminiAuth(approver Approver) gus.Middleware { return func(inner gus.Handler) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { identity := Identity(request) if identity == nil { return geminiMissingCert(ctx, request) @@ -23,8 +23,8 @@ func GeminiAuth(approver Approver) gus.Middleware { return geminiCertNotAuthorized(ctx, request) } - return inner(ctx, request) - } + return inner.Handle(ctx, request) + }) } } @@ -35,14 +35,14 @@ func GeminiAuth(approver Approver) gus.Middleware { // certificate, but it fails the approval. func GeminiOptionalAuth(approver Approver) gus.Middleware { return func(inner gus.Handler) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { identity := Identity(request) if identity != nil && !approver(identity) { return geminiCertNotAuthorized(ctx, request) } - return inner(ctx, request) - } + return inner.Handle(ctx, request) + }) } } diff --git a/contrib/tlsauth/gemini_test.go b/contrib/tlsauth/gemini_test.go index 8f1efda..7823de6 100644 --- a/contrib/tlsauth/gemini_test.go +++ b/contrib/tlsauth/gemini_test.go @@ -14,30 +14,30 @@ import ( ) func TestGeminiAuth(t *testing.T) { - handler1 := func(_ context.Context, request *gus.Request) *gus.Response { + handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, "/one") { return nil } return gemini.Success("", &bytes.Buffer{}) - } - handler2 := func(_ context.Context, request *gus.Request) *gus.Response { + }) + handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, "/two") { return nil } return gemini.Success("", &bytes.Buffer{}) - } - handler3 := func(_ context.Context, request *gus.Request) *gus.Response { + }) + handler3 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, "/three") { return nil } return gemini.Success("", &bytes.Buffer{}) - } - handler4 := func(_ context.Context, request *gus.Request) *gus.Response { + }) + handler4 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { return gemini.Success("", &bytes.Buffer{}) - } + }) handler := gus.FallthroughHandler( tlsauth.GeminiAuth(tlsauth.Allow)(handler1), @@ -74,12 +74,12 @@ func TestGeminiAuth(t *testing.T) { func TestGeminiOptionalAuth(t *testing.T) { pathHandler := func(path string) gus.Handler { - return func(_ context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response { if !strings.HasPrefix(request.Path, path) { return nil } return gemini.Success("", &bytes.Buffer{}) - } + }) } handler := gus.FallthroughHandler( diff --git a/examples/cowsay/main.go b/examples/cowsay/main.go index 4a3f980..93f50d8 100644 --- a/examples/cowsay/main.go +++ b/examples/cowsay/main.go @@ -36,7 +36,7 @@ func main() { server.Serve() } -func cowsayHandler(ctx context.Context, req *gus.Request) *gus.Response { +var cowsayHandler = gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { // prompt for a query if there is none already if req.RawQuery == "" { return gemini.Input("enter a phrase") @@ -82,7 +82,7 @@ func cowsayHandler(ctx context.Context, req *gus.Request) *gus.Response { bytes.NewBufferString("\n```\n=> . again"), ) return gemini.Success("text/gemini", out) -} +}) func envConfig() (string, string) { certfile, ok := os.LookupEnv("SERVER_CERTIFICATE") diff --git a/examples/inspectls/main.go b/examples/inspectls/main.go index ce82f43..485b84e 100644 --- a/examples/inspectls/main.go +++ b/examples/inspectls/main.go @@ -54,11 +54,11 @@ func envConfig() (string, string) { return certfile, keyfile } -func inspectHandler(ctx context.Context, req *gus.Request) *gus.Response { +var inspectHandler = gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { // build and return a ```-wrapped description of the connection TLS state body := "```\n" + displayTLSState(req.TLSState) + "\n```" return gemini.Success("text/gemini", bytes.NewBufferString(body)) -} +}) func displayTLSState(state *tls.ConnectionState) string { builder := &strings.Builder{} diff --git a/finger/serve.go b/finger/serve.go index 8623de5..68b0fa5 100644 --- a/finger/serve.go +++ b/finger/serve.go @@ -58,7 +58,7 @@ func (fs *fingerServer) handleConn(conn net.Conn) { _, _ = fmt.Fprint(conn, "Error handling request.\r\n") } }() - response := fs.handler(fs.Ctx, request) + response := fs.handler.Handle(fs.Ctx, request) if response == nil { response = Error("No result found.") } diff --git a/finger/system.go b/finger/system.go index 7112967..4bcf573 100644 --- a/finger/system.go +++ b/finger/system.go @@ -14,7 +14,7 @@ var ListingDenied = errors.New("Finger online user list denied.") // SystemFinger handles finger requests by invoking the finger(1) command-line utility. func SystemFinger(allowListings bool) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { fingerPath, err := exec.LookPath("finger") if err != nil { _ = request.Server.LogError( @@ -44,5 +44,5 @@ func SystemFinger(allowListings bool) gus.Handler { return Error(err.Error()) } return Success(outbuf) - } + }) } diff --git a/gemini/roundtrip_test.go b/gemini/roundtrip_test.go index a9d9b59..e8d2b48 100644 --- a/gemini/roundtrip_test.go +++ b/gemini/roundtrip_test.go @@ -20,9 +20,9 @@ func TestRoundTrip(t *testing.T) { tlsConf, err := gemini.FileTLS("./testdata/server.crt", "./testdata/server.key") require.Nil(t, err) - handler := func(ctx context.Context, req *gus.Request) *gus.Response { + handler := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { return gemini.Success("text/gemini", bytes.NewBufferString("you've found my page")) - } + }) server, err := gemini.NewServer(context.Background(), "localhost", "tcp", "127.0.0.1:0", handler, nil, tlsConf) require.Nil(t, err) @@ -54,7 +54,7 @@ func TestTitanRequest(t *testing.T) { require.Nil(t, err) invoked := false - handler := func(ctx context.Context, request *gus.Request) *gus.Response { + handler := gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { invoked = true body := ctx.Value(gemini.TitanRequestBody) @@ -67,7 +67,7 @@ func TestTitanRequest(t *testing.T) { assert.Equal(t, "the request body\n", string(bodyBytes)) return gemini.Success("", nil) - } + }) server, err := gemini.NewServer(context.Background(), "localhost", "tcp", "127.0.0.1:0", handler, nil, tlsConf) require.Nil(t, err) diff --git a/gemini/serve.go b/gemini/serve.go index 60e0242..2f93153 100644 --- a/gemini/serve.go +++ b/gemini/serve.go @@ -94,7 +94,7 @@ func (s *server) handleConn(conn net.Conn) { _, _ = io.Copy(conn, NewResponseReader(Failure(err))) } }() - response = s.handler(ctx, request) + response = s.handler.Handle(ctx, request) if response == nil { response = NotFound("Resource does not exist.") } @@ -127,12 +127,12 @@ func sizeParam(path string) (int, error) { // Filtered requests will be turned away with a 53 response "proxy request refused". func GeminiOnly(allowTitan bool) gus.Middleware { return func(inner gus.Handler) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { if request.Scheme == "gemini" || (allowTitan && request.Scheme == "titan") { - return inner(ctx, request) + return inner.Handle(ctx, request) } return RefuseProxy("Non-gemini protocol requests are not supported.") - } + }) } } diff --git a/gopher/serve.go b/gopher/serve.go index 84745d7..572fa55 100644 --- a/gopher/serve.go +++ b/gopher/serve.go @@ -61,7 +61,7 @@ func (gs *gopherServer) handleConn(conn net.Conn) { _, _ = io.Copy(conn, rdr) } }() - response = gs.handler(gs.Ctx, request) + response = gs.handler.Handle(gs.Ctx, request) if response == nil { response = Error(errors.New("Resource does not exist.")).Response() } @@ -2,11 +2,25 @@ package gus import "context" -// Handler is a function which can turn a request into a response. +// Handler is a type which can turn a request into a response. // -// A Handler can return a nil response, in which case the Server is expected +// Handle may return a nil response, in which case the Server is expected // to build the protocol-appropriate "Not Found" response. -type Handler func(context.Context, *Request) *Response +type Handler interface { + Handle(context.Context, *Request) *Response +} + +type handlerFunc func(context.Context, *Request) *Response + +// HandlerFunc is a wrapper to allow using a function as a Handler. +func HandlerFunc(f func(context.Context, *Request) *Response) Handler { + return handlerFunc(f) +} + +// Handle implements Handler. +func (f handlerFunc) Handle(ctx context.Context, request *Request) *Response { + return f(ctx, request) +} // Middleware is a handler decorator. // @@ -19,14 +33,14 @@ type Middleware func(Handler) Handler // The returned handler will invoke each of the passed-in handlers in order, // stopping when it receives a non-nil response. func FallthroughHandler(handlers ...Handler) Handler { - return func(ctx context.Context, request *Request) *Response { + return HandlerFunc(func(ctx context.Context, request *Request) *Response { for _, handler := range handlers { - if response := handler(ctx, request); response != nil { + if response := handler.Handle(ctx, request); response != nil { return response } } return nil - } + }) } // Filter builds a middleware which only calls the wrapped Handler under a condition. @@ -39,14 +53,14 @@ func Filter( failure Handler, ) Middleware { return func(success Handler) Handler { - return func(ctx context.Context, request *Request) *Response { + return HandlerFunc(func(ctx context.Context, request *Request) *Response { if condition(ctx, request) { - return success(ctx, request) + return success.Handle(ctx, request) } if failure == nil { return nil } - return failure(ctx, request) - } + return failure.Handle(ctx, request) + }) } } diff --git a/handler_test.go b/handler_test.go index a83ef3b..18ef562 100644 --- a/handler_test.go +++ b/handler_test.go @@ -13,19 +13,19 @@ import ( ) func TestFallthrough(t *testing.T) { - h1 := func(ctx context.Context, req *gus.Request) *gus.Response { + h1 := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { if req.Path == "/one" { return gemini.Success("text/gemini", bytes.NewBufferString("one")) } return nil - } + }) - h2 := func(ctx context.Context, req *gus.Request) *gus.Response { + h2 := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { if req.Path == "/two" { return gemini.Success("text/gemini", bytes.NewBufferString("two")) } return nil - } + }) fth := gus.FallthroughHandler(h1, h2) @@ -34,7 +34,7 @@ func TestFallthrough(t *testing.T) { t.Fatalf("url.Parse: %s", err.Error()) } - resp := fth(context.Background(), &gus.Request{URL: u}) + resp := fth.Handle(context.Background(), &gus.Request{URL: u}) if resp.Status != gemini.StatusSuccess { t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status) @@ -57,7 +57,7 @@ func TestFallthrough(t *testing.T) { t.Fatalf("url.Parse: %s", err.Error()) } - resp = fth(context.Background(), &gus.Request{URL: u}) + resp = fth.Handle(context.Background(), &gus.Request{URL: u}) if resp.Status != gemini.StatusSuccess { t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status) @@ -80,7 +80,7 @@ func TestFallthrough(t *testing.T) { t.Fatalf("url.Parse: %s", err.Error()) } - resp = fth(context.Background(), &gus.Request{URL: u}) + resp = fth.Handle(context.Background(), &gus.Request{URL: u}) if resp != nil { t.Errorf("expected nil, got %+v", resp) @@ -91,9 +91,9 @@ func TestFilter(t *testing.T) { pred := func(ctx context.Context, req *gus.Request) bool { return strings.HasPrefix(req.Path, "/allow") } - base := func(ctx context.Context, req *gus.Request) *gus.Response { + base := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { return gemini.Success("text/gemini", bytes.NewBufferString("allowed!")) - } + }) handler := gus.Filter(pred, nil)(base) u, err := url.Parse("gemini://test.local/allow/please") @@ -101,7 +101,7 @@ func TestFilter(t *testing.T) { t.Fatalf("url.Parse: %s", err.Error()) } - resp := handler(context.Background(), &gus.Request{URL: u}) + resp := handler.Handle(context.Background(), &gus.Request{URL: u}) if resp.Status != gemini.StatusSuccess { t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status) } @@ -111,7 +111,7 @@ func TestFilter(t *testing.T) { t.Fatalf("url.Parse: %s", err.Error()) } - resp = handler(context.Background(), &gus.Request{URL: u}) + resp = handler.Handle(context.Background(), &gus.Request{URL: u}) if resp != nil { t.Errorf("expected nil, got %+v", resp) } diff --git a/logging/middleware.go b/logging/middleware.go index 5442203..4e23c1e 100644 --- a/logging/middleware.go +++ b/logging/middleware.go @@ -11,14 +11,14 @@ import ( func LogRequests(logger Logger) gus.Middleware { return func(inner gus.Handler) gus.Handler { - return func(ctx context.Context, request *gus.Request) *gus.Response { - response := inner(ctx, request) + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { + response := inner.Handle(ctx, request) if response != nil { response.Body = loggingBody(logger, request, response) } return response - } + }) } } diff --git a/logging/middleware_test.go b/logging/middleware_test.go index 288c960..76406ef 100644 --- a/logging/middleware_test.go +++ b/logging/middleware_test.go @@ -14,11 +14,11 @@ import ( func TestLogRequests(t *testing.T) { logger := logRecorder{} - handler := logging.LogRequests(&logger)(func(_ context.Context, _ *gus.Request) *gus.Response { + handler := logging.LogRequests(&logger)(gus.HandlerFunc(func(_ context.Context, _ *gus.Request) *gus.Response { return &gus.Response{} - }) + })) - response := handler(context.Background(), &gus.Request{}) + response := handler.Handle(context.Background(), &gus.Request{}) _, err := io.ReadAll(response.Body) assert.Nil(t, err) @@ -50,10 +50,10 @@ func (r Router) Handler(ctx context.Context, request *Request) *Response { return nil } - return handler(context.WithValue(ctx, routeParamsKey, params), request) + return handler.Handle(context.WithValue(ctx, routeParamsKey, params), request) } -// Match returns the matched handler and captured path parameters, or nils. +// Match returns the matched handler and captured path parameters, or (nil, nil). // // The returned handlers will be wrapped with any middleware attached to the router. func (r Router) Match(request *Request) (Handler, map[string]string) { diff --git a/router_test.go b/router_test.go index 6f9c915..bfc48bd 100644 --- a/router_test.go +++ b/router_test.go @@ -13,24 +13,24 @@ import ( "tildegit.org/tjp/gus/gemini" ) -func h1(_ context.Context, _ *gus.Request) *gus.Response { +var h1 = gus.HandlerFunc(func(_ context.Context, _ *gus.Request) *gus.Response { return gemini.Success("", &bytes.Buffer{}) -} +}) func mw1(h gus.Handler) gus.Handler { - return func(ctx context.Context, req *gus.Request) *gus.Response { - resp := h(ctx, req) + return gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { + resp := h.Handle(ctx, req) resp.Body = io.MultiReader(resp.Body, bytes.NewBufferString("\nmiddleware 1")) return resp - } + }) } func mw2(h gus.Handler) gus.Handler { - return func(ctx context.Context, req *gus.Request) *gus.Response { - resp := h(ctx, req) + return gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response { + resp := h.Handle(ctx, req) resp.Body = io.MultiReader(resp.Body, bytes.NewBufferString("\nmiddleware 2")) return resp - } + }) } func TestRouterUse(t *testing.T) { |