Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the way mime types are identified and set #4

Merged
merged 6 commits into from Jan 18, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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