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 +29 -0
- data/Rakefile +0 -0
- data/lib/rack/facebook.rb +154 -0
- data/rack-facebook.gemspec +20 -0
- data/spec/facebook_spec.rb +167 -0
- metadata +68 -0
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
|