Developing virtualhost-aware PSGI applications: Plack::Middleware::MockProxyFrontend
Let’s say you work on a team that runs a web content management system for various different customers. It is hosted at
ourcms.com, but each customer’s public content is published on a different domain, which is determined by a setting in the interface, which they can change at will. When a customer is logged into
ourcms.com they see links to their public content in various places, and some of the public content has “edit this”-type links back into
ourcms.com. All of this runs as a single PSGI application. A not unfamiliar scenario, presumably.
How do you spin up a development server where you can test this?
OK, let’s see. We all know that you
plackup and then go to
localhost:5000 in your browser and then you can click around. What will that show you in this case? Most probably: what
ourcms.com would look like. And then you log in there and click around the edit interface.
But how do you test the linking to the customer’s site as you edit stuff?
This is the scenario for which I wrote Plack::Middleware::MockProxyFrontend.
What you do when you use it is after you
plackup, you set
localhost:5000 as the proxy in your web browser configuration. Then you navigate not to
localhost:5000 but to
ourcms.com – just as you would if you were going to the live site.
That request will actually hit your local development server, since you set it as the proxy. The reason you need a middleware to do this is that it will be an HTTP proxy request rather than a regular HTTP request, which has one small but important difference. MockProxyFrontend’s job is to convert that request back to a regular HTTP request and pass it to your app. The request will then look just as it would if the browser was actually talking to
ourcms.com. Therefore likewise, your app’s response will look just as it would if the app was actually running at that domain. And so the browser will think that that is what it’s doing.
This even works with HTTPS. All you need to do if your site uses HTTPS is give MockProxyFrontend your SSL key and certificate(-chain), just like you would configure the production server for your site. (If your site does not use HTTPS then you need not do anything.)
Effectively, by setting your app as your proxy and putting MockProxyFrontend in front of it (preferably as the very outermost middleware), your app becomes your browser’s entire internet. Thus, you get to click around your entire network of sites on your development machine as though it was the real deal.
For the interested, let me give some further background on the SSL support – because this is the real trick to the module, if anything is clever about it at all.
Proxy requests that involve SSL are very different from regular, unencrypted proxy requests: there is an outer, unencrypted request to the proxy whose payload is effectively the real-time streaming connection to the target site – over which the actual HTTP request, wrapped inside an encrypted envelope, takes place.
To a regular proxy the content of this connection is opaque. But for MockProxyFrontend, the app it wraps is the target site and there is no other HTTP server with SSL support to deal with the connection. The goal is certainly that the user won’t have to set one up. So unlike a proxy, MockProxyFrontend never connects anywhere, and again unlike a proxy, it needs to see inside the connection.
So it must unwrap the connection’s crypto layer itself and then process the connection with a server-side HTTP protocol implementation… a second time, after the connection in its unencrypted phase has already been processed once before by the development server. By default MockProxyFrontend uses a local HTTP::Server::PSGI instance (not listening on any port) for that purpose.
The upshot is that you generally need not do anything more than pass your key and certificate and let the middleware do the rest – except when your app needs PSGI server capabilities that HTTP::Server::PSGI does not implement. In that case, you will have to provide MockProxyFrontend with an alternative implementation which does implement those capabilities. (Consult the documentation about how. If you do have this use case, please let me know, especially if you have issues; this is an area I did not test at all.)
Again, share and enjoy.