When using Shelf such things are handled by Shelf Middleware.
A Shelf Middleware is nothing more than a Shelf Handler that consumes a Handler and return another Handler, meaning you can chain a whole bunch of Middleware together for very complex functionality. In this article I'm going to use the createMiddleware() method to create two Middleware's:
- One that takes care of CORS, ensuring that all CORS preflight requests are handled and that all responses have the
- One that checks if a request bears a valid token, and if not, show the user an interface where a token can be obtained
In my previous post I touched on how to use Shelf and how to unit test Shelf request handlers, so take a look at that to learn some more about Shelf, since I'm not going to spend much time on Shelf itself in this post. I'll mainly focus on the Middleware functionality.
Before we move on, it is probably a good idea to clone the shelfmiddleware example code.
Dependencies - pubspec.yamlHere's what we need:
A quick rundown:
- args - parse command-line arguments
- Shelf - Shelf itself
- Shelf Route - provides means to create routes
Nothing too surprising here, I hope. :)
Where it all begins - bin/shelfmiddleware.dartAll roads lead to
We add 3 Middleware to the
Pipelinedefined at line16, two of which are always added (
addCORSHeaders), and one which we only add if the server is started without the
--disableTokenscommand-line option. If the server is started with the
--disableTokenscommand-line option, then all tokens are considered valid. This is here to demonstrate how easy it is to add Middleware, depending on various circumstances. It is a very flexible system the Shelf people have come up with.
The three Middleware we use are:
shelf.logRequests()at line 17 - log all requests and output to STDOUT
addCORSHeadersat line 18 - handle CORS preflight requests and add the
Access-Control-Allow-Originheader to all responses
checkToken()at line 23 - check that requests bear a valid token, and if not, return a page where clients can obtain a token
The final Middleware added to the chain is the last to handle a
shelf.Requestand the first to handle a
shelf.Response, which means that in our example a new
shelf.Requestis first intercepted by
checkToken. When a
shelf.Responseis generated, it passes back up the chain, ending with
shelf.logRequests(), which incidentally is how
shelf.logRequests()is able to tell us how long it took to complete a request to the server - it is the first to see an incoming request and the last to hand off the response.
Lets take a look at how the
CORS headers - lib/cors.dartIf you don't know what CORS is, then I suggest you go read this excellent Mozilla Developer Network article on the subject before moving on. Done now? Good.
At line 7 we create the
addCORSHeadersMiddleware using the shelf.createMiddleware() method. We tell it to hand over all
shelf.Request's to the
_options()method and all
shelf.Responseobjects to the
All we do in
_options()is return an empty
Access-Control-Allow-Originheader set if this is a preflight request, ie. the request method is
OPTIONS, or a
nullif this is not a preflight request. Returning a
nullis equivalent to saying "move on, nothing to see here", whereby the
shelf.Requestis passed on to the next Middleware or Handler in the chain.
_cors()method we grab the
shelf.Responseobject and change its headers, always adding the
And this is in its most basic form all you need to CORS enable your server. Naturally you can impose all sorts of restrictions on when and what is CORS enabled, but that is beyond the scope of this article.
Checking for valid tokens - lib/checktoken.dartBefore reading on, please do not consider this example a functional way of handling authentication. It is not. There's a Shelf package for doing authentication, aptly named Shelf Auth. This merely serves as an example on how to use Shelf Middleware, and token checking was what I could come up with.
With that out of the way, lets take a look at our
As with the
addCORSHeadersMiddleware, we use the
shelf.createMiddleware()method to create the
checkTokenMiddleware at line 8, but as you can see this one differs in that we do not set a
responseHandler, which of course is because this Middleware only cares about whether or not an incoming request bear a valid token - outgoing responses are of no importance.
The functionality of the
_passOrForbid()is mindblowingly simple: We grab the token from the
shelf.Requestobject and return
null(meaning: "all OK, pass the request on") if it is valid. If the token isn't valid, we generate a new token at line 17 and then some basic HTML to show to the user, enabling him/her to soldier ahead with a brand new fully functional token.
Worth noting is the response we generate at line 25:
What this does is return a
403 Forbiddenalongside the HTML we've created at lines 19 through 23. If you prefer, you could just as well use some sort of redirect here, if that better fits your model. I like the 403 because it sends a clear signal to the user why he/she did not end up at their originally intended destination.
Also observe that this example never respond with a
404 Not Foundon requests that contain an invalid token, as all of these requests end up getting caught be the
checkTokenMiddleware. You will only ever get
404's if you request an unknown resource while having a valid token.
The rest of the files - all in one fell swoopSince this example is all about the two Middleware mentioned above, I'm not going to touch much on the routes, the handlers and the various support methods, but for the sake of completeness I'll post them here anyway:
Two interfaces, both requiring the
tokenpath parameter. Nothing else going on here. Each point to their own handler, which we find in:
I think even non-coders can read this. If there's something here that is unclear, please let me know in the comments.
This is where we store valid tokens, and yea this will potentially kill your server if left to its own devices, since I don't do any kind of cleanup on the
_validTokenslist. Also the tokens themselves are hardly safe, so as I've said before: Don't use this example for ANYTHING serious. It's a security deathtrap!
Simple convenience constants. Note that the Tentacle Response Formatter package can help you determine and set the correct response headers. It's a pretty neat package, that I might write a post about at some point.
Final wordsThe more I experiment with Shelf, the more I like it. It is clear that the authors of this excellent framework know a thing or two about the world of HTTP. There are still a few rough edges here and there, such as the weird spacing done by the
shelf.logRequests()method - I would personally prefer a simple whitespace between each field, since that looks better when the log is piped to syslog, at least on my Debian servers.
Also there seems to be missing a few response types in the
shelf.response.dartfile, but I guess they'll get added in due time. The most important ones are there, and it is possible to create your own
Responseobjects so it's not a huge issue.
All in all Shelf is awesome.