Skip to content

gafrom/web_package

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

Packaging Websites with Ruby

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages