refraction 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Pivotal Labs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Refraction
2
+ ==========
3
+
4
+ Refraction is a Rack middleware replacement for `mod_rewrite`. It can rewrite URLs before they are
5
+ processed by your web application, and can redirect using 301 and 302 status codes. Refraction is
6
+ thread-safe, so it doesn't need to be guarded by Rack::Lock.
7
+
8
+ The best thing about Refraction is that rewrite rules are written in plain old Ruby code, not some
9
+ funky web server config syntax. That means you can use Ruby regular expressions, case statements,
10
+ conditionals, and whatever else you feel like.
11
+
12
+ For example:
13
+
14
+ Refraction.configure do |req|
15
+ feedburner = "http://feeds.pivotallabs.com/pivotallabs"
16
+
17
+ if req.env['HTTP_USER_AGENT'] !~ /FeedBurner|FeedValidator/ && req.host =~ /pivotallabs\.com/
18
+ case req.path
19
+ when %r{^/(talks|blabs|blog)\.(atom|rss)$} ; req.found! "#{feedburner}/#{$1}.#{$2}"
20
+ when %r{^/users/(chris|edward)/blog\.(atom|rss)$} ; req.found! "#{feedburner}/#{$1}.#{$2}"
21
+ end
22
+ else
23
+ case req.host
24
+ when 'tweed.pivotallabs.com'
25
+ req.rewrite! "http://pivotallabs.com/tweed#{req.path}"
26
+ when /([-\w]+\.)?pivotallabs\.com/
27
+ # passthrough with no change
28
+ else # wildcard domains (e.g. pivotalabs.com)
29
+ req.permanent! :host => "pivotallabs.com"
30
+ end
31
+ end
32
+ end
33
+
34
+ Notice the use of regular expressions, the $1, $2, etc pseudo-variables, and string interpolation.
35
+ This is an easy way to match URL patterns and assemble the new URL based on what was matched.
36
+
37
+ ## Installation (Rails)
38
+
39
+ Refraction can be installed in a Rails application as a plugin.
40
+
41
+ $ script/plugin install git://github.com/pivotal/refraction.git
42
+
43
+ In `environments/production.rb`, add Refraction at or near the top of your middleware stack.
44
+
45
+ config.middleware.insert_before(::Rack::Lock, ::Refraction, {})
46
+
47
+ You may want to occasionally turn on Refraction in the development environment for testing
48
+ purposes, but if your rules redirect to other servers that can be a problem.
49
+
50
+ Put your rules in `config/initializers/refraction_rules.rb` (see example above). The file name
51
+ doesn't actually matter, but convention is useful.
52
+
53
+ ## Server Configuration
54
+
55
+ If your application is serving multiple virtual hosts, it's probably easiest to configure your web
56
+ server to handle a wildcard server name and let Refraction handle managing the virtual hosts. For
57
+ example, in nginx, that is done with a `server_name _;` directive.
58
+
59
+ ## Writing Rules
60
+
61
+ Set up your rewrite/redirection rules during your app initialization using `Refraction.configure`.
62
+ The `configure` method takes a block which is run for every request to process the rules. The block
63
+ is passed a RequestContext object that contains information about the request URL and environment.
64
+ The request object also has a small API for effecting rewrites and redirects.
65
+
66
+ > Important note: don't do a `return` from within the configuration
67
+ > block. That would be bad (meaning your entire application would
68
+ > break). That's just how blocks work in Ruby.
69
+
70
+ ### `RequestContext#set(options)`
71
+
72
+ The `set` method takes an options hash that sets pieces of the rewritten URL or redirect location
73
+ header.
74
+
75
+ * :scheme - Usually `http` or `https`.
76
+ * :host - The server name.
77
+ * :port - The server port. Usually not needed, as the scheme implies a default value.
78
+ * :path - The path of the URL.
79
+ * :query - Added at the end of the URL after a question mark (?)
80
+
81
+ Any URL components not explicitly set remain unchanged from the original request URL. You can use
82
+ `set` before calls to `rewrite!`, `permanent!`, or `found!` to set common values. Subsequent
83
+ methods will merge their component values into values from `set`.
84
+
85
+ ### `RequestContext#rewrite!(options)`
86
+
87
+ The `rewrite!` method modifies the request URL and relevant pieces of the environment. When
88
+ Refraction rule processing results in a `rewrite!`, the request is passed on down the Rack stack
89
+ to the app or the next middleware component. `rewrite!` can take a single argument, either an
90
+ options hash that uses the same options as the `set` method, or a string that sets all components
91
+ of the URL.
92
+
93
+ ### `RequestContext#permanent!(options)`
94
+
95
+ The `permanent!` method tells Refraction to return a response with a `301 Moved Permanently`
96
+ status, and sets the URL for the Location header. Like `rewrite!` it can take either a string or
97
+ hash argument to set the URL or some of its components.
98
+
99
+ ### `RequestContext#found!(options)`
100
+
101
+ The `found!` method tells Refraction to return a response with a `302 Found` status, and sets the
102
+ URL for the Location header. Like `#rewrite!` it can take either a string or hash argument to set
103
+ the URL or some of its components.
104
+
105
+ ### URL components
106
+
107
+ The request object provides the following components of the URL for matching requests: `scheme`,
108
+ `host`, `port`, `path`, and `query`. It also provides a full environment hash as the `env`
109
+ attribute. For example, `req.env['HTTP_USER_AGENT']` can be used to access the request's user
110
+ agent property.
111
+
112
+ ## Contributors
113
+
114
+ * Josh Susser (maintainer)
115
+ * Sam Pierson
116
+ * Wai Lun Mang
117
+
@@ -0,0 +1,28 @@
1
+ require 'rake'
2
+ require 'spec'
3
+ require 'spec/rake/spectask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :spec
7
+
8
+ desc 'Test the refraction plugin.'
9
+ Spec::Rake::SpecTask.new(:spec) do |t|
10
+ t.libs << 'lib'
11
+ t.verbose = true
12
+ end
13
+
14
+ begin
15
+ require 'jeweler'
16
+ Jeweler::Tasks.new do |gem|
17
+ gem.name = "refraction"
18
+ gem.summary = %Q{Rack middleware replacement for mod_rewrite}
19
+ gem.description = %Q{Reflection is a Rails plugin and standalone Rack middleware library. Give up quirky config syntax and use plain old Ruby for your rewrite and redirection rules.}
20
+ gem.email = "gems@pivotallabs.com"
21
+ gem.homepage = "http://github.com/pivotal/refraction"
22
+ gem.authors = ["Pivotal Labs", "Josh Susser", "Sam Pierson", "Wai Lun Mang"]
23
+ gem.add_development_dependency "rspec", ">= 1.2.9"
24
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
25
+ end
26
+ rescue LoadError
27
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
28
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,12 @@
1
+ ---
2
+ gems:
3
+ - name: rack
4
+ version: ~> 1.0.0
5
+ - name: rack-test
6
+ version: ~> 0.5.0
7
+ - name: rake
8
+ version: > 0.8.7
9
+ - name: rspec
10
+ version: ~> 1.1.3
11
+ - name: rspec
12
+ version: 1.1.12
@@ -0,0 +1,21 @@
1
+ # generate refraction_rules.rb
2
+
3
+ init_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "config", "initializers"))
4
+ if test(?d, init_dir)
5
+ rules_file = File.join(init_dir, "refraction_rules.rb")
6
+ unless File.exists?(rules_file)
7
+ File.open(rules_file, "w") do |f|
8
+ f.puts(<<'EOF')
9
+ Refraction.configure do |req|
10
+ # req.permanent! "http://example.com/"
11
+ end
12
+ EOF
13
+ puts "Generated starter rules file in #{rules_file}"
14
+ end
15
+ end
16
+ end
17
+
18
+ puts ""
19
+ puts "Make sure to add Refraction to your middleware stack in your production environment:"
20
+ puts ' config.middleware.insert_before(::Rack::Lock, ::Refraction, {})'
21
+ puts ""
@@ -0,0 +1,141 @@
1
+ require 'rack'
2
+
3
+ class Refraction
4
+ class RequestContext
5
+ attr_reader :env
6
+ attr_reader :status, :message, :action
7
+
8
+ def initialize(env)
9
+ @action = nil
10
+ @env = env
11
+
12
+ hostname = env['SERVER_NAME'] # because the rack mock doesn't set the HTTP_HOST
13
+ hostname = env['HTTP_HOST'].split(':').first if env['HTTP_HOST']
14
+ env_path = env['PATH_INFO'] || env['REQUEST_PATH']
15
+
16
+ @uri = URI::Generic.build(
17
+ :scheme => env['rack.url_scheme'],
18
+ :host => hostname,
19
+ :path => env_path.empty? ? '/' : env_path
20
+ )
21
+ unless [URI::HTTP::DEFAULT_PORT, URI::HTTPS::DEFAULT_PORT].include?(env['SERVER_PORT'].to_i)
22
+ @uri.port = env['SERVER_PORT']
23
+ end
24
+ @uri.query = env['QUERY_STRING'] if env['QUERY_STRING'] && !env['QUERY_STRING'].empty?
25
+ end
26
+
27
+ def response
28
+ headers = {
29
+ 'Location' => location,
30
+ 'Content-Type' => 'text/plain',
31
+ 'Content-Length' => message.length.to_s
32
+ }
33
+ [status, headers, message]
34
+ end
35
+
36
+ # URI part accessors
37
+
38
+ def scheme
39
+ @uri.scheme
40
+ end
41
+
42
+ def host
43
+ @uri.host
44
+ end
45
+
46
+ def port
47
+ @uri.port
48
+ end
49
+
50
+ def path
51
+ @uri.path
52
+ end
53
+
54
+ def query
55
+ @uri.query
56
+ end
57
+
58
+ def method
59
+ @env['REQUEST_METHOD']
60
+ end
61
+
62
+ # actions
63
+
64
+ def set(options)
65
+ if options.is_a?(String)
66
+ @uri = URI.parse(options)
67
+ else
68
+ @uri.port = nil
69
+ options.each do |k,v|
70
+ k = 'scheme' if k == :protocol
71
+ @uri.send("#{k}=", v)
72
+ end
73
+ end
74
+ end
75
+
76
+ def rewrite!(options)
77
+ @action = :rewrite
78
+ set(options)
79
+ end
80
+
81
+ def permanent!(options)
82
+ @action = :permanent
83
+ @status = 301
84
+ set(options)
85
+ @message = "moved to #{@uri}"
86
+ end
87
+
88
+ def found!(options)
89
+ @action = :found
90
+ @status = 302
91
+ set(options)
92
+ @message = "moved to #{@uri}"
93
+ end
94
+
95
+ def location
96
+ @uri.to_s
97
+ end
98
+
99
+ end # RequestContext
100
+
101
+ def self.configure(&block)
102
+ @rules = block
103
+ end
104
+
105
+ def self.rules
106
+ @rules
107
+ end
108
+
109
+ def initialize(app)
110
+ @app = app
111
+ end
112
+
113
+ def rules
114
+ self.class.rules
115
+ end
116
+
117
+ def call(env)
118
+ if self.rules
119
+ context = RequestContext.new(env)
120
+
121
+ self.rules.call(context)
122
+
123
+ case context.action
124
+ when :permanent, :found
125
+ context.response
126
+ when :rewrite
127
+ env["rack.url_scheme"] = context.scheme
128
+ env["HTTP_HOST"] = env["SERVER_NAME"] = context.host
129
+ env["HTTP_PORT"] = context.port if context.port
130
+ env["PATH_INFO"] = env["REQUEST_PATH"] = context.path
131
+ env["QUERY_STRING"] = context.query
132
+ env["REQUEST_URI"] = context.query ? "#{context.path}?#{context.query}" : context.path
133
+ @app.call(env)
134
+ else
135
+ @app.call(env)
136
+ end
137
+ else
138
+ @app.call(env)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,180 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+ require File.join(File.dirname(__FILE__), "..", "lib", "refraction")
3
+
4
+ describe Refraction do
5
+
6
+ describe "if no rules have been configured" do
7
+ before do
8
+ Refraction.configure
9
+ end
10
+
11
+ it "does nothing" do
12
+ env = Rack::MockRequest.env_for('http://bar.com/about', :method => 'get')
13
+ app = mock('app')
14
+ app.should_receive(:call) { |resp|
15
+ resp['rack.url_scheme'].should == 'http'
16
+ resp['SERVER_NAME'].should == 'bar.com'
17
+ resp['PATH_INFO'].should == '/about'
18
+ [200, {}, ["body"]]
19
+ }
20
+ response = Refraction.new(app).call(env)
21
+ end
22
+ end
23
+
24
+ describe "path" do
25
+ before do
26
+ Refraction.configure do |req|
27
+ if req.path == '/'
28
+ req.permanent! 'http://yes.com/'
29
+ elsif req.path == ''
30
+ req.permanent! 'http://no.com/'
31
+ end
32
+ end
33
+ end
34
+
35
+ it "should be set to / if empty" do
36
+ env = Rack::MockRequest.env_for('http://bar.com', :method => 'get')
37
+ env['PATH_INFO'] = '/'
38
+ app = mock('app')
39
+ response = Refraction.new(app).call(env)
40
+ response[0].should == 301
41
+ response[1]['Location'].should == "http://yes.com/"
42
+ end
43
+ end
44
+
45
+ describe "permanent redirection" do
46
+
47
+ describe "using string arguments" do
48
+ before do
49
+ Refraction.configure do |req|
50
+ req.permanent! "http://foo.com/bar?baz"
51
+ end
52
+ end
53
+
54
+ it "should redirect everything to foo.com" do
55
+ env = Rack::MockRequest.env_for('http://bar.com', :method => 'get')
56
+ app = mock('app')
57
+ response = Refraction.new(app).call(env)
58
+ response[0].should == 301
59
+ response[1]['Location'].should == "http://foo.com/bar?baz"
60
+ end
61
+ end
62
+
63
+ describe "using hash arguments" do
64
+ before do
65
+ Refraction.configure do |req|
66
+ req.permanent! :host => "foo.com", :path => "/bar", :query => "baz"
67
+ end
68
+ end
69
+
70
+ it "should redirect http://bar.com to http://foo.com" do
71
+ env = Rack::MockRequest.env_for('http://bar.com', :method => 'get')
72
+ app = mock('app')
73
+ response = Refraction.new(app).call(env)
74
+ response[0].should == 301
75
+ response[1]['Location'].should == "http://foo.com/bar?baz"
76
+ end
77
+
78
+ it "should redirect https://bar.com to https://foo.com" do
79
+ env = Rack::MockRequest.env_for('https://bar.com', :method => 'get')
80
+ app = mock('app')
81
+ response = Refraction.new(app).call(env)
82
+ response[0].should == 301
83
+ response[1]['Location'].should == "https://foo.com/bar?baz"
84
+ end
85
+
86
+ it "should clear the port unless set explicitly" do
87
+ env = Rack::MockRequest.env_for('http://bar.com:3000/', :method => 'get')
88
+ app = mock('app')
89
+ response = Refraction.new(app).call(env)
90
+ response[0].should == 301
91
+ response[1]['Location'].should == "http://foo.com/bar?baz"
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "temporary redirect for found" do
97
+ before(:each) do
98
+ Refraction.configure do |req|
99
+ if req.path =~ %r{^/users/(josh|edward)/blog\.(atom|rss)$}
100
+ req.found! "http://feeds.pivotallabs.com/pivotallabs/#{$1}.#{$2}"
101
+ end
102
+ end
103
+ end
104
+
105
+ it "should temporarily redirect to feedburner.com" do
106
+ env = Rack::MockRequest.env_for('http://bar.com/users/josh/blog.atom', :method => 'get')
107
+ app = mock('app')
108
+ response = Refraction.new(app).call(env)
109
+ response[0].should == 302
110
+ response[1]['Location'].should == "http://feeds.pivotallabs.com/pivotallabs/josh.atom"
111
+ end
112
+
113
+ it "should not redirect when no match" do
114
+ env = Rack::MockRequest.env_for('http://bar.com/users/sam/blog.rss', :method => 'get')
115
+ app = mock('app')
116
+ app.should_receive(:call) { |resp|
117
+ resp['rack.url_scheme'].should == 'http'
118
+ resp['SERVER_NAME'].should == 'bar.com'
119
+ resp['PATH_INFO'].should == '/users/sam/blog.rss'
120
+ [200, {}, ["body"]]
121
+ }
122
+ response = Refraction.new(app).call(env)
123
+ end
124
+ end
125
+
126
+ describe "rewrite url" do
127
+ before(:each) do
128
+ Refraction.configure do |req|
129
+ if req.host =~ /(tweed|pockets)\.example\.com/
130
+ req.rewrite! :host => 'example.com', :path => "/#{$1}#{req.path == '/' ? '' : req.path}"
131
+ end
132
+ end
133
+ end
134
+
135
+ it "should rewrite subdomain to scope the path for matching subdomains" do
136
+ env = Rack::MockRequest.env_for('http://tweed.example.com', :method => 'get')
137
+ app = mock('app')
138
+ app.should_receive(:call) { |resp|
139
+ resp['rack.url_scheme'].should == 'http'
140
+ resp['SERVER_NAME'].should == 'example.com'
141
+ resp['PATH_INFO'].should == '/tweed'
142
+ [200, {}, ["body"]]
143
+ }
144
+ Refraction.new(app).call(env)
145
+ end
146
+
147
+ it "should not rewrite if the subdomain does not match" do
148
+ env = Rack::MockRequest.env_for('http://foo.example.com', :method => 'get')
149
+ app = mock('app')
150
+ app.should_receive(:call) { |resp|
151
+ resp['rack.url_scheme'].should == 'http'
152
+ resp['SERVER_NAME'].should == 'foo.example.com'
153
+ resp['PATH_INFO'].should == '/'
154
+ [200, {}, ["body"]]
155
+ }
156
+ Refraction.new(app).call(env)
157
+ end
158
+ end
159
+
160
+ describe "environment" do
161
+ before(:each) do
162
+ Refraction.configure do |req|
163
+ if req.env['HTTP_USER_AGENT'] =~ /FeedBurner/
164
+ req.permanent! "http://yes.com/"
165
+ else
166
+ req.permanent! "http://no.com/"
167
+ end
168
+ end
169
+ end
170
+
171
+ it "should expose environment settings" do
172
+ env = Rack::MockRequest.env_for('http://foo.com/', :method => 'get')
173
+ env['HTTP_USER_AGENT'] = 'FeedBurner'
174
+ app = mock('app')
175
+ response = Refraction.new(app).call(env)
176
+ response[0].should == 301
177
+ response[1]['Location'].should == "http://yes.com/"
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,3 @@
1
+ require "rubygems"
2
+ require "spec"
3
+ require "rack/test"
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: refraction
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pivotal Labs
8
+ - Josh Susser
9
+ - Sam Pierson
10
+ - Wai Lun Mang
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2009-10-29 00:00:00 -07:00
16
+ default_executable:
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: rspec
20
+ type: :development
21
+ version_requirement:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.9
27
+ version:
28
+ description: Reflection is a Rails plugin and standalone Rack middleware library. Give up quirky config syntax and use plain old Ruby for your rewrite and redirection rules.
29
+ email: gems@pivotallabs.com
30
+ executables: []
31
+
32
+ extensions: []
33
+
34
+ extra_rdoc_files:
35
+ - README.md
36
+ files:
37
+ - MIT-LICENSE
38
+ - README.md
39
+ - Rakefile
40
+ - VERSION
41
+ - geminstaller.yml
42
+ - install.rb
43
+ - lib/refraction.rb
44
+ - spec/refraction_spec.rb
45
+ - spec/spec_helper.rb
46
+ has_rdoc: true
47
+ homepage: http://github.com/pivotal/refraction
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.3.5
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Rack middleware replacement for mod_rewrite
74
+ test_files:
75
+ - spec/refraction_spec.rb
76
+ - spec/spec_helper.rb