rack-facebook 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,29 @@
1
+ This Rack middleware checks the signature of Facebook params, and
2
+ converts them to Ruby objects when appropiate. Also, it converts
3
+ the request method from the Facebook POST to the original HTTP
4
+ method used by the client.
5
+
6
+ If the signature is wrong, it returns a "400 Invalid Facebook Signature".
7
+
8
+ Optionally, it can take a block that receives the Rack environment
9
+ and returns a value that evaluates to true when we want the middleware to
10
+ be executed for the specific request.
11
+
12
+ # Usage
13
+
14
+ In your config.ru:
15
+
16
+ require 'rack/facebook'
17
+ use Rack::Facebook, "my_facebook_secret_key"
18
+
19
+ Using a block condition:
20
+
21
+ use Rack::Facebook, "my_facebook_secret_key" do |env|
22
+ env['REQUEST_URI'] =~ /^\/facebook_only/
23
+ end
24
+
25
+ # Credits
26
+
27
+ Carlos Paramio
28
+
29
+ [http://evolve.st/](http://evolve.st/)
data/Rakefile ADDED
File without changes
@@ -0,0 +1,154 @@
1
+ require 'digest'
2
+ require 'rack/request'
3
+
4
+ module Rack
5
+ # This Rack middleware checks the signature of Facebook params and
6
+ # converts them to Ruby objects when appropiate. Also, it converts
7
+ # the request method from the Facebook POST to the original HTTP
8
+ # method used by the client.
9
+ #
10
+ # If the signature is wrong, it returns a "400 Invalid Facebook Signature".
11
+ #
12
+ # Optionally, it can take a block that receives the Rack environment
13
+ # and returns a value that evaluates to true when we want the middleware to
14
+ # be executed for the specific request.
15
+ #
16
+ # == Usage
17
+ #
18
+ # In your rack builder:
19
+ #
20
+ # use Rack::Facebook, :application_secret => "SECRET", :api_key => "APIKEY"
21
+ #
22
+ # Using a block condition:
23
+ #
24
+ # use Rack::Facebook, options do |env|
25
+ # env['REQUEST_URI'] =~ /^\/facebook_only/
26
+ # end
27
+ #
28
+ # == References
29
+ # * http://wiki.developers.facebook.com/index.php/Authorizing_Applications
30
+ # * http://wiki.developers.facebook.com/index.php/Verifying_The_Signature
31
+ #
32
+ class Facebook
33
+ def initialize(app, options, &condition)
34
+ @app = app
35
+ @options = options
36
+ @condition = condition
37
+ end
38
+
39
+ def app_name
40
+ @options[:application_name]
41
+ end
42
+
43
+ def secret
44
+ @options[:application_secret]
45
+ end
46
+
47
+ def api_key
48
+ @options[:api_key]
49
+ end
50
+
51
+ def call(env)
52
+ request = Request.new(env)
53
+ request.api_key = api_key
54
+
55
+ if passes_condition?(request) and request.facebook?
56
+ valid = true
57
+
58
+ if request.params_signature
59
+ fb_params = request.extract_facebook_params(:post)
60
+
61
+ if valid = valid_signature?(fb_params, request.params_signature)
62
+ env["facebook.original_method"] = env["REQUEST_METHOD"]
63
+ env["REQUEST_METHOD"] = fb_params.delete("request_method")
64
+ save_facebook_params(fb_params, env)
65
+ end
66
+ elsif request.cookies_signature
67
+ cookie_params = request.extract_facebook_params(:cookies)
68
+ valid = valid_signature?(cookie_params, request.cookies_signature)
69
+ end
70
+
71
+ unless valid
72
+ return [400, {"Content-Type" => "text/html"}, ["Invalid Facebook signature"]]
73
+ end
74
+ end
75
+ return @app.call(env)
76
+ end
77
+
78
+ private
79
+
80
+ def passes_condition?(request)
81
+ @condition.nil? or @condition.call(request.env)
82
+ end
83
+
84
+ def valid_signature?(fb_params, actual_sig)
85
+ actual_sig == calculate_signature(fb_params)
86
+ end
87
+
88
+ def calculate_signature(hash)
89
+ raw_string = hash.map{ |*pair| pair.join('=') }.sort.join
90
+ Digest::MD5.hexdigest([raw_string, secret].join)
91
+ end
92
+
93
+ def save_facebook_params(params, env)
94
+ params.each do |key, value|
95
+ ruby_value = case key
96
+ when 'added', 'page_added', 'in_canvas', 'in_profile_tab', 'in_new_facebook', 'position_fix', 'logged_out_facebook'
97
+ value == '1'
98
+ when 'expires', 'profile_update_time', 'time'
99
+ Time.at(value.to_f) rescue TypeError
100
+ when 'friends'
101
+ value.split(',')
102
+ else
103
+ value
104
+ end
105
+
106
+ env["facebook.#{key}"] = ruby_value
107
+ end
108
+
109
+ env["facebook.app_name"] = app_name
110
+ env["facebook.api_key"] = api_key
111
+ env["facebook.secret"] = secret
112
+ end
113
+
114
+ class Request < ::Rack::Request
115
+ FB_PREFIX = "fb_sig".freeze
116
+ attr_accessor :api_key
117
+
118
+ def facebook?
119
+ params_signature or cookies_signature
120
+ end
121
+
122
+ def params_signature
123
+ return @params_signature if @params_signature or @params_signature == false
124
+ @params_signature = self.POST.delete(FB_PREFIX) || false
125
+ end
126
+
127
+ def cookies_signature
128
+ cookies[@api_key]
129
+ end
130
+
131
+ def extract_facebook_params(where)
132
+
133
+ case where
134
+ when :post
135
+ source = self.POST
136
+ prefix = FB_PREFIX
137
+ when :cookies
138
+ source = cookies
139
+ prefix = @api_key
140
+ end
141
+
142
+ prefix = "#{prefix}_"
143
+
144
+ source.inject({}) do |extracted, (key, value)|
145
+ if key.index(prefix) == 0
146
+ extracted[key.sub(prefix, '')] = value
147
+ source.delete(key) if :post == where
148
+ end
149
+ extracted
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rack-facebook"
3
+ s.version = "0.0.2"
4
+ s.date = "2009-01-09"
5
+ s.summary = "Rack middleware to verify and parse Facebook parameters"
6
+ s.email = "carlos@evolve.st"
7
+ s.homepage = "http://www.evolve.st/notebook/2009/1/9/rack-facebook-a-new-rack-middleware-to-parse-facebook-parameters"
8
+ s.description = "rack-facebook is a Rack middleware that checks the signature of Facebook params, and converts them to Ruby objects when appropiate. Also, it converts the request method from the Facebook POST to the original HTTP method used by the client."
9
+ s.has_rdoc = true
10
+ s.authors = ["Carlos Paramio"]
11
+ s.files = [
12
+ "README.markdown",
13
+ "Rakefile",
14
+ "rack-facebook.gemspec",
15
+ "lib/rack/facebook.rb"]
16
+ s.test_files = ["spec/facebook_spec.rb"]
17
+ s.rdoc_options = ["--main", "Rack::Facebook"]
18
+ #s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
19
+ s.add_dependency("rack", [">= 0.4.0"])
20
+ end
@@ -0,0 +1,167 @@
1
+ require 'rack/request'
2
+ require 'rack/mock'
3
+ require 'rack/facebook'
4
+
5
+ describe Rack::Facebook do
6
+ APP_NAME = 'my_app'
7
+ SECRET = "123456789"
8
+ API_KEY = "616313"
9
+
10
+ def calculate_signature(hash)
11
+ raw_string = hash.map{ |*pair| pair.join('=') }.sort.join
12
+ Digest::MD5.hexdigest([raw_string, SECRET].join)
13
+ end
14
+
15
+ def sign_hash(hash, prefix)
16
+ fb_hash = hash.inject({}) do |all, (key, value)|
17
+ all[key.sub("#{prefix}_", "")] = value if key.index("#{prefix}_") == 0
18
+ all
19
+ end
20
+ hash.merge(prefix => calculate_signature(fb_hash))
21
+ end
22
+
23
+ def sign_params(hash)
24
+ sign_hash(hash, "fb_sig")
25
+ end
26
+
27
+ def sign_cookies(hash)
28
+ sign_hash(hash, API_KEY)
29
+ end
30
+
31
+ def mock_post(app, env)
32
+ facebook = described_class.new(app, :application_name => APP_NAME, :application_secret => SECRET, :api_key => API_KEY)
33
+ request = Rack::MockRequest.new(facebook)
34
+ @response = request.post("/", env)
35
+ end
36
+
37
+ def post_env(params)
38
+ # set up form variables like rack::request had parsed them
39
+ env = {"rack.request.form_hash" => params}
40
+ env["rack.request.form_input"] = env["rack.input"] = "fb"
41
+ env
42
+ end
43
+
44
+ def post_request(app, params)
45
+ mock_post app, post_env(params)
46
+ end
47
+
48
+ def cookie_env(cookies)
49
+ # set up the cookie hash like rack::request would
50
+ env = {"rack.request.cookie_hash" => cookies}
51
+ env["rack.request.cookie_string"] = env["HTTP_COOKIE"] = "fb"
52
+ env
53
+ end
54
+
55
+ def cookie_request(app, cookies)
56
+ mock_post app, cookie_env(cookies)
57
+ end
58
+
59
+ def request
60
+ @request
61
+ end
62
+
63
+ def response
64
+ @response
65
+ end
66
+
67
+ def response_env(status = 200)
68
+ [status, {"Content-type" => "test/plain", "Content-length" => "5"}, ["hello"]]
69
+ end
70
+
71
+ def app
72
+ @app ||= lambda do |env|
73
+ @request = Rack::Request.new(env)
74
+ response_env
75
+ end
76
+ end
77
+
78
+ describe "without a block" do
79
+ describe "when the signature is not valid" do
80
+ it "should fail with 400 Invalid Facebook signature" do
81
+ post_request mock('rack app'), "fb_sig" => "INVALID"
82
+ response.status.should == 400
83
+ end
84
+ end
85
+
86
+ describe "when the fb_sig is valid" do
87
+ def post(params)
88
+ post_request(app, sign_params(params))
89
+ end
90
+
91
+ it "should convert the facebook parameters to ruby objects" do
92
+ post "fb_sig_in_canvas" => "1", "fb_sig_time" => "1", "fb_sig_user" => "234433"
93
+
94
+ request.env['facebook.time'].should == Time.at(1)
95
+ request.env['facebook.in_canvas'].should be_true
96
+ request.env['facebook.user'].should == "234433"
97
+ end
98
+
99
+ it "should add app name, api_key and secret to the environment" do
100
+ post "fb_sig_in_canvas" => "1", "fb_sig_time" => "1", "fb_sig_user" => "234433"
101
+
102
+ request.env['facebook.app_name'].should == APP_NAME
103
+ request.env['facebook.api_key'].should == API_KEY
104
+ request.env['facebook.secret'].should == SECRET
105
+ end
106
+
107
+ it "should strip facebook parameters from params hash" do
108
+ post "fb_sig_in_canvas" => "1", "fb_sig_user" => "234433"
109
+
110
+ request['fb_sig'].should be_nil
111
+ request['fb_sig_user'].should be_nil
112
+ request['fb_sig_in_canvas'].should be_nil
113
+ end
114
+
115
+ it "should not touch parameters not prefixed with \"fb_sig\"" do
116
+ post "fb_sig_user" => "234433", "foo" => "bar"
117
+
118
+ request['foo'].should == 'bar'
119
+ end
120
+
121
+ it "should split friend IDs into an array" do
122
+ post "fb_sig_friends" => "2,3,5"
123
+
124
+ request.env["facebook.friends"].should == ["2", "3", "5"]
125
+ end
126
+
127
+ it "should convert the request method from POST to the original client method" do
128
+ post "fb_sig_request_method" => "PUT"
129
+
130
+ request.request_method.should == 'PUT'
131
+ request.env['facebook.original_method'].should == 'POST'
132
+ end
133
+
134
+ it "should call app" do
135
+ @app = mock('rack app')
136
+ @app.should_receive(:call).with(instance_of(Hash)).and_return(response_env)
137
+
138
+ post "fb_sig_foo" => "bar"
139
+ response.status.should == 200
140
+ end
141
+ end
142
+
143
+ context "cookie authentication" do
144
+ it "should run app if cookie signature is valid" do
145
+ app = mock('rack app')
146
+ app.should_receive(:call).with(instance_of(Hash)).and_return(response_env)
147
+
148
+ cookie_request app, sign_cookies("#{API_KEY}_user" => "22", "#{API_KEY}_ss" => "SEKRIT")
149
+ response.status.should == 200
150
+ end
151
+
152
+ it "should not run app if cookie signature turns out invalid" do
153
+ cookie_request mock('rack app'), API_KEY => "INVALID", "#{API_KEY}_ss" => "SEKRIT"
154
+ response.status.should == 400
155
+ end
156
+ end
157
+ end
158
+
159
+ describe 'with a block' do
160
+ describe 'when the block returns a value that evaluates to true' do
161
+ it 'should execute the middleware'
162
+ end
163
+ describe 'when the block returns a value that evaluates to true' do
164
+ it 'should skip the middleware'
165
+ end
166
+ end
167
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-facebook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Carlos Paramio
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-09 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.4.0
24
+ version:
25
+ description: rack-facebook is a Rack middleware that checks the signature of Facebook params, and converts them to Ruby objects when appropiate. Also, it converts the request method from the Facebook POST to the original HTTP method used by the client.
26
+ email: carlos@evolve.st
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README.markdown
35
+ - Rakefile
36
+ - rack-facebook.gemspec
37
+ - lib/rack/facebook.rb
38
+ has_rdoc: true
39
+ homepage: http://www.evolve.st/notebook/2009/1/9/rack-facebook-a-new-rack-middleware-to-parse-facebook-parameters
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --main
45
+ - Rack::Facebook
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.5
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Rack middleware to verify and parse Facebook parameters
67
+ test_files:
68
+ - spec/facebook_spec.rb