https assets ruby rails markdown

Depending on your webbrowser, embedding images via unencrypted HTTP URIs in a webpage that is otherwise served through HTTPS results in warnings or error messages about mixed content. A common case for that: user input that allows referencing external images, e.g. in a blog-like application where the app itself is served using TLS.

Github had this issue (Github … issue… get it?) and came up with camo, a tool that »makes insecure assets look secure« by routing all insecure assets through a secure proxy application. Your TLS-secured website will have to rewrite all <img src="…"> tags in order to reference the camo-proxied image instead. This article describes how to do it with Redcarpet markdown and the html-pipeline gem.

Installing camo

Installing camo couldn’t be easier: The camo Github repo sports a »Deploy to Heroku« button. If you’re already registered at Heroku, deploying a camo instance to production is literally a thing of 5 minutes.

The »Deploy to Heroku« routine will create a secret key for your camo instance. You can access it as the CAMO_KEY environment variable. The following paragraphs assume that you have your camo instance up and running. Also, make sure you have your CAMO_KEY available.

Redcarpet

The camo gem is needed for creating the required HMAC digests. Add it to your Gemfile and bundle. The gem expects the host and secret to be configured as CAMO_HOST and CAMO_KEY environment variables.

To integrate with Redcarpet markdown, we need to hook into the expansion of ![alt-text](img-source) tags. I created a new subclass of Redcarpet::Render::HTML that overrides the image instance method of its superclass:

# lib/redcarpet/render/camo.rb
module Redcarpet
  module Render
    class Camo < Redcarpet::Render::HTML
      include ::Camo # provided by the camo gem

      def image(link, title, alt_text)
        if camo_configured? and link =~ %r[\Ahttp:]
          link = camo(link) # rewrite with camo url
        end
        %Q[<img src="#{link}" alt="#{alt_text}" title="#{title}">]
      end

      private

      # Predicate to make the overriden `image` instance method
      # behave like the superclass version when essential ENV
      # variables are not set.
      def camo_configured?
        [ENV["CAMO_HOST"], ENV["CAMO_KEY"]].all?
      end
    end
  end
end

Make sure your custom class is required in your project. Render markdown like this:

renderer = Redcarpet::Render::Camo.new
Redcarpet::Markdown.new(renderer).render "![🔐](http://i.imgur.com/oPXo3kl.jpg)"

# Output (w/o the surrounding <p> tag):
# <img src="https://camo.aegisnet.de/56fec5e12d1be568a6ca7b650e7a88d78037d4e0/687474703a2f2f692e696d6775722e636f6d2f6f50586f336b6c2e6a7067" alt="🔐" title="">

Have a look at the tests for it in this changeset.

html-pipeline

The html-pipeline gem provides a set of filters that can be chained to form a pipeline for HTML content transformation. It’s a very versatile tool, again made by Github. You can read more about it in their blog post. It comes with a camo filter:

html = '<p><img src="http://i.imgur.com/oPXo3kl.jpg"></p>'

camo_filter = HTML::Pipeline::CamoFilter.new(html, asset_proxy: ENV['CAMO_HOST'], asset_proxy_secret_key: ENV['CAMO_KEY'])
camo_filter.call

# Output:
# <p><img src="https://camo.aegisnet.de/56fec5e12d1be568a6ca7b650e7a88d78037d4e0/687474703a2f2f692e696d6775722e636f6d2f6f50586f336b6c2e6a7067" data-canonical-src="http://i.imgur.com/oPXo3kl.jpg"></p>

I love the idea of adding the original, untainted image source as a data attribute.

html-pipeline’s real strength is using it in conjunction with other filters, creating pipelines. Here’s how the camo filter fits in:

markdown = <<-EOMD
# Hello World
This is a ![proxied image](http://i.imgur.com/oPXo3kl.jpg)
EOMD

# The `context` hash gets all the config options for all the filters in the pipeline.
# Check the docs for the individual filter for available options.
context = { asset_proxy: ENV['CAMO_HOST'], asset_proxy_secret_key: ENV['CAMO_KEY'] }

CamoPipeline = HTML::Pipeline.new [HTML::Pipeline::MarkdownFilter, HTML::Pipeline::CamoFilter], context
result = CamoPipeline.call markdown
result[:output] # holds the rendered HTML

# Output:
# <h1>Hello World</h1>
#
# <p>This is a <img src="https://camo.aegisnet.de/89abf09cd9ef15a03c6bee187c00985cb3154640/687474703a2f2f692e696d6775722e636f6d2f6f50586f336b6c2e6a7067" alt="proxied image" data-canonical-src="http://i.imgur.com/oPXo3kl.jpg"></p>

Hint: Jekyll, the blog engine that powers this website, can also be used with html-pipeline using the jekyll-html-pipeline plugin.

comments powered by Disqus