refraction 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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