Skip to content

Commit

Permalink
Merge pull request ericallam#4 from rubymaverick/mime_types
Browse files Browse the repository at this point in the history
Refactor the way mime types are identified and set
  • Loading branch information
nbibler committed Jan 18, 2012
2 parents 01df9e4 + feedd8a commit 88312c9
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .rspec
@@ -0,0 +1,2 @@
--color
--format progress
87 changes: 78 additions & 9 deletions README.md
@@ -1,32 +1,101 @@
Font Assets
=============

This little gem helps serve font-face assets in Rails 3.1. It really does these two things:
This little gem helps serve font-face assets in Rails 3.1. It really does these
two things:

* Registers font Mime Types for woff, eot, tff, and svg font files
* Sets Access-Control-Allow-Origin response headers for font assets, which Firefox requires for cross domain fonts
* Responds with "proper" mime types for woff, eot, tff, and svg font files, and
* Sets Access-Control-Allow-Origin response headers for font assets, which Firefox requires for cross domain fonts.

In addition, it will also respond to the pre-flight OPTIONS requests made by
supporting browsers (Firefox).

Install
-------

Add `font_assets` to your Gemfile:

gem 'font_assets'
```ruby
gem 'font_assets'
```


Usage
-----

Set the origin domain that will get set in the `Access-Control-Allow-Origin` header
By default, in a Rails application, this gem should Just Work™. However, the
default settings allow any requesting site to use the linked fonts, due to the
Allowed Origin being '*', by default. This is only useful for browsers which
support this feature (Firefox), but restricting it to certain domains may be
beneficial.

Set the origin domain that will get set in the `Access-Control-Allow-Origin`
header:

```ruby
# in config/environments/production.rb
config.font_assets.origin = 'http://codeschool.com'
```

The origin domain must match the domain of the site serving the page that is
requesting the font, not the host of the font. For example, if you are using a
CDN to serve your assets (like CloudFront), and the full path to the font asset
is `http://d3rd6rvl24noqd.cloudfront.net/assets/fonts/Pacifico-webfont-734f1436e605566ae2f1c132905e28b2.woff`,
but the URI the user is visiting is `http://coffeescript.codeschool.com/level/1`,
you'd want to set the origin header to this:

```ruby
config.font_assets.origin = 'http://coffeescript.codeschool.com'
```

An Example Response
-------------------

# in config/environments/production.rb
Below is an example response for a .woff font asset on a Rails 3.1 application
running behind several proxies and caches (including CloudFront):

config.font_assets.origin = 'http://codeschool.com'
```
$ curl -i http://d1tijy5l7mg5kk.cloudfront.net/assets/ubuntu/Ubuntu-Bold-webfont-4bcb5239bfd34be67bc07901959fc6e1.woff
HTTP/1.0 200 OK
Server: nginx
Date: Sat, 14 Jan 2012 19:45:19 GMT
Content-Type: application/x-font-woff
Last-Modified: Sat, 14 Jan 2012 16:58:14 GMT
Cache-Control: public, max-age=31557600
Access-Control-Allow-Origin: http://www.codeschool.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: x-requested-with
Access-Control-Max-Age: 3628800
X-Content-Digest: 66049433125f563329c4178848643536f76459e5
X-Rack-Cache: fresh
Content-Length: 17440
X-Varnish: 311344447
Age: 289983
X-Cache: Hit from cloudfront
X-Amz-Cf-Id: 9yzifs_hIQF_MxPLwSR8zck3eZVXJ8LFKpMUpXnu2SmMuEmyrUbHdQ==,Lbh9kfjr0YRm77seSmOSQ6oFkUEMabvtFStJLhTOy9BfGrIXVneoKQ==
Via: 1.1 varnish, 1.0 2815dd16e8c2a0074b81a6148bd8aa3a.cloudfront.net:11180 (CloudFront), 1.0 f9e7403ca14431787835521769ace98a.cloudfront.net:11180 (CloudFront)
Connection: close
```

The origin domain must match the domain of the site serving the page that is requesting the font, not the host of the font. For example, if you are using a CDN to serve your assets (like CloudFront), and the full path to the font asset is `http://d3rd6rvl24noqd.cloudfront.net/assets/fonts/Pacifico-webfont-734f1436e605566ae2f1c132905e28b2.woff`, but the URI the user is visiting is `http://coffeescript.codeschool.com/level/1`, you'd want to set the origin header to this:
In it, you can see where this middleware has injected the `Content-Type` and
`Access-Control-*` headers into the response.

config.font_assets.origin = 'http://coffeescript.codeschool.com'
And below is an example OPTIONS request response:

```
$ curl -i -X OPTIONS http://www.codeschool.com/
HTTP/1.1 200 OK
Server: nginx
Date: Wed, 18 Jan 2012 04:13:25 GMT
Connection: keep-alive
Access-Control-Allow-Origin: http://www.codeschool.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: x-requested-with
Access-Control-Max-Age: 3628800
Vary: Accept-Encoding
X-Rack-Cache: invalidate, pass
Content-Length: 0
```

License
-------
Expand Down
5 changes: 2 additions & 3 deletions font_assets.gemspec
Expand Up @@ -18,7 +18,6 @@ Gem::Specification.new do |s|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]

# specify any dependencies here; for example:
# s.add_development_dependency "rspec"
# s.add_runtime_dependency "rest-client"
s.add_dependency "rack"
s.add_development_dependency "rspec"
end
23 changes: 19 additions & 4 deletions lib/font_assets/middleware.rb
@@ -1,9 +1,13 @@
require 'rack'
require 'font_assets/mime_types'

module FontAssets
class Middleware

def initialize(app, origin)
@app = app
@origin = origin
@mime_types = FontAssets::MimeTypes.new(Rack::Mime::MIME_TYPES)
end

def access_control_headers
Expand All @@ -21,21 +25,32 @@ def call(env)
return [200, access_control_headers, []]
else
code, headers, body = @app.call(env)
headers.merge!(access_control_headers) if font_asset?(env["PATH_INFO"])
set_headers! headers, body, env["PATH_INFO"]
[code, headers, body]
end
end


private


def extension(path)
path.split("?").first.split(".").last
"." + path.split("?").first.split(".").last
end

def font_asset?(path)
%w(woff eot ttf svg).include? extension(path)
@mime_types.font? extension(path)
end

def set_headers!(headers, body, path)
if ext = extension(path) and font_asset?(ext)
headers.merge!(access_control_headers)
headers.merge!('Content-Type' => mime_type(ext)) unless body.empty?
end
end

def mime_type(extension)
@mime_types[extension]
end
end

end
38 changes: 38 additions & 0 deletions lib/font_assets/mime_types.rb
@@ -0,0 +1,38 @@
module FontAssets
class MimeTypes
DEFAULT_TYPE = 'application/octet-stream'
MIME_TYPES = {
'.eot' => 'application/vnd.ms-fontobject',
'.svg' => 'image/svg+xml',
'.ttf' => 'application/x-font-ttf',
'.woff' => 'application/x-font-woff'
}

def initialize(types, default = DEFAULT_TYPE.dup)
@types = types.dup
@default = default

MIME_TYPES.each_pair do |extension, type|
set extension, type
end
end

def [](extension)
@types.fetch(extension, DEFAULT_TYPE.dup).dup
end

def font?(extension)
MIME_TYPES.keys.include? extension
end

def set(extension, mime_type)
if @types[extension].nil? || @types[extension] == @default
set!(extension, mime_type)
end
end

def set!(extension, mime_type)
@types[extension] = mime_type
end
end
end
7 changes: 0 additions & 7 deletions lib/font_assets/railtie.rb
Expand Up @@ -9,12 +9,5 @@ class Railtie < Rails::Railtie

app.middleware.insert_before 'ActionDispatch::Static', FontAssets::Middleware, config.font_assets.origin
end

config.after_initialize do
Rack::Mime::MIME_TYPES['.woff'] ||= 'application/x-font-woff'
Rack::Mime::MIME_TYPES['.ttf'] ||= 'application/x-font-ttf'
Rack::Mime::MIME_TYPES['.eot'] ||= 'application/vnd.ms-fontobject'
Rack::Mime::MIME_TYPES['.svg'] ||= 'image/svg+xml'
end
end
end
80 changes: 80 additions & 0 deletions spec/middleware_spec.rb
@@ -0,0 +1,80 @@
require 'spec_helper'
require 'font_assets/middleware'

describe FontAssets::Middleware do
it 'passes all Rack::Lint checks' do
app = Rack::Lint.new(FontAssets::Middleware.new(inner_app, 'http://test.local'))
request app, '/'
end

context 'for GET requests' do
context 'to font assets' do
let(:app) { load_app 'http://test.origin' }
let(:call) { request app, '/test.ttf' }

context 'the response headers' do
subject { call[1] }

its(["Access-Control-Allow-Headers"]) { should == "x-requested-with" }
its(["Access-Control-Max-Age"]) { should == "3628800" }
its(['Access-Control-Allow-Methods']) { should == 'GET' }
its(['Access-Control-Allow-Origin']) { should == 'http://test.origin' }
its(['Content-Type']) { should == 'application/x-font-ttf' }
end
end

context 'to non-font assets' do
let(:app) { load_app }
let(:call) { request app, '/' }

context 'the response headers' do
subject { call[1] }

its(["Access-Control-Allow-Headers"]) { should be_nil }
its(["Access-Control-Max-Age"]) { should be_nil }
its(['Access-Control-Allow-Methods']) { should be_nil }
its(['Access-Control-Allow-Origin']) { should be_nil }
its(['Content-Type']) { should == 'text/plain' }
end
end
end

context 'for OPTIONS requests' do
let(:app) { load_app 'http://test.options' }
let(:call) { request app, '/test.ttf', :method => 'OPTIONS' }

context 'the response headers' do
subject { call[1] }

its(["Access-Control-Allow-Headers"]) { should == "x-requested-with" }
its(["Access-Control-Max-Age"]) { should == "3628800" }
its(['Access-Control-Allow-Methods']) { should == 'GET' }
its(['Access-Control-Allow-Origin']) { should == 'http://test.options' }

it 'should not contain a Content-Type' do
subject['Content-Type'].should be_nil
end
end

context 'the response body' do
subject { call[2] }
it { should be_empty }
end
end


private


def load_app(origin = 'http://test.local')
FontAssets::Middleware.new(inner_app, origin)
end

def inner_app
lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'Success'] }
end

def request(app, path, options = {})
app.call Rack::MockRequest.env_for(path, options)
end
end
55 changes: 55 additions & 0 deletions spec/mime_types_spec.rb
@@ -0,0 +1,55 @@
require 'spec_helper'
require 'font_assets/mime_types'

describe FontAssets::MimeTypes do
context 'given an empty hash' do
let(:hash) { Hash.new }
subject { described_class.new(hash) }

it 'adds the known mime types' do
FontAssets::MimeTypes::MIME_TYPES.each_pair do |ext, type|
subject[ext].should == type
end
end
end

context 'given a populated hash' do
let(:default_type) { 'default/type' }
let(:hash) { { '.ttf' => default_type, '.svg' => 'test/type' } }
subject { described_class.new(hash, default_type) }

it 'retains the non-default-matching mime types' do
subject['.svg'].should == hash['.svg']
end

it 'overrides the default-matching mime types' do
subject['.ttf'].should_not == hash['.ttf']
end
end

context '#[]' do
let(:types) { described_class.new({}) }

it 'returns the mime type of the passed extension' do
types['.woff'].should == 'application/x-font-woff'
end

it 'returns the default mime type for unknown extensions' do
types['.bad'].should == 'application/octet-stream'
end
end

context '#font?' do
let(:types) { described_class.new({}) }

it 'is true for known font extensions' do
FontAssets::MimeTypes::MIME_TYPES.keys.each do |key|
types.font?(key).should be_true
end
end

it 'is false for unrecognized font extensions' do
types.font?('.bad').should be_false
end
end
end
5 changes: 5 additions & 0 deletions spec/spec_helper.rb
@@ -0,0 +1,5 @@
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true
config.filter_run :focus
end

0 comments on commit 88312c9

Please sign in to comment.