Hybrid static sites — 3288 requests a second while logged in

Samuel Michael Squire
4 min readJul 31, 2020

--

./wrk -t12 -c200 -d30s http://lonely-people.com/private/f04d7d570241420cba83d32f9cf2509e/1/dashboard/
Running 30s test @ http://lonely-people.com/private/f04d7d570241420cba83d32f9cf2509e/1/dashboard/
12 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 72.10ms 111.19ms 1.78s 96.61%
Req/Sec 279.57 62.13 1.49k 83.82%
98983 requests in 30.10s, 353.86MB read
Requests/sec: 3288.54
Transfer/sec: 11.76MB

Get the best of dynamic web servers and static sites at the same time with hybrid static sites

You can get 3288 requests a second for your logged in users too.

Did you know you can get the high performance of a static website at the same time as the dynanism of a dynamic website at the same time? You want a hybrid site. I’ll tell you how I made my website a hybrid static site. The problem with this approach is that it breaks URL sharing but some funky Javascript can probably work around that.

You have pages that only a logged in user should see. The solution is to move your session token to the URL. This sounds insecure but it really isn’t if you introduce session validation for every request. We can use nginx’s auth_request to implement a simple session validator.

The architecture is that a request comes in, nginx fires off a request to your session validator service and the session validation service checks the IP address of the session matches. This session validator uses Redis.

The full nginx configuration looks like this:

  • traffic to the API goes through api.lonely-people.com and is directed directly to the API server, which is a gunicorn server running the python application.
  • traffic to the root domain goes through lonely-people.com and is served by a static site for most URLs. If the URL begins with /private, the auth request module fires a request to /auth of the backend server to verify the session token in the URL. If it succeeds, it points to a Varnish server on port 90 which is pointed back a this nginx server on port 83 and serves the logged in user pages.

The session validator validates tokens by IP address. In python:

(There is some commented out debug stuff for debugging those requests to the auth server.)

Or, in Rust, which is what I used to receive the headline figure of requests per second:

The nginx configuration sends the original URL to the /auth endpoint:

proxy_set_header        X-Original-URI $request_uri;

If a request succeeds, it’s served by a static directory of HTML files backed by Varnish. This is what gives the site its performance.

Logon symlink

When the user logs in, you need to associate the static files of a user with the session identifier. The privateuser folder contains a spidered static site of logged in users.

Create a symlink to the static files to associate the session ID in the URL with the private files.

example.com/private/user_id is protected by nginx.

So in your server side API function for a logged in user, you’ll need to validate the session ID given by the URL by IP address (X-Forwarded-For)

How to generate the site

We have a site generator that spiders the site. To make a hybrid static site, you need to write a static site spiderer that spiders your application server for a given user.

In your application, you generate logged in user pages in batch, whenever some thing changes or at login time. To talk to endpoints protected by the above mechanism, you’ll need a way to override the normal session based protection.

You’ll need some way of programmatically logging in as a user and generating content as that user. Whatever solution you use must not interfere with the user’s real session, it also must not cause insecurity to the publicly facing API.

I suggest deploying the application a second time with session handling switched off. Normally the logged in user comes from the session. I suggest using a header called Override-User and using this header only when session handling is disabled.

Then your site generation code can look something akin to the following:

For a logged in user, a site generation would look some thing like:

This is a simple site generator that takes a couple of seconds to run. I plan to have a site generator that can

  • run requests in parallel
  • understands the data dependencies of the site between pages and pieces of information
  • understands cache invalidations when data is updated

Fixing up links and forms

We have to use Javascript to fix up links and forms with the correct session ID.

To fix up forms and links we generate links with USER and SESSION placeholders:

Protecting forms with CSRF

How do we protect forms with CSRF now that they’re not generated by the server on each request? We write a CSRF token server:

We need to enable cors, because API requests go through api.lonely-people.com and allow cookies to be sent from lonely-people.com. We do this with Access-Control-Allow-Origin and Access-Control-Allow-Credentials.

In our nginx configuration:

if ($http_origin ~ "lonely-people.com$") {
add_header "Access-Control-Allow-Origin" $http_origin;
add_header "Access-Control-Allow-Credentials" "true";
}

We need to protect forms with some Javascript funkiness. The funky javascript form protector runs on every page.

Conclusion

You can get the benefits of a static website with logged in users with these techniques.

--

--