lib/koala.rb in koala-0.10.0 vs lib/koala.rb in koala-1.0.0.beta
- old
+ new
@@ -3,12 +3,13 @@
# rubygems is required to support json, how facebook returns data
require 'rubygems'
require 'json'
-# openssl is required to support signed_request
+# OpenSSL and Base64 are required to support signed_request
require 'openssl'
+require 'base64'
# include default http services
require 'koala/http_services'
# add Graph API methods
@@ -22,11 +23,11 @@
# add test user methods
require 'koala/test_users'
module Koala
-
+
module Facebook
# Ruby client library for the Facebook Platform.
# Copyright 2010 Facebook
# Adapted from the Python library by Alex Koppel, Rafi Jacoby, and the team at Context Optional
#
@@ -38,11 +39,11 @@
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
- #
+ #
# This client library is designed to support the Graph API and the official
# Facebook JavaScript SDK, which is the canonical way to implement
# Facebook authentication. Read more about the Graph API at
# http://developers.facebook.com/docs/api. You can download the Facebook
# JavaScript SDK at http://github.com/facebook/connect-js/.
@@ -51,93 +52,93 @@
# initialize with an access token
def initialize(access_token = nil)
@access_token = access_token
end
attr_reader :access_token
-
+
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
# Fetches the given path in the Graph API.
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
# add a leading /
path = "/#{path}" unless path =~ /^\//
# make the request via the provided service
result = Koala.make_request(path, args, verb, options)
-
+
# Check for any 500 errors before parsing the body
# since we're not guaranteed that the body is valid JSON
# in the case of a server error
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
-
- # Parse the body as JSON and check for errors if provided a mechanism to do so
+
+ # Parse the body as JSON and check for errors if provided a mechanism to do so
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
# and cause JSON.parse to fail -- so we account for that by wrapping the result in []
body = response = JSON.parse("[#{result.body.to_s}]")[0]
if error_checking_block
yield(body)
end
-
+
# now return the desired information
if options[:http_component]
result.send(options[:http_component])
else
body
end
end
end
-
+
class GraphAPI < API
include GraphAPIMethods
end
-
+
class RestAPI < API
include RestAPIMethods
end
-
+
class GraphAndRestAPI < API
include GraphAPIMethods
include RestAPIMethods
end
-
+
class RealtimeUpdates < API
include RealtimeUpdateMethods
end
-
+
class TestUsers < API
include TestUserMethods
# make the Graph API accessible in case someone wants to make other calls to interact with their users
attr_reader :graph_api
end
-
+
class APIError < Exception
attr_accessor :fb_error_type
def initialize(details = {})
- self.fb_error_type = details["type"]
+ self.fb_error_type = details["type"]
super("#{fb_error_type}: #{details["message"]}")
end
end
-
-
+
+
class OAuth
attr_reader :app_id, :app_secret, :oauth_callback_url
def initialize(app_id, app_secret, oauth_callback_url = nil)
@app_id = app_id
@app_secret = app_secret
- @oauth_callback_url = oauth_callback_url
+ @oauth_callback_url = oauth_callback_url
end
def get_user_info_from_cookie(cookie_hash)
# Parses the cookie set by the official Facebook JavaScript SDK.
- #
+ #
# cookies should be a Hash, like the one Rails provides
- #
+ #
# If the user is logged in via Facebook, we return a dictionary with the
# keys "uid" and "access_token". The former is the user's Facebook ID,
# and the latter can be used to make authenticated requests to the Graph API.
# If the user is not logged in, we return None.
- #
+ #
# Download the official Facebook JavaScript SDK at
# http://github.com/facebook/connect-js/. Read more about Facebook
# authentication at http://developers.facebook.com/docs/authentication/.
if fb_cookie = cookie_hash["fbs_" + @app_id.to_s]
@@ -148,162 +149,177 @@
components = {}
fb_cookie.split("&").map {|param| param = param.split("="); components[param[0]] = param[1]}
# generate the signature and make sure it matches what we expect
auth_string = components.keys.sort.collect {|a| a == "sig" ? nil : "#{a}=#{components[a]}"}.reject {|a| a.nil?}.join("")
- sig = Digest::MD5.hexdigest(auth_string + @app_secret)
+ sig = Digest::MD5.hexdigest(auth_string + @app_secret)
sig == components["sig"] && (components["expires"] == "0" || Time.now.to_i < components["expires"].to_i) ? components : nil
end
end
alias_method :get_user_info_from_cookies, :get_user_info_from_cookie
-
+
def get_user_from_cookie(cookies)
if info = get_user_info_from_cookies(cookies)
string = info["uid"]
end
end
alias_method :get_user_from_cookies, :get_user_from_cookie
-
+
# URLs
-
+
def url_for_oauth_code(options = {})
# for permissions, see http://developers.facebook.com/docs/authentication/permissions
permissions = options[:permissions]
scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
callback = options[:callback] || @oauth_callback_url
raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
# Creates the URL for oauth authorization for a given callback and optional set of permissions
- "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
+ "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
end
-
+
def url_for_access_token(code, options = {})
# Creates the URL for the token corresponding to a given code generated by Facebook
callback = options[:callback] || @oauth_callback_url
raise ArgumentError, "url_for_access_token must get a callback either from the OAuth object or in the parameters!" unless callback
"https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
end
-
+
def get_access_token_info(code)
# convenience method to get a parsed token from Facebook for a given code
# should this require an OAuth callback URL?
get_token_from_server(:code => code, :redirect_uri => @oauth_callback_url)
end
-
+
def get_access_token(code)
# upstream methods will throw errors if needed
- if info = get_access_token_info(code)
- string = info["access_token"]
+ if info = get_access_token_info(code)
+ string = info["access_token"]
end
end
-
+
def get_app_access_token_info
- # convenience method to get a the application's sessionless access token
+ # convenience method to get a the application's sessionless access token
get_token_from_server({:type => 'client_cred'}, true)
end
-
+
def get_app_access_token
if info = get_app_access_token_info
- string = info["access_token"]
+ string = info["access_token"]
end
end
-
- # signed_request
- def parse_signed_request(request)
- # Facebook's signed requests come in two parts -- the signature and the data payload
- # see http://developers.facebook.com/docs/authentication/canvas
- encoded_sig, payload = request.split(".")
-
- sig = base64_url_decode(encoded_sig)
+
+ # provided directly by Facebook
+ # see https://github.com/facebook/crypto-request-examples/blob/master/sample.rb
+ # and http://developers.facebook.com/docs/authentication/canvas/encryption_proposal
+ def parse_signed_request(input, max_age = 3600)
+ encoded_sig, encoded_envelope = input.split('.', 2)
+ envelope = JSON.parse(base64_url_decode(encoded_envelope))
+ algorithm = envelope['algorithm']
- # if the signature matches, return the data, decoded and parsed as JSON
- if OpenSSL::HMAC.digest("sha256", @app_secret, payload) == sig
- JSON.parse(base64_url_decode(payload))
- else
- nil
- end
+ raise 'Invalid request. (Unsupported algorithm.)' \
+ if algorithm != 'AES-256-CBC HMAC-SHA256' && algorithm != 'HMAC-SHA256'
+
+ raise 'Invalid request. (Too old.)' \
+ if algorithm == "AES-256-CBC HMAC-SHA256" && envelope['issued_at'].to_i < Time.now.to_i - max_age
+
+ raise 'Invalid request. (Invalid signature.)' \
+ if base64_url_decode(encoded_sig) !=
+ OpenSSL::HMAC.hexdigest(
+ 'sha256', @app_secret, encoded_envelope).split.pack('H*')
+
+ # for requests that are signed, but not encrypted, we're done
+ return envelope if algorithm == 'HMAC-SHA256'
+
+ # otherwise, decrypt the payload
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
+ cipher.decrypt
+ cipher.key = @app_secret
+ cipher.iv = base64_url_decode(envelope['iv'])
+ cipher.padding = 0
+ decrypted_data = cipher.update(base64_url_decode(envelope['payload']))
+ decrypted_data << cipher.final
+ return JSON.parse(decrypted_data.strip)
end
# from session keys
def get_token_info_from_session_keys(sessions)
# fetch the OAuth tokens from Facebook
response = fetch_token_string({
:type => 'client_cred',
:sessions => sessions.join(",")
}, true, "exchange_sessions")
-
+
# Facebook returns an empty body in certain error conditions
- if response == ""
+ if response == ""
raise APIError.new({
- "type" => "ArgumentError",
+ "type" => "ArgumentError",
"message" => "get_token_from_session_key received an error (empty response body) for sessions #{sessions.inspect}!"
})
end
-
+
JSON.parse(response)
end
-
+
def get_tokens_from_session_keys(sessions)
# get the original hash results
results = get_token_info_from_session_keys(sessions)
# now recollect them as just the access tokens
results.collect { |r| r ? r["access_token"] : nil }
end
-
+
def get_token_from_session_key(session)
# convenience method for a single key
# gets the overlaoded strings automatically
get_tokens_from_session_keys([session])[0]
end
-
+
protected
-
+
def get_token_from_server(args, post = false)
# fetch the result from Facebook's servers
result = fetch_token_string(args, post)
-
+
# if we have an error, parse the error JSON and raise an error
raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
# otherwise, parse the access token
- parse_access_token(result)
+ parse_access_token(result)
end
-
+
def parse_access_token(response_text)
components = response_text.split("&").inject({}) do |hash, bit|
key, value = bit.split("=")
hash.merge!(key => value)
end
- components
+ components
end
def fetch_token_string(args, post = false, endpoint = "access_token")
Koala.make_request("/oauth/#{endpoint}", {
- :client_id => @app_id,
+ :client_id => @app_id,
:client_secret => @app_secret
- }.merge!(args), post ? "post" : "get").body
+ }.merge!(args), post ? "post" : "get", :use_ssl => true).body
end
-
+
# base 64
- def base64_url_decode(string)
- # to properly decode what Facebook provides, we need to add == to the end
- # and translate certain characters to others before running the actual decoding
- # see http://developers.facebook.com/docs/authentication/canvas
- "#{string}==".tr("-_", "+/").unpack("m")[0]
+ # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
+ def base64_url_decode(str)
+ str += '=' * (4 - str.length.modulo(4))
+ Base64.decode64(str.gsub('-', '+').gsub('_', '/'))
end
end
end
-
+
# finally, set up the http service Koala methods used to make requests
# you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
def self.http_service=(service)
self.send(:include, service)
end
# by default, try requiring Typhoeus -- if that works, use it
begin
- require 'typhoeus'
Koala.http_service = TyphoeusService
rescue LoadError
Koala.http_service = NetHTTPService
end
end