Simple SSL Proxy for Insecure Browser Content with Ruby or NGINX

This is an AMP version of this page click here to see the oryginal version.

SSL protection is becoming de facto standard in web and mobile development. One potential problem is that website could be served via a secure SSL connection and still displayed as insecure by most of the modern browsers. It’s enough that at least one of its resources is served without SSL. In this blog post, I will explain how to setup Ruby and NGINX server to work as an SSL proxy for insecure content and describe some basic streaming techniques.

Developer console and URL bar display insecure content warnings on https://wishlist.apki.io.

Until recently iTunes Store pages were displayed as insecure in the browsers because of http image assets. At the time of writing this blog post, iTunes API does not officially1 provide image assets via SSL.

You can check yourself:

require "open-uri"
require "json"

JSON.parse(open("https://itunes.apple.com/lookup?id=1201642309").read)["results"][0]["screenshotUrls"][0]

=> "http://is4.mzstatic.com/image/thumb/Purple128/v4/50/b6/59/50b65977-5605-4cf3-eee6-6ff350a9c9c4/source/406x228bb.jpg"

If you want to display this kind of insecure asset2 on your webpage without browser warnings, here’s what you can do:

Download assets and serve them via Amazon S3

You could download all the required assets to deliver them via a secure connection. In this case Amazon S3 with CloudFront could serve you well. An advantage of this approach is that traffic does not go through your servers, and assets can be cached using CloudFront CDN. Unfortunately, you have to take care of updating assets yourself (app icons could change at any time), and pay for all the bandwidth.

“down” gem for streaming support

Each of the following examples uses down gem. It provides a simple API for working with file downloads and supports more advanced techniques like streaming and caching. It also has a small memory footprint of less the 0.4 MB on load.

Use Rails app as an SSL proxy

Another solution would be to proxy an asset request through a Rails-based server. In that case, a Rails app downloads an asset and sends it to the browser via a secure connection. You would need to include an asset location as a parameter of the request. Here’s how a simple Rails controller implementation could look like:

app/config/routes.rb

  get "/", to: "files#show"

app/controllers/files_controller.rb

require "down"

class FilesController < ApplicationController
  def show
    data_source = Down.download(params.fetch(:target))
    send_data data_source.read, type: data_source.content_type, disposition: "inline"
  end
end

Then you could access the asset using the following URL:

https://yourapp.com?target=http://is4.mzstatic.com/image/thumb/Purple128/v4/50/b6/59/50b65977-5605-4cf3-eee6-6ff350a9c9c4/source/406x228bb.jpg

Streaming for large assets

When dealing with a larger asset a better idea would be to stream it to client part by part. To do it you need to assign an object responding to each method to response_body controller property:

require "down"

class FilesController < ApplicationController
  CHUNK_SIZE = 1024 * 128 # 128 KB

  def show
    data_source = Down.open(params.fetch(:target))
    self.content_type = data_source.data.fetch(:headers).fetch("Content-Type")
    self.response_body = Enumerator.new do |body|
      while data_source.eof? == false
        body << data_source.read(CHUNK_SIZE)
      end
      data_source.close
    end
  end
end

An advantage of this approach is that in case of large assets, they would not need to be instantiated into memory all at once. It’s an equivalent of using File.readlines instead of File.read when working with files. Depending on your use case you could play around with CHUNK_SIZE constant value.

You can also check out my other blog post for more tips on how to reduce memory usage in Rails apps.

Use Ruby Rack app as an SSL proxy

You could improve performance by dropping Rails altogether and using a barebones Rack app to serve the asset. Here’s a basic Rack server implementation:

config.ru

require "down"

run Proc.new { |env|
  req = Rack::Request.new(env)
  data_source = Down.download(req.params.fetch("target"))
  [
    200,
     {
       "Content-Type" => data_source.content_type,
       "Content-Length" => data_source.size,
     },
     [data_source.read]
  ]
}

You can run Rack apps with a rackup command

Assets streaming with Rack

Here is how you could send an asset part by part using Rack::Chunked middleware:

config.ru

require "down"

class App
  CHUNK_SIZE = 1024 * 128 # 128 KB

  def call(env)
    req = Rack::Request.new(env)
    @data_source = Down.open(req.params.fetch("target"))
    headers = @data_source.data.fetch(:headers)
    [
      200,
      {
        "Content-Type" => headers.fetch("Content-Type"),
        "Content-Encoding" => "Chunked"
      },
      self
    ]
  end

  def each
    while @data_source.eof? == false
      yield @data_source.read(CHUNK_SIZE)
    end
    @data_source.close
  end
end

use Rack::Chunked
run App.new

Use NGINX as an SSL proxy

A different solution would be using an NGINX to proxy pass to an insecure assets. You can check out my previous blog post for tips on how to configure NGINX with free SSL. If you are using Heroku as your hosting provider, you can setup NGINX as a reverse proxy in front of your Rails app using a buildpack.

Here’s a sample config:

server {
     listen 80;
     listen 443;
     server_name example-proxy.com;

     ssl        on;
     ssl_certificate         /etc/nginx/origin_cert.pem;
     ssl_certificate_key     /etc/nginx/private_key.pem;

     location / {
        resolver 8.8.8.8;
        proxy_pass $arg_target;
     }
}

resolver 8.8.8.8; line is needed to dynamically resolve DNS config of a target asset server and enable support for different assets hosts. An advantage of this solution is that you don’t block your Ruby process and NGINX is better suited to handle multiple concurrent clients than Ruby servers.

Summary

I’ve been using the NGINX based solution in Smart Wishlist for quite a while now to serve iTunes assets via SSL to both React based frontend and iOS apps. Remember that if your project has a lot of traffic, you would need to watch out for problems with rate limiting by your assets API provider. You could also use these techniques to proxy pass any other kind of static assets, not only images.

Hope those tips can help you offload some of the work from your servers. Doing an SSL proxy pass is quicker and cheaper to implement than downloading the assets and hosting them yourself.

Disclaimer: As pointed out in the comments, these examples do not include any restrictions on how and which assets can be accessed. In theory bad guys could start piggybacking on a proxy configured like this. You should consider IP, host or asset type based whitelist to make it more secure.

  1. iTunes API assets are available through SSL via an undocumented URL: http://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/50/b6/59/50b65977-5605-4cf3-eee6-6ff350a9c9c4/source/406x228bb.jpg but it could change at any time. Hopefully, Apple will migrate all of iTunes API to SSL only soon. Point of this blog post is to show what you could do if SSL version of the asset was not available at all. 

  2. A screenshot comes for an INSIDE game. 

Posted by Paweł Urbanek on Jan 29, 2018

Find me on Twitter or GitHub.

This is an AMP version of this page click here to see the oryginal version and comments.