Web Package
Not to be confused with webpack, this repository holds Ruby implementation of Signed HTTP Exchange format, allowing a browser to trust that a HTTP request-response pair was generated by the origin it claims. For details please refer to the full list of use cases and resulting requirements (IETF draft).
Ever thought of saving the Internet on a flash?
Easily-peasily.
Let's sign a pair of request/response, store it somewhere out and serve the bundle as application/signed-exchange
. Chromium browsers understand what such responses mean and unpack them smoothly making it look as if a page is served directly from originating servers.
For that we need a certificate with a special "CanSignHttpExchanges" extension. However below we will use just a self-signed one for simplicity. Please refer here to create such.
Also we need an https
cdn serving static certificate in application/cert-chain+cbor
format. We can use gen-certurl
tool from here to convert PEM certificate into this format, so we could than serve it from a cdn.
Configuration
Several parameters can be modified via WebPackage::Settings
to configure WebPackage behavior.
E.g.
# variables can be set all at once:
WebPackage::Settings.merge! expires_in: ->(uri) { uri.path.start_with?('/news') ? 7.days : 1.day },
filter: ->(env) { env['HTTP_ACCEPT'].include?('application/signed-exchange;v=b3') }
# or individually via dot-methods:
WebPackage::Settings.cert_url = 'https://my.cdn.com/cert.cbor'
Parameter | Description | Default value |
---|---|---|
headers | A Hash , representing html headers of SXG (outer) response. |
{ 'Content-Type' => 'application/signed-exchange;v=b3', 'Cache-Control' => 'no-transform', 'X-Content-Type-Options' => 'nosniff' } |
expires_in | An Integer or a Proc evaluating to an Integer or an object responding to to_i . It sets the lifetime of signed exchange, in seconds. |
604800 (7 days), which is the maximum allowed by the standard. Please mind it when supplying your Proc . |
filter | A Proc , accepting a single argument of environment and returning boolean value. The filter determines for which requests an SXG format should be served. |
->(env) { env['HTTP_ACCEPT'].include?('application/signed-exchange') } |
cert_url, cert_path, priv_path | All three are of String class, pointing to a certificate with which all pages are to be signed: - cert_url is the url of a certificate in application/cert-chain+cbor format - cert_path and priv_path are two paths pointing at pem file and private key file respectively. |
These are the only parameters which do not have default values. An exception is raised if they are not set beforehand. Please refer below to the section of Required variables on the ways to set them. |
Required variables
For smooth running WebPackage requires three variables to be set. It can be done either via environment or with the use of WebPackage::Settings
object:
export SXG_CERT_URL='https://my.cdn.com/cert.cbor' \
SXG_CERT_PATH='/path/to/cert.pem' \
SXG_PRIV_PATH='/path/to/priv.key'
or
# app/initializers/web_package_init.rb
# variables can be set all at once:
WebPackage::Settings.merge! cert_url: 'https://my.cdn.com/cert.cbor',
cert_path: '/path/to/cert.pem',
priv_path: '/path/to/priv.key'
# or individually:
WebPackage::Settings.cert_url = 'https://my.cdn.com/cert.cbor'
Use it as a middleware
WebPackage::Middleware
wraps HTML responses for desired requests into signed exchange format.
If you already have a Rack-based application (like Rails or Sinatra), than it is easy to incorporate an SXG proxy into its middleware stack.
Rails
Add the gem to your Gemfile
:
gem 'web_package'
And then plug the middleware in:
# config/application.rb
config.middleware.insert 0, 'WebPackage::Middleware'
That is it. Now all successful requests with Accept: application/signed-exchange
header will be wrapped into signed exchanges.
Pure Rack app
Imagine we have a simple web app:
# config.ru
run ->(env) { [200, {}, ['<h1>Hello world!</h1>']] }
Add the gem and the middleware:
# Gemfile
gem 'web_package'
# config.ru
use WebPackage::Middleware
We are done. Start your app by running a command rackup config.ru
.
As expected, visiting http://localhost:9292/hello
will produce:
<h1>Hello world!</h1>
What's more, visiting http://localhost:9292/hello
with Accept: application/signed-exchange
header will spit signed http exchange, containing original <h1>Hello world!</h1>
HTML:
sxg1-b3\x00\x00\x1Chttps://localhost:9292/hello\x00\x019\x00\x00?label;cert-sha256=*+DoXYlCX+bFRyW65R3bFA2ICIz8Tyu54MLFUFo5tziA=*;cert-url=\"https://my.cdn.com/cert.cbor\";date=1557657274;expires=1558262074;integrity=\"digest/mi-sha256-03\";sig=*MEUCIAKKz+KSuhlzywfU12h3SkEq5ZuYYMxDZIgEDGYMd9sAAiEAj66Il48eb0CXFAnuZhnS+j6dqZVLJ6IwUVGWShhQu9g=*;validity-url=\"https://localhost/hello\"?FdigestX9mi-sha256-03=4QeUScOpSoJl7KJ47F11rSDHUTHZhDVwLiSLOWMcvqg=G:statusC200Pcontent-encodingLmi-sha256-03Vx-content-type-optionsGnosniff\x00\x00\x00\x00\x00\x00@\x00<h1>Hello world!</h1>
Use it as it is
require 'web_package'
# this is the request/response pair
request_url = 'https://my.app.com/abc'
response = [200, {}, ['<h1>Hello world!</h1>']]
exchange = WebPackage::SignedHttpExchange.new(request_url, response)
exchange.headers
# => {"Content-Type"=>"application/signed-exchange;v=b3", "Cache-Control"=>"no-transform", "X-Content-Type-Options"=>"nosniff"}
exchange.body
# => "sxg1-b3\x00\x00\x16https://my.app.com/abc\x00\x018\x00\x00\x8Clabel;cert-sha256=*+DoXYlCX+bFRyW65R3bFA2ICIz8Tyu54MLFUFo5tziA=*;cert-url=\"https://my.cdn.com/cert.cbor\";date=1557648268;expires=1558253068;integrity=\"digest/mi-sha256-03\";sig=*MEYCIQDSH2F6E/naM/ul1iIMZMBd9VHnrbsxp+dKhYcxy9u1ewIhAIRIuHcTVPLS73q2ETLLGwY5Y7nR52bDG251uBBHxsBZ*;validity-url=\"https://my.app.com/abc\"\xA4FdigestX9mi-sha256-03=4QeUScOpSoJl7KJ47F11rSDHUTHZhDVwLiSLOWMcvqg=G:statusC200Pcontent-encodingLmi-sha256-03Vx-content-type-optionsGnosniff\x00\x00\x00\x00\x00\x00@\x00<h1>Hello world!</h1>"
The body can be stored on disk and served from any other server. That is, visiting e.g. https://other.cdn.com/foo/bar.sxg
will result in "Hello world!" HTML with https://my.app.com/abc
in a browser's address bar - with no requests sent to https://my.app.com/abc
(until the page expired).
Successive reloads will force browser to factually send requests to https://my.app.com/abc
.
Note also, that SXG is only supported by the anchor tag (<a>
) and link rel=prefetch
, so actually typing https://other.cdn.com/foo/bar.sxg
into browser's address bar and hitting enter will just download an SXG file.
This all could be helpful to preload content or serve it from closer location. For details please refer to hands-on description of Signed Http Exchanges.
Self-signed certificates in Chrome
Chrome will not proceed with a self-signed certificate - at least as long as its cbor representation is generated with dummy data for OCSP. To accomodate this, please launch the browser with the following flags:
chrome --user-data-dir=/tmp/udd\
--ignore-certificate-errors-spki-list=`openssl x509 -noout -pubkey -in cert.pem | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64`
Note, that the browser might spit a warning You are using unsupported command-line flag: --ignore-certificate-errors-spki-list
- just ignore it - the browser does support this flag (tested in versions 73 and 74).
Contributing
- Fork it
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create new Pull Request
License
Web Package is released under the MIT License.