Skip to content

Commit

Permalink
Add Web Application Middleware
Browse files Browse the repository at this point in the history
    Add new middleware relevant for web applications intended to be
    rendered in a web browser:
    
        useIsSecureRequestMiddleware - Record on request whether it
        was delivered via a secure channel.
        useHstsMiddleware - Inform browsers that your app should only
        be visited via HTTPS.
        useStaticFilesMiddleware - Serves static files.
        useRoutingMiddleware - Adds the ability to route a request to
        a handler. Must be used with and called before
        useHandlerMiddleware.
        useBrowserHardeningMiddleware - Harden the browser environment
        your web app is rendered in.
        useHandlerMiddleware - Calls a selected handler after routing.
        Must be used with and called after useRoutingMiddleware.
    
    Middleware are specified in WebAppSettings.middleware.
    
    Refactor static file handling into a middleware. Some web
    application middleware are irrelevant for static files and with the
    previous handler implementation there was no ability to opt out of
    them. With a static files middleware we can short-circuit early and
    keep static file serving lean as we add more middleware.
    
    Add protection against trusting client-provided security headers
    from misconfigured proxies.
    
    Add conversion of exceptions to requests in middleware system. This
    enables surfacing messages on error conditions from deep within
    middleware.
    
    Add core package for exceptions and other centrally-required code.
  • Loading branch information
kyleingraham authored Nov 2, 2023
1 parent ef9b18b commit db88be6
Show file tree
Hide file tree
Showing 14 changed files with 1,162 additions and 113 deletions.
118 changes: 97 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ If you would like to see Potcake in action before reading further, take a look a
you will find demonstration apps for Potcake's features.

Each can be run by cloning this repo, navigating to the example app's base, and running `dub run`
(on macOS `MACOSX_DEPLOYMENT_TARGET=11 dub run`).
(on macOS `MACOSX_DEPLOYMENT_TARGET=12 dub run`).

[collect_static_files](examples/collect_static_files)

Expand Down Expand Up @@ -197,6 +197,10 @@ Potcake offers two ways to organize your static files (e.g. images, JavaScript,
1. In a central directory
2. In multiple directories e.g. a directory per package in a larger project.

In each case, static files will by default be served from a local directory named `'static'`
at the route prefix `'/static/'`. These settings are controlled by `WebAppSettings.rootStaticDirectory`
and `WebAppSettings.staticRoutePath` respectively.

#### Central Directory
1. Choose one directory in your project for storing all static files.
2. Set `WebAppSettings.rootStaticDirectory` to the relative path to your static directory from your compiled executable. You will need to deploy this directory alongside your executable.
Expand All @@ -222,42 +226,97 @@ postBuildCommands "\"$DUB_TARGET_PATH/$DUB_ROOT_PACKAGE_TARGET_NAME\" --collects
See [collect_static_files](examples/collect_static_files) for a demonstration.

### Middleware
Potcake provides a framework for middleware. Any middleware provided is run after a request has been routed.
Potcake provides a framework for middleware. Middleware provided can be run at any point in the request handling
process.

#### Default Middleware
In its out-of-the-box configuration Potcake provides a set of default middleware.

`useIsSecureRequestMiddleware` - Record on request whether it was delivered via a secure channel.

`useHstsMiddleware` - Inform browsers that your app should only be visited via HTTPS.

`useStaticFilesMiddleware` - Serves static files.

`useRoutingMiddleware` - Adds the ability to route a request to a handler. Must be used with and called before `useHandlerMiddleware`.

`useBrowserHardeningMiddleware` - Harden the browser environment your web app is rendered in.

`useHandlerMiddleware` - Calls a selected handler after routing. Must be used with and called after `useRoutingMiddleware`.

When adding custom middleware take care to preserve the order recorded in `WebAppSettings.middleware`.

#### Custom Middleware

Middleware can be added via `WebAppSettings.middleware`. Middleware in that list are run forward and reverse in order,
like layers in an onion i.e. middleware have the opportunity to run on a forward pass before their succeeding middleware
is run and on a reverse pass after their succeeding middleware is run. For the following middleware definition:

```d
settings.middleware = [
&A, &B, &C,
];
```

To add middleware, create a `MiddlewareDelegate`. A `MiddlewareDelegate` should accept and call a
`HTTPServerRequestDelegate` that represents the next middleware in the chain. A middleware can carry out actions both
before and after the next middleware has been called. A `MiddlewareDelegate` should return a
middleware will be run in this order:

A -> B -> C -> B -> A

Middleware can short-circuit the chain by omitting a call to their succeeding middleware. Potcake uses this to skip
routing and handling middleware when serving a static file.

To craft and add middleware, first create a `MiddlewareDelegate` or `MiddlewareFunction`. Both accept and call an
`HTTPServerRequestDelegate` that represents the next middleware in the chain. Both should return an
`HTTPServerRequestDelegate` that can be called by the middleware prior to it.

Next create a function that accepts a `WebApp` and returns a `WebApp`. This function must call `WebApp.addMiddleware` to
add your `MiddlewareDelegate`/`MiddlewareFunction`. You can safely add middleware anywhere in the middleware chain e.g.
pre-routing or directly pre-handling.

For example:

```d
import potcake.web;
HTTPServerRequestDelegate middleware(HTTPServerRequestDelegate next)
WebApp useMiddleware(WebApp webApp)
{
void middlewareDelegate(HTTPServerRequest req, HTTPServerResponse res)
HTTPServerRequestDelegate middleware(HTTPServerRequestDelegate next)
{
// Run actions prior to the next middleware.
next(req, res);
// Run actions after the next middleware.
void middlewareDelegate(HTTPServerRequest req, HTTPServerResponse res)
{
// Run actions prior to the next middleware.
next(req, res);
// Run actions after the next middleware.
}
return &middlewareDelegate;
}
return &middlewareDelegate;
webApp.addMiddleware(&middleware);
return webApp;
}
int main()
{
auto webApp = new WebApp;
webApp
auto settings = new WebAppSettings;
settings.middleware = [
&useIsSecureRequestMiddleware,
&useHstsMiddleware,
&useStaticFilesMiddleware,
&useRoutingMiddleware,
&useBrowserHardeningMiddleware,
&useMiddleware, // your middleware
&useHandlerMiddleware,
];
return new WebApp(settings)
.addRoute("/", delegate void(HTTPServerRequest req, HTTPServerResponse res) {})
.addMiddleware(&middleware);
return webApp.run();
.run();
}
```

## Settings
### Settings
On initial setup, a Potcake `WebApp` accepts a settings class in the family of `WebAppSettings`. `WebAppSettings` has
settings core to Potcake with the framework's defaults.

Expand Down Expand Up @@ -287,11 +346,22 @@ int main()
}
```

## Web App Environment
#### Core Settings
`behindSecureProxy`

Default: `false`

Signal to Potcake that your app is running behind a proxy that you trust. This matters when you are using a proxy
to provide HTTPS for your app. In order for Potcake to know that a request is secure, your proxy must signal that using
headers. If your proxy isn't taking control and ignoring those headers from clients then your app is open to being
coerced into carrying out sensitive actions over an insecure channel. Potcake forces the developer to opt in to trusting
a proxy to prevent accidentally opening up their app to exploitation.

### Web App Environment
You can control the behaviour of your web app based on the environment it's running in via the `WebAppSettings.environment`
setting. Potcake is configured out of the box to react to `WebAppEnvironment` values but any string value can be used.

## Logging
### Logging
Potcake allows for setting logging settings keyed on environment. This allows for:
- varying logging between development and production
- varying log levels and formats between loggers in an environment
Expand All @@ -312,7 +382,7 @@ settings.logging = [
];
```

## Environment Variables
### Environment Variables
Potcake provides a convenience function for fetching environment variables. The function can also optionally convert
variables to a given type. The interface is the same as the one for `std.process.environment.get`.

Expand All @@ -322,6 +392,12 @@ auto settings = new WebAppSettings;
settings.vibed.port = getEnvVar!ushort("WEB_APP_PORT", "9000");
```

Environment variables can be converted to booleans using the following rules:

```"y", "yes", "t", "true", "on", "1"``` map to `true`

```"n", "no", "f", "false", "off", "0"``` map to `false`

## FAQ
Q - Why the name Potcake?

Expand Down
26 changes: 26 additions & 0 deletions core/potcake/core/exceptions.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module potcake.core.exceptions;
@safe:

class ImproperlyConfigured : Exception
{
this(string msg, string file = __FILE__, size_t line = __LINE__) @safe
{
super(msg, file, line);
}
}

class SuspiciousOperation : Exception
{
this(string msg, string file = __FILE__, size_t line = __LINE__)
{
super(msg, file, line);
}
}

class DisallowedHost : SuspiciousOperation
{
this(string msg, string file = __FILE__, size_t line = __LINE__)
{
super(msg, file, line);
}
}
3 changes: 3 additions & 0 deletions core/potcake/core/package.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module potcake.core;

public import potcake.core.exceptions;
17 changes: 15 additions & 2 deletions dub.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ authors "Kyle Ingraham"
copyright "Copyright © 2022, Kyle Ingraham"
license "MIT"

dependency "potcake:core" version="*"
dependency "potcake:http" version="*"
dependency "potcake:web" version="*"

targetType "library"
targetName "potcake"

sourcePaths "http" "web"
importPaths "http" "web"
sourcePaths "core" "http" "web"
importPaths "core" "http" "web"

subPackage {
name "http"
description "Potcake web framework lower-level http components. Includes its vibe.d router."

dependency "pegged" version="~>0.4.6"
dependency "potcake:core" version="*"
dependency "vibe-core" version="~>2.2.0"
dependency "vibe-d:http" version="~>0.9.6"
dependency "vibe-d:inet" version="~>0.9.6"
Expand All @@ -36,6 +38,7 @@ subPackage {
description "Potcake web framework higher-level web app components."

dependency "diet-ng" version="~>1.8.1"
dependency "potcake:core" version="*"
dependency "potcake:http" version="*"
dependency "unit-threaded:assertions" version="~>2.1.6"
dependency "urllibparse" version="~>0.1.0"
Expand All @@ -45,3 +48,13 @@ subPackage {
sourcePaths "web"
importPaths "web"
}

subPackage {
name "core"
description "Potcake core components used by its subpackages."

targetType "library"

sourcePaths "core"
importPaths "core"
}
24 changes: 13 additions & 11 deletions examples/collect_static_files/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ import potcake.web;

int main(string[] args)
{
auto routes = [
route("/", &handler),
route("/diet/<int:num>/", &dietHandler),
];

auto settings = new WebAppSettings;
settings.staticDirectories = ["static_a", "static_b"];
settings.rootStaticDirectory = "staticroot";
settings.staticRoutePath = "/static/";
settings.rootRouteConfig = routes;

auto webApp = new WebApp(settings);
settings.rootRouteConfig = [
route("/", &handler),
route("/diet/<int:num>/", &dietHandler),
];
settings.environment = "production";
settings.allowedHosts["production"] = ["127.0.0.1", "localhost", "[::1]"];
settings.behindSecureProxy = true;
settings.logging["production"] = [
LoggerSetting(LogLevel.info, new VibedStdoutLogger(), FileLogger.Format.threadTime),
];
settings.vibed.accessLogToConsole = true;

return webApp
.serveStaticFiles()
.run(args); // For detection of the --collectstatic flag.
return new WebApp(settings)
.run(args); // args passed for detection of the --collectstatic flag.
}

void handler(HTTPServerRequest req, HTTPServerResponse res)
Expand Down
3 changes: 0 additions & 3 deletions examples/collect_static_files/staticroot/css/styles_a.css

This file was deleted.

3 changes: 0 additions & 3 deletions examples/collect_static_files/staticroot/css/styles_b.css

This file was deleted.

15 changes: 8 additions & 7 deletions examples/static_files/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import potcake.web;
int main()
{
auto settings = new WebAppSettings;
settings.rootStaticDirectory = "static";
settings.staticRoutePath = "/static/";
// Static files will by default be served from a local directory named 'static' at the route prefix '/static/'.
// These settings are controlled by WebAppSettings.rootStaticDirectory and WebAppSettings.staticRoutePath
// respectively. Uncomment the following lines to make adjustments to these settings:
//
// settings.rootStaticDirectory = "static";
// settings.staticRoutePath = "/static/"; // Use `staticPath` in templates to seamlessly update links to match this.

auto webApp = new WebApp(settings);
webApp
return new WebApp(settings)
.addRoute("/", &handler)
.serveStaticFiles();

return webApp.run();
.run();
}

void handler(HTTPServerRequest req, HTTPServerResponse res)
Expand Down
Loading

0 comments on commit db88be6

Please sign in to comment.