mislav_contacts 0.2.7
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.
- data/LICENSE +18 -0
- data/README.rdoc +51 -0
- data/lib/config/contacts.yml +10 -0
- data/lib/contacts.rb +51 -0
- data/lib/contacts/flickr.rb +131 -0
- data/lib/contacts/google.rb +304 -0
- data/lib/contacts/version.rb +9 -0
- data/lib/contacts/windows_live.rb +163 -0
- data/lib/contacts/yahoo.rb +238 -0
- data/lib/mislav_contacts.rb +1 -0
- data/vendor/windowslivelogin.rb +1151 -0
- metadata +143 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w{.. .. vendor windowslivelogin}))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hpricot'
|
5
|
+
require 'uri'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module Contacts
|
9
|
+
# = How I can fetch Windows Live Contacts?
|
10
|
+
# To gain access to a Windows Live user's data in the Live Contacts service,
|
11
|
+
# a third-party developer first must ask the owner for permission. You must
|
12
|
+
# do that through Windows Live Delegated Authentication.
|
13
|
+
#
|
14
|
+
# This library give you access to Windows Live Delegated Authentication System
|
15
|
+
# and Windows Live Contacts API. Just follow the steps below and be happy!
|
16
|
+
#
|
17
|
+
# === Registering your app
|
18
|
+
# First of all, follow the steps in this
|
19
|
+
# page[http://msdn.microsoft.com/en-us/library/cc287659.aspx] to register your
|
20
|
+
# app.
|
21
|
+
#
|
22
|
+
# === Configuring your Windows Live YAML
|
23
|
+
# After registering your app, you will have an *appid*, a <b>secret key</b> and
|
24
|
+
# a <b>return URL</b>. Use their values to fill in the config/contacts.yml file.
|
25
|
+
# The policy URL field inside the YAML config file must contain the URL
|
26
|
+
# of the privacy policy of your Web site for Delegated Authentication.
|
27
|
+
#
|
28
|
+
# === Authenticating your user and fetching his contacts
|
29
|
+
#
|
30
|
+
# wl = Contacts::WindowsLive.new
|
31
|
+
# auth_url = wl.get_authentication_url
|
32
|
+
#
|
33
|
+
# Use that *auth_url* to redirect your user to Windows Live. He will authenticate
|
34
|
+
# there and Windows Live will POST to your return URL. You have to get the
|
35
|
+
# body of that POST, let's call it post_body. (if you're using Rails, you can
|
36
|
+
# get the POST body through request.raw_post, in the context of an action inside
|
37
|
+
# ActionController)
|
38
|
+
#
|
39
|
+
# Now, to fetch his contacts, just do this:
|
40
|
+
#
|
41
|
+
# contacts = wl.contacts(post_body)
|
42
|
+
# #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
|
43
|
+
# ['William Paginate', 'will.paginate@gmail.com'], ...
|
44
|
+
# ]
|
45
|
+
#--
|
46
|
+
# This class has two responsibilities:
|
47
|
+
# 1. Access the Windows Live Contacts API through Delegated Authentication
|
48
|
+
# 2. Import contacts from Windows Live and deliver it inside an Array
|
49
|
+
#
|
50
|
+
class WindowsLive
|
51
|
+
CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
|
52
|
+
|
53
|
+
# Initialize a new WindowsLive object.
|
54
|
+
#
|
55
|
+
# ==== Paramaters
|
56
|
+
# * config_file <String>:: The contacts YAML config file name
|
57
|
+
#--
|
58
|
+
# You can check an example of a config file inside config/ directory
|
59
|
+
#
|
60
|
+
def initialize(config_file=CONFIG_FILE)
|
61
|
+
confs = YAML.load_file(config_file)['windows_live']
|
62
|
+
@wll = WindowsLiveLogin.new(confs['appid'], confs['secret'], confs['security_algorithm'],
|
63
|
+
nil, confs['policy_url'], confs['return_url'])
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Windows Live Contacts API need to authenticate the user that is giving you
|
68
|
+
# access to his contacts. To do that, you must give him a URL. That method
|
69
|
+
# generates that URL. The user must access that URL, and after he has done
|
70
|
+
# authentication, hi will be redirected to your application.
|
71
|
+
#
|
72
|
+
def get_authentication_url
|
73
|
+
@wll.getConsentUrl("Contacts.Invite")
|
74
|
+
end
|
75
|
+
|
76
|
+
# After the user has been authenticaded, Windows Live Delegated Authencation
|
77
|
+
# Service redirects to your application, through a POST HTTP method. Along
|
78
|
+
# with the POST, Windows Live send to you a Consent that you must process
|
79
|
+
# to access the user's contacts. This method process the Consent
|
80
|
+
# to you.
|
81
|
+
#
|
82
|
+
# ==== Paramaters
|
83
|
+
# * consent <String>:: A string containing the Consent given to you inside
|
84
|
+
# the redirection POST from Windows Live
|
85
|
+
#
|
86
|
+
def process_consent(consent)
|
87
|
+
consent.strip!
|
88
|
+
consent = URI.unescape(consent)
|
89
|
+
@consent_token = @wll.processConsent(consent)
|
90
|
+
end
|
91
|
+
|
92
|
+
# This method return the user's contacts inside an Array in the following
|
93
|
+
# format:
|
94
|
+
#
|
95
|
+
# [
|
96
|
+
# ['Brad Fitzgerald', 'fubar@gmail.com'],
|
97
|
+
# [nil, 'nagios@hotmail.com'],
|
98
|
+
# ['William Paginate', 'will.paginate@yahoo.com'] ...
|
99
|
+
# ]
|
100
|
+
#
|
101
|
+
# ==== Paramaters
|
102
|
+
# * consent <String>:: A string containing the Consent given to you inside
|
103
|
+
# the redirection POST from Windows Live
|
104
|
+
#
|
105
|
+
def contacts(consent)
|
106
|
+
process_consent(consent)
|
107
|
+
contacts_xml = access_live_contacts_api()
|
108
|
+
contacts_list = WindowsLive.parse_xml(contacts_xml)
|
109
|
+
end
|
110
|
+
|
111
|
+
# This method access the Windows Live Contacts API Web Service to get
|
112
|
+
# the XML contacts document
|
113
|
+
#
|
114
|
+
def access_live_contacts_api
|
115
|
+
http = http = Net::HTTP.new('livecontacts.services.live.com', 443)
|
116
|
+
http.use_ssl = true
|
117
|
+
|
118
|
+
response = nil
|
119
|
+
http.start do |http|
|
120
|
+
request = Net::HTTP::Get.new("/users/@L@#{@consent_token.locationid}/rest/invitationsbyemail", {"Authorization" => "DelegatedToken dt=\"#{@consent_token.delegationtoken}\""})
|
121
|
+
response = http.request(request)
|
122
|
+
end
|
123
|
+
|
124
|
+
return response.body
|
125
|
+
end
|
126
|
+
|
127
|
+
# This method parses the XML Contacts document and returns the contacts
|
128
|
+
# inside an Array
|
129
|
+
#
|
130
|
+
# ==== Paramaters
|
131
|
+
# * xml <String>:: A string containing the XML contacts document
|
132
|
+
#
|
133
|
+
def self.parse_xml(xml)
|
134
|
+
doc = Hpricot::XML(xml)
|
135
|
+
|
136
|
+
contacts = []
|
137
|
+
doc.search('/livecontacts/contacts/contact').each do |contact|
|
138
|
+
email = contact.at('/preferredemail').inner_text
|
139
|
+
email.strip!
|
140
|
+
|
141
|
+
first_name = last_name = nil
|
142
|
+
if first_name = contact.at('/profiles/personal/firstname')
|
143
|
+
first_name = first_name.inner_text.strip
|
144
|
+
end
|
145
|
+
|
146
|
+
if last_name = contact.at('/profiles/personal/lastname')
|
147
|
+
last_name = last_name.inner_text.strip
|
148
|
+
end
|
149
|
+
|
150
|
+
name = nil
|
151
|
+
if !first_name.nil? || !last_name.nil?
|
152
|
+
name = "#{first_name} #{last_name}"
|
153
|
+
name.strip!
|
154
|
+
end
|
155
|
+
new_contact = Contact.new(email, name)
|
156
|
+
contacts << new_contact
|
157
|
+
end
|
158
|
+
|
159
|
+
return contacts
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hpricot'
|
3
|
+
require 'md5'
|
4
|
+
require 'net/https'
|
5
|
+
require 'uri'
|
6
|
+
require 'yaml'
|
7
|
+
require 'json' unless defined? ActiveSupport::JSON
|
8
|
+
|
9
|
+
module Contacts
|
10
|
+
# = How I can fetch Yahoo Contacts?
|
11
|
+
# To gain access to a Yahoo user's data in the Yahoo Address Book Service,
|
12
|
+
# a third-party developer first must ask the owner for permission. You must
|
13
|
+
# do that through Yahoo Browser Based Authentication (BBAuth).
|
14
|
+
#
|
15
|
+
# This library give you access to Yahoo BBAuth and Yahoo Address Book API.
|
16
|
+
# Just follow the steps below and be happy!
|
17
|
+
#
|
18
|
+
# === Registering your app
|
19
|
+
# First of all, follow the steps in this
|
20
|
+
# page[http://developer.yahoo.com/wsregapp/] to register your app. If you need
|
21
|
+
# some help with that form, you can get it
|
22
|
+
# here[http://developer.yahoo.com/auth/appreg.html]. Just two tips: inside
|
23
|
+
# <b>Required access scopes</b> in that registration form, choose
|
24
|
+
# <b>Yahoo! Address Book with Read Only access</b>. Inside
|
25
|
+
# <b>Authentication method</b> choose <b>Browser Based Authentication</b>.
|
26
|
+
#
|
27
|
+
# === Configuring your Yahoo YAML
|
28
|
+
# After registering your app, you will have an <b>application id</b> and a
|
29
|
+
# <b>shared secret</b>. Use their values to fill in the config/contacts.yml
|
30
|
+
# file.
|
31
|
+
#
|
32
|
+
# === Authenticating your user and fetching his contacts
|
33
|
+
#
|
34
|
+
# yahoo = Contacts::Yahoo.new
|
35
|
+
# auth_url = yahoo.get_authentication_url
|
36
|
+
#
|
37
|
+
# Use that *auth_url* to redirect your user to Yahoo BBAuth. He will authenticate
|
38
|
+
# there and Yahoo will redirect to your application entrypoint URL (that you provided
|
39
|
+
# while registering your app with Yahoo). You have to get the path of that
|
40
|
+
# redirect, let's call it path (if you're using Rails, you can get it through
|
41
|
+
# request.request_uri, in the context of an action inside ActionController)
|
42
|
+
#
|
43
|
+
# Now, to fetch his contacts, just do this:
|
44
|
+
#
|
45
|
+
# contacts = wl.contacts(path)
|
46
|
+
# #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
|
47
|
+
# ['William Paginate', 'will.paginate@gmail.com'], ...
|
48
|
+
# ]
|
49
|
+
#--
|
50
|
+
# This class has two responsibilities:
|
51
|
+
# 1. Access the Yahoo Address Book API through Delegated Authentication
|
52
|
+
# 2. Import contacts from Yahoo Mail and deliver it inside an Array
|
53
|
+
#
|
54
|
+
class Yahoo
|
55
|
+
AUTH_DOMAIN = "https://api.login.yahoo.com"
|
56
|
+
AUTH_PATH = "/WSLogin/V1/wslogin?appid=#appid&ts=#ts"
|
57
|
+
CREDENTIAL_PATH = "/WSLogin/V1/wspwtoken_login?appid=#appid&ts=#ts&token=#token"
|
58
|
+
ADDRESS_BOOK_DOMAIN = "address.yahooapis.com"
|
59
|
+
ADDRESS_BOOK_PATH = "/v1/searchContacts?format=json&fields=name,email&appid=#appid&WSSID=#wssid"
|
60
|
+
CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
|
61
|
+
|
62
|
+
attr_reader :appid, :secret, :token, :wssid, :cookie
|
63
|
+
|
64
|
+
# Initialize a new Yahoo object.
|
65
|
+
#
|
66
|
+
# ==== Paramaters
|
67
|
+
# * config_file <String>:: The contacts YAML config file name
|
68
|
+
#--
|
69
|
+
# You can check an example of a config file inside config/ directory
|
70
|
+
#
|
71
|
+
def initialize(config_file=CONFIG_FILE)
|
72
|
+
confs = YAML.load_file(config_file)['yahoo']
|
73
|
+
@appid = confs['appid']
|
74
|
+
@secret = confs['secret']
|
75
|
+
end
|
76
|
+
|
77
|
+
# Yahoo Address Book API need to authenticate the user that is giving you
|
78
|
+
# access to his contacts. To do that, you must give him a URL. This method
|
79
|
+
# generates that URL. The user must access that URL, and after he has done
|
80
|
+
# authentication, hi will be redirected to your application.
|
81
|
+
#
|
82
|
+
def get_authentication_url
|
83
|
+
path = AUTH_PATH.clone
|
84
|
+
path.sub!(/#appid/, @appid)
|
85
|
+
|
86
|
+
timestamp = Time.now.utc.to_i
|
87
|
+
path.sub!(/#ts/, timestamp.to_s)
|
88
|
+
|
89
|
+
signature = MD5.hexdigest(path + @secret)
|
90
|
+
return AUTH_DOMAIN + "#{path}&sig=#{signature}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# This method return the user's contacts inside an Array in the following
|
94
|
+
# format:
|
95
|
+
#
|
96
|
+
# [
|
97
|
+
# ['Brad Fitzgerald', 'fubar@gmail.com'],
|
98
|
+
# [nil, 'nagios@hotmail.com'],
|
99
|
+
# ['William Paginate', 'will.paginate@yahoo.com'] ...
|
100
|
+
# ]
|
101
|
+
#
|
102
|
+
# ==== Paramaters
|
103
|
+
# * path <String>:: The path of the redirect request that Yahoo sent to you
|
104
|
+
# after authenticating the user
|
105
|
+
#
|
106
|
+
def contacts(path)
|
107
|
+
begin
|
108
|
+
validate_signature(path)
|
109
|
+
credentials = access_user_credentials()
|
110
|
+
parse_credentials(credentials)
|
111
|
+
contacts_json = access_address_book_api()
|
112
|
+
Yahoo.parse_contacts(contacts_json)
|
113
|
+
rescue Exception => e
|
114
|
+
"Error #{e.class}: #{e.message}."
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# This method processes and validates the redirect request that Yahoo send to
|
119
|
+
# you. Validation is done to verify that the request was really made by
|
120
|
+
# Yahoo. Processing is done to get the token.
|
121
|
+
#
|
122
|
+
# ==== Paramaters
|
123
|
+
# * path <String>:: The path of the redirect request that Yahoo sent to you
|
124
|
+
# after authenticating the user
|
125
|
+
#
|
126
|
+
def validate_signature(path)
|
127
|
+
path.match(/^(.+)&sig=(\w{32})$/)
|
128
|
+
path_without_sig = $1
|
129
|
+
sig = $2
|
130
|
+
|
131
|
+
if sig == MD5.hexdigest(path_without_sig + @secret)
|
132
|
+
path.match(/token=(.+?)&/)
|
133
|
+
@token = $1
|
134
|
+
return true
|
135
|
+
else
|
136
|
+
raise 'Signature not valid. This request may not have been sent from Yahoo.'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# This method accesses Yahoo to retrieve the user's credentials.
|
141
|
+
#
|
142
|
+
def access_user_credentials
|
143
|
+
url = get_credential_url()
|
144
|
+
uri = URI.parse(url)
|
145
|
+
|
146
|
+
http = http = Net::HTTP.new(uri.host, uri.port)
|
147
|
+
http.use_ssl = true
|
148
|
+
|
149
|
+
response = nil
|
150
|
+
http.start do |http|
|
151
|
+
request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
|
152
|
+
response = http.request(request)
|
153
|
+
end
|
154
|
+
|
155
|
+
return response.body
|
156
|
+
end
|
157
|
+
|
158
|
+
# This method generates the URL that you must access to get user's
|
159
|
+
# credentials.
|
160
|
+
#
|
161
|
+
def get_credential_url
|
162
|
+
path = CREDENTIAL_PATH.clone
|
163
|
+
path.sub!(/#appid/, @appid)
|
164
|
+
|
165
|
+
path.sub!(/#token/, @token)
|
166
|
+
|
167
|
+
timestamp = Time.now.utc.to_i
|
168
|
+
path.sub!(/#ts/, timestamp.to_s)
|
169
|
+
|
170
|
+
signature = MD5.hexdigest(path + @secret)
|
171
|
+
return AUTH_DOMAIN + "#{path}&sig=#{signature}"
|
172
|
+
end
|
173
|
+
|
174
|
+
# This method parses the user's credentials to generate the WSSID and
|
175
|
+
# Coookie that are needed to give you access to user's address book.
|
176
|
+
#
|
177
|
+
# ==== Paramaters
|
178
|
+
# * xml <String>:: A String containing the user's credentials
|
179
|
+
#
|
180
|
+
def parse_credentials(xml)
|
181
|
+
doc = Hpricot::XML(xml)
|
182
|
+
@wssid = doc.at('/BBAuthTokenLoginResponse/Success/WSSID').inner_text.strip
|
183
|
+
@cookie = doc.at('/BBAuthTokenLoginResponse/Success/Cookie').inner_text.strip
|
184
|
+
end
|
185
|
+
|
186
|
+
# This method accesses the Yahoo Address Book API and retrieves the user's
|
187
|
+
# contacts in JSON.
|
188
|
+
#
|
189
|
+
def access_address_book_api
|
190
|
+
http = http = Net::HTTP.new(ADDRESS_BOOK_DOMAIN, 80)
|
191
|
+
|
192
|
+
response = nil
|
193
|
+
http.start do |http|
|
194
|
+
path = ADDRESS_BOOK_PATH.clone
|
195
|
+
path.sub!(/#appid/, @appid)
|
196
|
+
path.sub!(/#wssid/, @wssid)
|
197
|
+
|
198
|
+
request = Net::HTTP::Get.new(path, {'Cookie' => @cookie})
|
199
|
+
response = http.request(request)
|
200
|
+
end
|
201
|
+
|
202
|
+
return response.body
|
203
|
+
end
|
204
|
+
|
205
|
+
# This method parses the JSON contacts document and returns an array
|
206
|
+
# contaning all the user's contacts.
|
207
|
+
#
|
208
|
+
# ==== Parameters
|
209
|
+
# * json <String>:: A String of user's contacts in JSON format
|
210
|
+
#
|
211
|
+
def self.parse_contacts(json)
|
212
|
+
contacts = []
|
213
|
+
people = if defined? ActiveSupport::JSON
|
214
|
+
ActiveSupport::JSON.decode(json)
|
215
|
+
else
|
216
|
+
JSON.parse(json)
|
217
|
+
end
|
218
|
+
|
219
|
+
people['contacts'].each do |contact|
|
220
|
+
name = nil
|
221
|
+
email = nil
|
222
|
+
contact['fields'].each do |field|
|
223
|
+
case field['type']
|
224
|
+
when 'email'
|
225
|
+
email = field['data']
|
226
|
+
email.strip!
|
227
|
+
when 'name'
|
228
|
+
name = "#{field['first']} #{field['last']}"
|
229
|
+
name.strip!
|
230
|
+
end
|
231
|
+
end
|
232
|
+
contacts.push Contact.new(email, name)
|
233
|
+
end
|
234
|
+
return contacts
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'contacts'
|
@@ -0,0 +1,1151 @@
|
|
1
|
+
#######################################################################
|
2
|
+
# This SDK is provide by Microsoft. I just use it for the delegated
|
3
|
+
# authentication part. You can find it in http://www.microsoft.com/downloads/details.aspx?FamilyId=24195B4E-6335-4844-A71D-7D395D20E67B&displaylang=en
|
4
|
+
#
|
5
|
+
# Author: Hugo Baraúna (hugo.barauna@gmail.com)
|
6
|
+
#######################################################################
|
7
|
+
|
8
|
+
|
9
|
+
#######################################################################
|
10
|
+
#######################################################################
|
11
|
+
# FILE: windowslivelogin.rb
|
12
|
+
#
|
13
|
+
# DESCRIPTION: Sample implementation of Web Authentication and
|
14
|
+
# Delegated Authentication protocol in Ruby. Also
|
15
|
+
# includes trusted sign-in and application verification
|
16
|
+
# sample implementations.
|
17
|
+
#
|
18
|
+
# VERSION: 1.1
|
19
|
+
#
|
20
|
+
# Copyright (c) 2008 Microsoft Corporation. All Rights Reserved.
|
21
|
+
#######################################################################
|
22
|
+
|
23
|
+
require 'cgi'
|
24
|
+
require 'uri'
|
25
|
+
require 'base64'
|
26
|
+
require 'openssl'
|
27
|
+
require 'net/https'
|
28
|
+
require 'rexml/document'
|
29
|
+
|
30
|
+
class WindowsLiveLogin
|
31
|
+
|
32
|
+
#####################################################################
|
33
|
+
# Stub implementation for logging errors. If you want to enable
|
34
|
+
# debugging output using the default mechanism, specify true.
|
35
|
+
# By default, debug information will be printed to the standard
|
36
|
+
# error output and should be visible in the web server logs.
|
37
|
+
#####################################################################
|
38
|
+
def setDebug(flag)
|
39
|
+
@debug = flag
|
40
|
+
end
|
41
|
+
|
42
|
+
#####################################################################
|
43
|
+
# Stub implementation for logging errors. By default, this function
|
44
|
+
# does nothing if the debug flag has not been set with setDebug.
|
45
|
+
# Otherwise, it tries to log the error message.
|
46
|
+
#####################################################################
|
47
|
+
def debug(error)
|
48
|
+
return unless @debug
|
49
|
+
return if error.nil? or error.empty?
|
50
|
+
warn("Windows Live ID Authentication SDK #{error}")
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
#####################################################################
|
55
|
+
# Stub implementation for handling a fatal error.
|
56
|
+
#####################################################################
|
57
|
+
def fatal(error)
|
58
|
+
debug(error)
|
59
|
+
raise(error)
|
60
|
+
end
|
61
|
+
|
62
|
+
#####################################################################
|
63
|
+
# Initialize the WindowsLiveLogin module with the application ID,
|
64
|
+
# secret key, and security algorithm.
|
65
|
+
#
|
66
|
+
# We recommend that you employ strong measures to protect the
|
67
|
+
# secret key. The secret key should never be exposed to the Web
|
68
|
+
# or other users.
|
69
|
+
#
|
70
|
+
# Be aware that if you do not supply these settings at
|
71
|
+
# initialization time, you may need to set the corresponding
|
72
|
+
# properties manually.
|
73
|
+
#
|
74
|
+
# For Delegated Authentication, you may optionally specify the
|
75
|
+
# privacy policy URL and return URL. If you do not specify these
|
76
|
+
# values here, the default values that you specified when you
|
77
|
+
# registered your application will be used.
|
78
|
+
#
|
79
|
+
# The 'force_delauth_nonprovisioned' flag also indicates whether
|
80
|
+
# your application is registered for Delegated Authentication
|
81
|
+
# (that is, whether it uses an application ID and secret key). We
|
82
|
+
# recommend that your Delegated Authentication application always
|
83
|
+
# be registered for enhanced security and functionality.
|
84
|
+
#####################################################################
|
85
|
+
def initialize(appid=nil, secret=nil, securityalgorithm=nil,
|
86
|
+
force_delauth_nonprovisioned=nil,
|
87
|
+
policyurl=nil, returnurl=nil)
|
88
|
+
self.force_delauth_nonprovisioned = force_delauth_nonprovisioned
|
89
|
+
self.appid = appid if appid
|
90
|
+
self.secret = secret if secret
|
91
|
+
self.securityalgorithm = securityalgorithm if securityalgorithm
|
92
|
+
self.policyurl = policyurl if policyurl
|
93
|
+
self.returnurl = returnurl if returnurl
|
94
|
+
end
|
95
|
+
|
96
|
+
#####################################################################
|
97
|
+
# Initialize the WindowsLiveLogin module from a settings file.
|
98
|
+
#
|
99
|
+
# 'settingsFile' specifies the location of the XML settings file
|
100
|
+
# that contains the application ID, secret key, and security
|
101
|
+
# algorithm. The file is of the following format:
|
102
|
+
#
|
103
|
+
# <windowslivelogin>
|
104
|
+
# <appid>APPID</appid>
|
105
|
+
# <secret>SECRET</secret>
|
106
|
+
# <securityalgorithm>wsignin1.0</securityalgorithm>
|
107
|
+
# </windowslivelogin>
|
108
|
+
#
|
109
|
+
# In a Delegated Authentication scenario, you may also specify
|
110
|
+
# 'returnurl' and 'policyurl' in the settings file, as shown in the
|
111
|
+
# Delegated Authentication samples.
|
112
|
+
#
|
113
|
+
# We recommend that you store the WindowsLiveLogin settings file
|
114
|
+
# in an area on your server that cannot be accessed through the
|
115
|
+
# Internet. This file contains important confidential information.
|
116
|
+
#####################################################################
|
117
|
+
def self.initFromXml(settingsFile)
|
118
|
+
o = self.new
|
119
|
+
settings = o.parseSettings(settingsFile)
|
120
|
+
|
121
|
+
o.setDebug(settings['debug'] == 'true')
|
122
|
+
o.force_delauth_nonprovisioned =
|
123
|
+
(settings['force_delauth_nonprovisioned'] == 'true')
|
124
|
+
|
125
|
+
o.appid = settings['appid']
|
126
|
+
o.secret = settings['secret']
|
127
|
+
o.oldsecret = settings['oldsecret']
|
128
|
+
o.oldsecretexpiry = settings['oldsecretexpiry']
|
129
|
+
o.securityalgorithm = settings['securityalgorithm']
|
130
|
+
o.policyurl = settings['policyurl']
|
131
|
+
o.returnurl = settings['returnurl']
|
132
|
+
o.baseurl = settings['baseurl']
|
133
|
+
o.secureurl = settings['secureurl']
|
134
|
+
o.consenturl = settings['consenturl']
|
135
|
+
o
|
136
|
+
end
|
137
|
+
|
138
|
+
#####################################################################
|
139
|
+
# Sets the application ID. Use this method if you did not specify
|
140
|
+
# an application ID at initialization.
|
141
|
+
#####################################################################
|
142
|
+
def appid=(appid)
|
143
|
+
if (appid.nil? or appid.empty?)
|
144
|
+
return if force_delauth_nonprovisioned
|
145
|
+
fatal("Error: appid: Null application ID.")
|
146
|
+
end
|
147
|
+
if (not appid =~ /^\w+$/)
|
148
|
+
fatal("Error: appid: Application ID must be alpha-numeric: " + appid)
|
149
|
+
end
|
150
|
+
@appid = appid
|
151
|
+
end
|
152
|
+
|
153
|
+
#####################################################################
|
154
|
+
# Returns the application ID.
|
155
|
+
#####################################################################
|
156
|
+
def appid
|
157
|
+
if (@appid.nil? or @appid.empty?)
|
158
|
+
fatal("Error: appid: App ID was not set. Aborting.")
|
159
|
+
end
|
160
|
+
@appid
|
161
|
+
end
|
162
|
+
|
163
|
+
#####################################################################
|
164
|
+
# Sets your secret key. Use this method if you did not specify
|
165
|
+
# a secret key at initialization.
|
166
|
+
#####################################################################
|
167
|
+
def secret=(secret)
|
168
|
+
if (secret.nil? or secret.empty?)
|
169
|
+
return if force_delauth_nonprovisioned
|
170
|
+
fatal("Error: secret=: Secret must be non-null.")
|
171
|
+
end
|
172
|
+
if (secret.size < 16)
|
173
|
+
fatal("Error: secret=: Secret must be at least 16 characters.")
|
174
|
+
end
|
175
|
+
@signkey = derive(secret, "SIGNATURE")
|
176
|
+
@cryptkey = derive(secret, "ENCRYPTION")
|
177
|
+
end
|
178
|
+
|
179
|
+
#####################################################################
|
180
|
+
# Sets your old secret key.
|
181
|
+
#
|
182
|
+
# Use this property to set your old secret key if you are in the
|
183
|
+
# process of transitioning to a new secret key. You may need this
|
184
|
+
# property because the Windows Live ID servers can take up to
|
185
|
+
# 24 hours to propagate a new secret key after you have updated
|
186
|
+
# your application settings.
|
187
|
+
#
|
188
|
+
# If an old secret key is specified here and has not expired
|
189
|
+
# (as determined by the oldsecretexpiry setting), it will be used
|
190
|
+
# as a fallback if token decryption fails with the new secret
|
191
|
+
# key.
|
192
|
+
#####################################################################
|
193
|
+
def oldsecret=(secret)
|
194
|
+
return if (secret.nil? or secret.empty?)
|
195
|
+
if (secret.size < 16)
|
196
|
+
fatal("Error: oldsecret=: Secret must be at least 16 characters.")
|
197
|
+
end
|
198
|
+
@oldsignkey = derive(secret, "SIGNATURE")
|
199
|
+
@oldcryptkey = derive(secret, "ENCRYPTION")
|
200
|
+
end
|
201
|
+
|
202
|
+
#####################################################################
|
203
|
+
# Sets the expiry time for your old secret key.
|
204
|
+
#
|
205
|
+
# After this time has passed, the old secret key will no longer be
|
206
|
+
# used even if token decryption fails with the new secret key.
|
207
|
+
#
|
208
|
+
# The old secret expiry time is represented as the number of seconds
|
209
|
+
# elapsed since January 1, 1970.
|
210
|
+
#####################################################################
|
211
|
+
def oldsecretexpiry=(timestamp)
|
212
|
+
return if (timestamp.nil? or timestamp.empty?)
|
213
|
+
timestamp = timestamp.to_i
|
214
|
+
fatal("Error: oldsecretexpiry=: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
|
215
|
+
@oldsecretexpiry = Time.at timestamp
|
216
|
+
end
|
217
|
+
|
218
|
+
#####################################################################
|
219
|
+
# Gets the old secret key expiry time.
|
220
|
+
#####################################################################
|
221
|
+
attr_accessor :oldsecretexpiry
|
222
|
+
|
223
|
+
#####################################################################
|
224
|
+
# Sets or gets the version of the security algorithm being used.
|
225
|
+
#####################################################################
|
226
|
+
attr_accessor :securityalgorithm
|
227
|
+
|
228
|
+
def securityalgorithm
|
229
|
+
if(@securityalgorithm.nil? or @securityalgorithm.empty?)
|
230
|
+
"wsignin1.0"
|
231
|
+
else
|
232
|
+
@securityalgorithm
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
#####################################################################
|
237
|
+
# Sets a flag that indicates whether Delegated Authentication
|
238
|
+
# is non-provisioned (i.e. does not use an application ID or secret
|
239
|
+
# key).
|
240
|
+
#####################################################################
|
241
|
+
attr_accessor :force_delauth_nonprovisioned
|
242
|
+
|
243
|
+
#####################################################################
|
244
|
+
# Sets the privacy policy URL, to which the Windows Live ID consent
|
245
|
+
# service redirects users to view the privacy policy of your Web
|
246
|
+
# site for Delegated Authentication.
|
247
|
+
#####################################################################
|
248
|
+
def policyurl=(policyurl)
|
249
|
+
if ((policyurl.nil? or policyurl.empty?) and force_delauth_nonprovisioned)
|
250
|
+
fatal("Error: policyurl=: Invalid policy URL specified.")
|
251
|
+
end
|
252
|
+
@policyurl = policyurl
|
253
|
+
end
|
254
|
+
|
255
|
+
#####################################################################
|
256
|
+
# Gets the privacy policy URL for your site.
|
257
|
+
#####################################################################
|
258
|
+
def policyurl
|
259
|
+
if (@policyurl.nil? or @policyurl.empty?)
|
260
|
+
debug("Warning: In the initial release of Del Auth, a Policy URL must be configured in the SDK for both provisioned and non-provisioned scenarios.")
|
261
|
+
raise("Error: policyurl: Policy URL must be set in a Del Auth non-provisioned scenario. Aborting.") if force_delauth_nonprovisioned
|
262
|
+
end
|
263
|
+
@policyurl
|
264
|
+
end
|
265
|
+
|
266
|
+
#####################################################################
|
267
|
+
# Sets the return URL--the URL on your site to which the consent
|
268
|
+
# service redirects users (along with the action, consent token,
|
269
|
+
# and application context) after they have successfully provided
|
270
|
+
# consent information for Delegated Authentication. This value will
|
271
|
+
# override the return URL specified during registration.
|
272
|
+
#####################################################################
|
273
|
+
def returnurl=(returnurl)
|
274
|
+
if ((returnurl.nil? or returnurl.empty?) and force_delauth_nonprovisioned)
|
275
|
+
fatal("Error: returnurl=: Invalid return URL specified.")
|
276
|
+
end
|
277
|
+
@returnurl = returnurl
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
#####################################################################
|
282
|
+
# Returns the return URL of your site.
|
283
|
+
#####################################################################
|
284
|
+
def returnurl
|
285
|
+
if ((@returnurl.nil? or @returnurl.empty?) and force_delauth_nonprovisioned)
|
286
|
+
fatal("Error: returnurl: Return URL must be set in a Del Auth non-provisioned scenario. Aborting.")
|
287
|
+
end
|
288
|
+
@returnurl
|
289
|
+
end
|
290
|
+
|
291
|
+
#####################################################################
|
292
|
+
# Sets or gets the base URL to use for the Windows Live Login server. You
|
293
|
+
# should not have to change this property. Furthermore, we recommend
|
294
|
+
# that you use the Sign In control instead of the URL methods
|
295
|
+
# provided here.
|
296
|
+
#####################################################################
|
297
|
+
attr_accessor :baseurl
|
298
|
+
|
299
|
+
def baseurl
|
300
|
+
if(@baseurl.nil? or @baseurl.empty?)
|
301
|
+
"http://login.live.com/"
|
302
|
+
else
|
303
|
+
@baseurl
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
#####################################################################
|
308
|
+
# Sets or gets the secure (HTTPS) URL to use for the Windows Live Login
|
309
|
+
# server. You should not have to change this property.
|
310
|
+
#####################################################################
|
311
|
+
attr_accessor :secureurl
|
312
|
+
|
313
|
+
def secureurl
|
314
|
+
if(@secureurl.nil? or @secureurl.empty?)
|
315
|
+
"https://login.live.com/"
|
316
|
+
else
|
317
|
+
@secureurl
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
#####################################################################
|
322
|
+
# Sets or gets the Consent Base URL to use for the Windows Live Consent
|
323
|
+
# server. You should not have to use or change this property directly.
|
324
|
+
#####################################################################
|
325
|
+
attr_accessor :consenturl
|
326
|
+
|
327
|
+
def consenturl
|
328
|
+
if(@consenturl.nil? or @consenturl.empty?)
|
329
|
+
"https://consent.live.com/"
|
330
|
+
else
|
331
|
+
@consenturl
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
#######################################################################
|
337
|
+
# Implementation of the basic methods needed for Web Authentication.
|
338
|
+
#######################################################################
|
339
|
+
class WindowsLiveLogin
|
340
|
+
#####################################################################
|
341
|
+
# Returns the sign-in URL to use for the Windows Live Login server.
|
342
|
+
# We recommend that you use the Sign In control instead.
|
343
|
+
#
|
344
|
+
# If you specify it, 'context' will be returned as-is in the sign-in
|
345
|
+
# response for site-specific use.
|
346
|
+
#####################################################################
|
347
|
+
def getLoginUrl(context=nil, market=nil)
|
348
|
+
url = baseurl + "wlogin.srf?appid=#{appid}"
|
349
|
+
url += "&alg=#{securityalgorithm}"
|
350
|
+
url += "&appctx=#{CGI.escape(context)}" if context
|
351
|
+
url += "&mkt=#{CGI.escape(market)}" if market
|
352
|
+
url
|
353
|
+
end
|
354
|
+
|
355
|
+
#####################################################################
|
356
|
+
# Returns the sign-out URL to use for the Windows Live Login server.
|
357
|
+
# We recommend that you use the Sign In control instead.
|
358
|
+
#####################################################################
|
359
|
+
def getLogoutUrl(market=nil)
|
360
|
+
url = baseurl + "logout.srf?appid=#{appid}"
|
361
|
+
url += "&mkt=#{CGI.escape(market)}" if market
|
362
|
+
url
|
363
|
+
end
|
364
|
+
|
365
|
+
#####################################################################
|
366
|
+
# Holds the user information after a successful sign-in.
|
367
|
+
#
|
368
|
+
# 'timestamp' is the time as obtained from the SSO token.
|
369
|
+
# 'id' is the pairwise unique ID for the user.
|
370
|
+
# 'context' is the application context that was originally passed to
|
371
|
+
# the sign-in request, if any.
|
372
|
+
# 'token' is the encrypted Web Authentication token that contains the
|
373
|
+
# UID. This can be cached in a cookie and the UID can be retrieved by
|
374
|
+
# calling the processToken method.
|
375
|
+
# 'usePersistentCookie?' indicates whether the application is
|
376
|
+
# expected to store the user token in a session or persistent
|
377
|
+
# cookie.
|
378
|
+
#####################################################################
|
379
|
+
class User
|
380
|
+
attr_reader :timestamp, :id, :context, :token
|
381
|
+
|
382
|
+
def usePersistentCookie?
|
383
|
+
@usePersistentCookie
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
#####################################################################
|
388
|
+
# Initialize the User with time stamp, userid, flags, context and token.
|
389
|
+
#####################################################################
|
390
|
+
def initialize(timestamp, id, flags, context, token)
|
391
|
+
self.timestamp = timestamp
|
392
|
+
self.id = id
|
393
|
+
self.flags = flags
|
394
|
+
self.context = context
|
395
|
+
self.token = token
|
396
|
+
end
|
397
|
+
|
398
|
+
private
|
399
|
+
attr_writer :timestamp, :id, :flags, :context, :token
|
400
|
+
|
401
|
+
#####################################################################
|
402
|
+
# Sets or gets the Unix timestamp as obtained from the SSO token.
|
403
|
+
#####################################################################
|
404
|
+
def timestamp=(timestamp)
|
405
|
+
raise("Error: User: Null timestamp in token.") unless timestamp
|
406
|
+
timestamp = timestamp.to_i
|
407
|
+
raise("Error: User: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
|
408
|
+
@timestamp = Time.at timestamp
|
409
|
+
end
|
410
|
+
|
411
|
+
#####################################################################
|
412
|
+
# Sets or gets the pairwise unique ID for the user.
|
413
|
+
#####################################################################
|
414
|
+
def id=(id)
|
415
|
+
raise("Error: User: Null id in token.") unless id
|
416
|
+
raise("Error: User: Invalid id: #{id}") unless (id =~ /^\w+$/)
|
417
|
+
@id = id
|
418
|
+
end
|
419
|
+
|
420
|
+
#####################################################################
|
421
|
+
# Sets or gets the usePersistentCookie flag for the user.
|
422
|
+
#####################################################################
|
423
|
+
def flags=(flags)
|
424
|
+
@usePersistentCookie = false
|
425
|
+
if flags
|
426
|
+
@usePersistentCookie = ((flags.to_i % 2) == 1)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
#####################################################################
|
432
|
+
# Processes the sign-in response from the Windows Live sign-in server.
|
433
|
+
#
|
434
|
+
# 'query' contains the preprocessed POST table, such as that
|
435
|
+
# returned by CGI.params or Rails. (The unprocessed POST string
|
436
|
+
# could also be used here but we do not recommend it).
|
437
|
+
#
|
438
|
+
# This method returns a User object on successful sign-in; otherwise
|
439
|
+
# it returns nil.
|
440
|
+
#####################################################################
|
441
|
+
def processLogin(query)
|
442
|
+
query = parse query
|
443
|
+
unless query
|
444
|
+
debug("Error: processLogin: Failed to parse query.")
|
445
|
+
return
|
446
|
+
end
|
447
|
+
action = query['action']
|
448
|
+
unless action == 'login'
|
449
|
+
debug("Warning: processLogin: query action ignored: #{action}.")
|
450
|
+
return
|
451
|
+
end
|
452
|
+
token = query['stoken']
|
453
|
+
context = CGI.unescape(query['appctx']) if query['appctx']
|
454
|
+
processToken(token, context)
|
455
|
+
end
|
456
|
+
|
457
|
+
#####################################################################
|
458
|
+
# Decodes and validates a Web Authentication token. Returns a User
|
459
|
+
# object on success. If a context is passed in, it will be returned
|
460
|
+
# as the context field in the User object.
|
461
|
+
#####################################################################
|
462
|
+
def processToken(token, context=nil)
|
463
|
+
if token.nil? or token.empty?
|
464
|
+
debug("Error: processToken: Null/empty token.")
|
465
|
+
return
|
466
|
+
end
|
467
|
+
stoken = decodeAndValidateToken token
|
468
|
+
stoken = parse stoken
|
469
|
+
unless stoken
|
470
|
+
debug("Error: processToken: Failed to decode/validate token: #{token}")
|
471
|
+
return
|
472
|
+
end
|
473
|
+
sappid = stoken['appid']
|
474
|
+
unless sappid == appid
|
475
|
+
debug("Error: processToken: Application ID in token did not match ours: #{sappid}, #{appid}")
|
476
|
+
return
|
477
|
+
end
|
478
|
+
begin
|
479
|
+
user = User.new(stoken['ts'], stoken['uid'], stoken['flags'],
|
480
|
+
context, token)
|
481
|
+
return user
|
482
|
+
rescue Exception => e
|
483
|
+
debug("Error: processToken: Contents of token considered invalid: #{e}")
|
484
|
+
return
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
#####################################################################
|
489
|
+
# Returns an appropriate content type and body response that the
|
490
|
+
# application handler can return to signify a successful sign-out
|
491
|
+
# from the application.
|
492
|
+
#
|
493
|
+
# When a user signs out of Windows Live or a Windows Live
|
494
|
+
# application, a best-effort attempt is made at signing the user out
|
495
|
+
# from all other Windows Live applications the user might be signed
|
496
|
+
# in to. This is done by calling the handler page for each
|
497
|
+
# application with 'action' set to 'clearcookie' in the query
|
498
|
+
# string. The application handler is then responsible for clearing
|
499
|
+
# any cookies or data associated with the sign-in. After successfully
|
500
|
+
# signing the user out, the handler should return a GIF (any GIF)
|
501
|
+
# image as response to the 'action=clearcookie' query.
|
502
|
+
#####################################################################
|
503
|
+
def getClearCookieResponse()
|
504
|
+
type = "image/gif"
|
505
|
+
content = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7"
|
506
|
+
content = Base64.decode64(content)
|
507
|
+
return type, content
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
#######################################################################
|
512
|
+
# Implementation of the basic methods needed for Delegated
|
513
|
+
# Authentication.
|
514
|
+
#######################################################################
|
515
|
+
class WindowsLiveLogin
|
516
|
+
#####################################################################
|
517
|
+
# Returns the consent URL to use for Delegated Authentication for
|
518
|
+
# the given comma-delimited list of offers.
|
519
|
+
#
|
520
|
+
# If you specify it, 'context' will be returned as-is in the consent
|
521
|
+
# response for site-specific use.
|
522
|
+
#
|
523
|
+
# The registered/configured return URL can also be overridden by
|
524
|
+
# specifying 'ru' here.
|
525
|
+
#
|
526
|
+
# You can change the language in which the consent page is displayed
|
527
|
+
# by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
|
528
|
+
# 'market' parameter.
|
529
|
+
#####################################################################
|
530
|
+
def getConsentUrl(offers, context=nil, ru=nil, market=nil)
|
531
|
+
if (offers.nil? or offers.empty?)
|
532
|
+
fatal("Error: getConsentUrl: Invalid offers list.")
|
533
|
+
end
|
534
|
+
url = consenturl + "Delegation.aspx?ps=#{CGI.escape(offers)}"
|
535
|
+
url += "&appctx=#{CGI.escape(context)}" if context
|
536
|
+
ru = returnurl if (ru.nil? or ru.empty?)
|
537
|
+
url += "&ru=#{CGI.escape(ru)}" if ru
|
538
|
+
pu = policyurl
|
539
|
+
url += "&pl=#{CGI.escape(pu)}" if pu
|
540
|
+
url += "&mkt=#{CGI.escape(market)}" if market
|
541
|
+
url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
|
542
|
+
url
|
543
|
+
end
|
544
|
+
|
545
|
+
#####################################################################
|
546
|
+
# Returns the URL to use to download a new consent token, given the
|
547
|
+
# offers and refresh token.
|
548
|
+
# The registered/configured return URL can also be overridden by
|
549
|
+
# specifying 'ru' here.
|
550
|
+
#####################################################################
|
551
|
+
def getRefreshConsentTokenUrl(offers, refreshtoken, ru)
|
552
|
+
if (offers.nil? or offers.empty?)
|
553
|
+
fatal("Error: getRefreshConsentTokenUrl: Invalid offers list.")
|
554
|
+
end
|
555
|
+
if (refreshtoken.nil? or refreshtoken.empty?)
|
556
|
+
fatal("Error: getRefreshConsentTokenUrl: Invalid refresh token.")
|
557
|
+
end
|
558
|
+
url = consenturl + "RefreshToken.aspx?ps=#{CGI.escape(offers)}"
|
559
|
+
url += "&reft=#{refreshtoken}"
|
560
|
+
ru = returnurl if (ru.nil? or ru.empty?)
|
561
|
+
url += "&ru=#{CGI.escape(ru)}" if ru
|
562
|
+
url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
|
563
|
+
url
|
564
|
+
end
|
565
|
+
|
566
|
+
#####################################################################
|
567
|
+
# Returns the URL for the consent-management user interface.
|
568
|
+
# You can change the language in which the consent page is displayed
|
569
|
+
# by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
|
570
|
+
# 'market' parameter.
|
571
|
+
#####################################################################
|
572
|
+
def getManageConsentUrl(market=nil)
|
573
|
+
url = consenturl + "ManageConsent.aspx"
|
574
|
+
url += "?mkt=#{CGI.escape(market)}" if market
|
575
|
+
url
|
576
|
+
end
|
577
|
+
|
578
|
+
class ConsentToken
|
579
|
+
attr_reader :delegationtoken, :refreshtoken, :sessionkey, :expiry
|
580
|
+
attr_reader :offers, :offers_string, :locationid, :context
|
581
|
+
attr_reader :decodedtoken, :token
|
582
|
+
|
583
|
+
#####################################################################
|
584
|
+
# Indicates whether the delegation token is set and has not expired.
|
585
|
+
#####################################################################
|
586
|
+
def isValid?
|
587
|
+
return false unless delegationtoken
|
588
|
+
return ((Time.now.to_i-300) < expiry.to_i)
|
589
|
+
end
|
590
|
+
|
591
|
+
#####################################################################
|
592
|
+
# Refreshes the current token and replace it. If operation succeeds
|
593
|
+
# true is returned to signify success.
|
594
|
+
#####################################################################
|
595
|
+
def refresh
|
596
|
+
ct = @wll.refreshConsentToken(self)
|
597
|
+
return false unless ct
|
598
|
+
copy(ct)
|
599
|
+
true
|
600
|
+
end
|
601
|
+
|
602
|
+
#####################################################################
|
603
|
+
# Initialize the ConsentToken module with the WindowsLiveLogin,
|
604
|
+
# delegation token, refresh token, session key, expiry, offers,
|
605
|
+
# location ID, context, decoded token, and raw token.
|
606
|
+
#####################################################################
|
607
|
+
def initialize(wll, delegationtoken, refreshtoken, sessionkey, expiry,
|
608
|
+
offers, locationid, context, decodedtoken, token)
|
609
|
+
@wll = wll
|
610
|
+
self.delegationtoken = delegationtoken
|
611
|
+
self.refreshtoken = refreshtoken
|
612
|
+
self.sessionkey = sessionkey
|
613
|
+
self.expiry = expiry
|
614
|
+
self.offers = offers
|
615
|
+
self.locationid = locationid
|
616
|
+
self.context = context
|
617
|
+
self.decodedtoken = decodedtoken
|
618
|
+
self.token = token
|
619
|
+
end
|
620
|
+
|
621
|
+
private
|
622
|
+
attr_writer :delegationtoken, :refreshtoken, :sessionkey, :expiry
|
623
|
+
attr_writer :offers, :offers_string, :locationid, :context
|
624
|
+
attr_writer :decodedtoken, :token, :locationid
|
625
|
+
|
626
|
+
#####################################################################
|
627
|
+
# Sets the delegation token.
|
628
|
+
#####################################################################
|
629
|
+
def delegationtoken=(delegationtoken)
|
630
|
+
if (delegationtoken.nil? or delegationtoken.empty?)
|
631
|
+
raise("Error: ConsentToken: Null delegation token.")
|
632
|
+
end
|
633
|
+
@delegationtoken = delegationtoken
|
634
|
+
end
|
635
|
+
|
636
|
+
#####################################################################
|
637
|
+
# Sets the session key.
|
638
|
+
#####################################################################
|
639
|
+
def sessionkey=(sessionkey)
|
640
|
+
if (sessionkey.nil? or sessionkey.empty?)
|
641
|
+
raise("Error: ConsentToken: Null session key.")
|
642
|
+
end
|
643
|
+
@sessionkey = @wll.u64(sessionkey)
|
644
|
+
end
|
645
|
+
|
646
|
+
#####################################################################
|
647
|
+
# Sets the expiry time of the delegation token.
|
648
|
+
#####################################################################
|
649
|
+
def expiry=(expiry)
|
650
|
+
if (expiry.nil? or expiry.empty?)
|
651
|
+
raise("Error: ConsentToken: Null expiry time.")
|
652
|
+
end
|
653
|
+
expiry = expiry.to_i
|
654
|
+
raise("Error: ConsentToken: Invalid expiry: #{expiry}") if (expiry <= 0)
|
655
|
+
@expiry = Time.at expiry
|
656
|
+
end
|
657
|
+
|
658
|
+
#####################################################################
|
659
|
+
# Sets the offers/actions for which the user granted consent.
|
660
|
+
#####################################################################
|
661
|
+
def offers=(offers)
|
662
|
+
if (offers.nil? or offers.empty?)
|
663
|
+
raise("Error: ConsentToken: Null offers.")
|
664
|
+
end
|
665
|
+
|
666
|
+
@offers_string = ""
|
667
|
+
@offers = []
|
668
|
+
|
669
|
+
offers = CGI.unescape(offers)
|
670
|
+
offers = offers.split(";")
|
671
|
+
offers.each{|offer|
|
672
|
+
offer = offer.split(":")[0]
|
673
|
+
@offers_string += "," unless @offers_string.empty?
|
674
|
+
@offers_string += offer
|
675
|
+
@offers.push(offer)
|
676
|
+
}
|
677
|
+
end
|
678
|
+
|
679
|
+
#####################################################################
|
680
|
+
# Sets the LocationID.
|
681
|
+
#####################################################################
|
682
|
+
def locationid=(locationid)
|
683
|
+
if (locationid.nil? or locationid.empty?)
|
684
|
+
raise("Error: ConsentToken: Null Location ID.")
|
685
|
+
end
|
686
|
+
@locationid = locationid
|
687
|
+
end
|
688
|
+
|
689
|
+
#####################################################################
|
690
|
+
# Makes a copy of the ConsentToken object.
|
691
|
+
#####################################################################
|
692
|
+
def copy(consenttoken)
|
693
|
+
@delegationtoken = consenttoken.delegationtoken
|
694
|
+
@refreshtoken = consenttoken.refreshtoken
|
695
|
+
@sessionkey = consenttoken.sessionkey
|
696
|
+
@expiry = consenttoken.expiry
|
697
|
+
@offers = consenttoken.offers
|
698
|
+
@locationid = consenttoken.locationid
|
699
|
+
@offers_string = consenttoken.offers_string
|
700
|
+
@decodedtoken = consenttoken.decodedtoken
|
701
|
+
@token = consenttoken.token
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
#####################################################################
|
706
|
+
# Processes the POST response from the Delegated Authentication
|
707
|
+
# service after a user has granted consent. The processConsent
|
708
|
+
# function extracts the consent token string and returns the result
|
709
|
+
# of invoking the processConsentToken method.
|
710
|
+
#####################################################################
|
711
|
+
def processConsent(query)
|
712
|
+
query = parse query
|
713
|
+
unless query
|
714
|
+
debug("Error: processConsent: Failed to parse query.")
|
715
|
+
return
|
716
|
+
end
|
717
|
+
action = query['action']
|
718
|
+
unless action == 'delauth'
|
719
|
+
debug("Warning: processConsent: query action ignored: #{action}.")
|
720
|
+
return
|
721
|
+
end
|
722
|
+
responsecode = query['ResponseCode']
|
723
|
+
unless responsecode == 'RequestApproved'
|
724
|
+
debug("Error: processConsent: Consent was not successfully granted: #{responsecode}")
|
725
|
+
return
|
726
|
+
end
|
727
|
+
token = query['ConsentToken']
|
728
|
+
context = CGI.unescape(query['appctx']) if query['appctx']
|
729
|
+
processConsentToken(token, context)
|
730
|
+
end
|
731
|
+
|
732
|
+
#####################################################################
|
733
|
+
# Processes the consent token string that is returned in the POST
|
734
|
+
# response by the Delegated Authentication service after a
|
735
|
+
# user has granted consent.
|
736
|
+
#####################################################################
|
737
|
+
def processConsentToken(token, context=nil)
|
738
|
+
if token.nil? or token.empty?
|
739
|
+
debug("Error: processConsentToken: Null token.")
|
740
|
+
return
|
741
|
+
end
|
742
|
+
decodedtoken = token
|
743
|
+
parsedtoken = parse(CGI.unescape(decodedtoken))
|
744
|
+
unless parsedtoken
|
745
|
+
debug("Error: processConsentToken: Failed to parse token: #{token}")
|
746
|
+
return
|
747
|
+
end
|
748
|
+
eact = parsedtoken['eact']
|
749
|
+
if eact
|
750
|
+
decodedtoken = decodeAndValidateToken eact
|
751
|
+
unless decodedtoken
|
752
|
+
debug("Error: processConsentToken: Failed to decode/validate token: #{token}")
|
753
|
+
return
|
754
|
+
end
|
755
|
+
parsedtoken = parse(decodedtoken)
|
756
|
+
decodedtoken = CGI.escape(decodedtoken)
|
757
|
+
end
|
758
|
+
begin
|
759
|
+
consenttoken = ConsentToken.new(self,
|
760
|
+
parsedtoken['delt'],
|
761
|
+
parsedtoken['reft'],
|
762
|
+
parsedtoken['skey'],
|
763
|
+
parsedtoken['exp'],
|
764
|
+
parsedtoken['offer'],
|
765
|
+
parsedtoken['lid'],
|
766
|
+
context, decodedtoken, token)
|
767
|
+
return consenttoken
|
768
|
+
rescue Exception => e
|
769
|
+
debug("Error: processConsentToken: Contents of token considered invalid: #{e}")
|
770
|
+
return
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
#####################################################################
|
775
|
+
# Attempts to obtain a new, refreshed token and return it. The
|
776
|
+
# original token is not modified.
|
777
|
+
#####################################################################
|
778
|
+
def refreshConsentToken(consenttoken, ru=nil)
|
779
|
+
if consenttoken.nil?
|
780
|
+
debug("Error: refreshConsentToken: Null consent token.")
|
781
|
+
return
|
782
|
+
end
|
783
|
+
refreshConsentToken2(consenttoken.offers_string, consenttoken.refreshtoken, ru)
|
784
|
+
end
|
785
|
+
|
786
|
+
#####################################################################
|
787
|
+
# Helper function to obtain a new, refreshed token and return it.
|
788
|
+
# The original token is not modified.
|
789
|
+
#####################################################################
|
790
|
+
def refreshConsentToken2(offers_string, refreshtoken, ru=nil)
|
791
|
+
url = nil
|
792
|
+
begin
|
793
|
+
url = getRefreshConsentTokenUrl(offers_string, refreshtoken, ru)
|
794
|
+
ret = fetch url
|
795
|
+
ret.value # raises exception if fetch failed
|
796
|
+
body = ret.body
|
797
|
+
body.scan(/\{"ConsentToken":"(.*)"\}/){|match|
|
798
|
+
return processConsentToken("#{match}")
|
799
|
+
}
|
800
|
+
debug("Error: refreshConsentToken2: Failed to extract token: #{body}")
|
801
|
+
rescue Exception => e
|
802
|
+
debug("Error: Failed to refresh consent token: #{e}")
|
803
|
+
end
|
804
|
+
return
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
#######################################################################
|
809
|
+
# Common methods.
|
810
|
+
#######################################################################
|
811
|
+
class WindowsLiveLogin
|
812
|
+
|
813
|
+
#####################################################################
|
814
|
+
# Decodes and validates the token.
|
815
|
+
#####################################################################
|
816
|
+
def decodeAndValidateToken(token, cryptkey=@cryptkey, signkey=@signkey,
|
817
|
+
internal_allow_recursion=true)
|
818
|
+
haveoldsecret = false
|
819
|
+
if (oldsecretexpiry and (Time.now.to_i < oldsecretexpiry.to_i))
|
820
|
+
haveoldsecret = true if (@oldcryptkey and @oldsignkey)
|
821
|
+
end
|
822
|
+
haveoldsecret = (haveoldsecret and internal_allow_recursion)
|
823
|
+
|
824
|
+
stoken = decodeToken(token, cryptkey)
|
825
|
+
stoken = validateToken(stoken, signkey) if stoken
|
826
|
+
if (stoken.nil? and haveoldsecret)
|
827
|
+
debug("Warning: Failed to validate token with current secret, attempting old secret.")
|
828
|
+
stoken = decodeAndValidateToken(token, @oldcryptkey, @oldsignkey, false)
|
829
|
+
end
|
830
|
+
stoken
|
831
|
+
end
|
832
|
+
|
833
|
+
#####################################################################
|
834
|
+
# Decodes the given token string; returns undef on failure.
|
835
|
+
#
|
836
|
+
# First, the string is URL-unescaped and base64 decoded.
|
837
|
+
# Second, the IV is extracted from the first 16 bytes of the string.
|
838
|
+
# Finally, the string is decrypted using the encryption key.
|
839
|
+
#####################################################################
|
840
|
+
def decodeToken(token, cryptkey=@cryptkey)
|
841
|
+
if (cryptkey.nil? or cryptkey.empty?)
|
842
|
+
fatal("Error: decodeToken: Secret key was not set. Aborting.")
|
843
|
+
end
|
844
|
+
token = u64(token)
|
845
|
+
if (token.nil? or (token.size <= 16) or !(token.size % 16).zero?)
|
846
|
+
debug("Error: decodeToken: Attempted to decode invalid token.")
|
847
|
+
return
|
848
|
+
end
|
849
|
+
iv = token[0..15]
|
850
|
+
crypted = token[16..-1]
|
851
|
+
begin
|
852
|
+
aes128cbc = OpenSSL::Cipher::AES128.new("CBC")
|
853
|
+
aes128cbc.decrypt
|
854
|
+
aes128cbc.iv = iv
|
855
|
+
aes128cbc.key = cryptkey
|
856
|
+
decrypted = aes128cbc.update(crypted) + aes128cbc.final
|
857
|
+
rescue Exception => e
|
858
|
+
debug("Error: decodeToken: Decryption failed: #{token}, #{e}")
|
859
|
+
return
|
860
|
+
end
|
861
|
+
decrypted
|
862
|
+
end
|
863
|
+
|
864
|
+
#####################################################################
|
865
|
+
# Creates a signature for the given string by using the signature
|
866
|
+
# key.
|
867
|
+
#####################################################################
|
868
|
+
def signToken(token, signkey=@signkey)
|
869
|
+
if (signkey.nil? or signkey.empty?)
|
870
|
+
fatal("Error: signToken: Secret key was not set. Aborting.")
|
871
|
+
end
|
872
|
+
begin
|
873
|
+
digest = OpenSSL::Digest::SHA256.new
|
874
|
+
return OpenSSL::HMAC.digest(digest, signkey, token)
|
875
|
+
rescue Exception => e
|
876
|
+
debug("Error: signToken: Signing failed: #{token}, #{e}")
|
877
|
+
return
|
878
|
+
end
|
879
|
+
end
|
880
|
+
|
881
|
+
#####################################################################
|
882
|
+
# Extracts the signature from the token and validates it.
|
883
|
+
#####################################################################
|
884
|
+
def validateToken(token, signkey=@signkey)
|
885
|
+
if (token.nil? or token.empty?)
|
886
|
+
debug("Error: validateToken: Null token.")
|
887
|
+
return
|
888
|
+
end
|
889
|
+
body, sig = token.split("&sig=")
|
890
|
+
if (body.nil? or sig.nil?)
|
891
|
+
debug("Error: validateToken: Invalid token: #{token}")
|
892
|
+
return
|
893
|
+
end
|
894
|
+
sig = u64(sig)
|
895
|
+
return token if (sig == signToken(body, signkey))
|
896
|
+
debug("Error: validateToken: Signature did not match.")
|
897
|
+
return
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
#######################################################################
|
902
|
+
# Implementation of the methods needed to perform Windows Live
|
903
|
+
# application verification as well as trusted sign-in.
|
904
|
+
#######################################################################
|
905
|
+
class WindowsLiveLogin
|
906
|
+
#####################################################################
|
907
|
+
# Generates an application verifier token. An IP address can
|
908
|
+
# optionally be included in the token.
|
909
|
+
#####################################################################
|
910
|
+
def getAppVerifier(ip=nil)
|
911
|
+
token = "appid=#{appid}&ts=#{timestamp}"
|
912
|
+
token += "&ip=#{ip}" if ip
|
913
|
+
token += "&sig=#{e64(signToken(token))}"
|
914
|
+
CGI.escape token
|
915
|
+
end
|
916
|
+
|
917
|
+
#####################################################################
|
918
|
+
# Returns the URL that is required to retrieve the application
|
919
|
+
# security token.
|
920
|
+
#
|
921
|
+
# By default, the application security token is generated for
|
922
|
+
# the Windows Live site; a specific Site ID can optionally be
|
923
|
+
# specified in 'siteid'. The IP address can also optionally be
|
924
|
+
# included in 'ip'.
|
925
|
+
#
|
926
|
+
# If 'js' is nil, a JavaScript Output Notation (JSON) response is
|
927
|
+
# returned in the following format:
|
928
|
+
#
|
929
|
+
# {"token":"<value>"}
|
930
|
+
#
|
931
|
+
# Otherwise, a JavaScript response is returned. It is assumed that
|
932
|
+
# WLIDResultCallback is a custom function implemented to handle the
|
933
|
+
# token value:
|
934
|
+
#
|
935
|
+
# WLIDResultCallback("<tokenvalue>");
|
936
|
+
#####################################################################
|
937
|
+
def getAppLoginUrl(siteid=nil, ip=nil, js=nil)
|
938
|
+
url = secureurl + "wapplogin.srf?app=#{getAppVerifier(ip)}"
|
939
|
+
url += "&alg=#{securityalgorithm}"
|
940
|
+
url += "&id=#{siteid}" if siteid
|
941
|
+
url += "&js=1" if js
|
942
|
+
url
|
943
|
+
end
|
944
|
+
|
945
|
+
#####################################################################
|
946
|
+
# Retrieves the application security token for application
|
947
|
+
# verification from the application sign-in URL.
|
948
|
+
#
|
949
|
+
# By default, the application security token will be generated for
|
950
|
+
# the Windows Live site; a specific Site ID can optionally be
|
951
|
+
# specified in 'siteid'. The IP address can also optionally be
|
952
|
+
# included in 'ip'.
|
953
|
+
#
|
954
|
+
# Implementation note: The application security token is downloaded
|
955
|
+
# from the application sign-in URL in JSON format:
|
956
|
+
#
|
957
|
+
# {"token":"<value>"}
|
958
|
+
#
|
959
|
+
# Therefore we must extract <value> from the string and return it as
|
960
|
+
# seen here.
|
961
|
+
#####################################################################
|
962
|
+
def getAppSecurityToken(siteid=nil, ip=nil)
|
963
|
+
url = getAppLoginUrl(siteid, ip)
|
964
|
+
begin
|
965
|
+
ret = fetch url
|
966
|
+
ret.value # raises exception if fetch failed
|
967
|
+
body = ret.body
|
968
|
+
body.scan(/\{"token":"(.*)"\}/){|match|
|
969
|
+
return match
|
970
|
+
}
|
971
|
+
debug("Error: getAppSecurityToken: Failed to extract token: #{body}")
|
972
|
+
rescue Exception => e
|
973
|
+
debug("Error: getAppSecurityToken: Failed to get token: #{e}")
|
974
|
+
end
|
975
|
+
return
|
976
|
+
end
|
977
|
+
|
978
|
+
#####################################################################
|
979
|
+
# Returns a string that can be passed to the getTrustedParams
|
980
|
+
# function as the 'retcode' parameter. If this is specified as the
|
981
|
+
# 'retcode', the application will be used as return URL after it
|
982
|
+
# finishes trusted sign-in.
|
983
|
+
#####################################################################
|
984
|
+
def getAppRetCode
|
985
|
+
"appid=#{appid}"
|
986
|
+
end
|
987
|
+
|
988
|
+
#####################################################################
|
989
|
+
# Returns a table of key-value pairs that must be posted to the
|
990
|
+
# sign-in URL for trusted sign-in. Use HTTP POST to do this. Be aware
|
991
|
+
# that the values in the table are neither URL nor HTML escaped and
|
992
|
+
# may have to be escaped if you are inserting them in code such as
|
993
|
+
# an HTML form.
|
994
|
+
#
|
995
|
+
# The user to be trusted on the local site is passed in as string
|
996
|
+
# 'user'.
|
997
|
+
#
|
998
|
+
# Optionally, 'retcode' specifies the resource to which successful
|
999
|
+
# sign-in is redirected, such as Windows Live Mail, and is typically
|
1000
|
+
# a string in the format 'id=2000'. If you pass in the value from
|
1001
|
+
# getAppRetCode instead, sign-in will be redirected to the
|
1002
|
+
# application. Otherwise, an HTTP 200 response is returned.
|
1003
|
+
#####################################################################
|
1004
|
+
def getTrustedParams(user, retcode=nil)
|
1005
|
+
token = getTrustedToken(user)
|
1006
|
+
return unless token
|
1007
|
+
token = %{<wst:RequestSecurityTokenResponse xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust"><wst:RequestedSecurityToken><wsse:BinarySecurityToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">#{token}</wsse:BinarySecurityToken></wst:RequestedSecurityToken><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"><wsa:EndpointReference xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"><wsa:Address>uri:WindowsLiveID</wsa:Address></wsa:EndpointReference></wsp:AppliesTo></wst:RequestSecurityTokenResponse>}
|
1008
|
+
params = {}
|
1009
|
+
params['wa'] = securityalgorithm
|
1010
|
+
params['wresult'] = token
|
1011
|
+
params['wctx'] = retcode if retcode
|
1012
|
+
params
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
#####################################################################
|
1016
|
+
# Returns the trusted sign-in token in the format that is needed by a
|
1017
|
+
# control doing trusted sign-in.
|
1018
|
+
#
|
1019
|
+
# The user to be trusted on the local site is passed in as string
|
1020
|
+
# 'user'.
|
1021
|
+
#####################################################################
|
1022
|
+
def getTrustedToken(user)
|
1023
|
+
if user.nil? or user.empty?
|
1024
|
+
debug('Error: getTrustedToken: Null user specified.')
|
1025
|
+
return
|
1026
|
+
end
|
1027
|
+
token = "appid=#{appid}&uid=#{CGI.escape(user)}&ts=#{timestamp}"
|
1028
|
+
token += "&sig=#{e64(signToken(token))}"
|
1029
|
+
CGI.escape token
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
#####################################################################
|
1033
|
+
# Returns the trusted sign-in URL to use for the Windows Live Login
|
1034
|
+
# server.
|
1035
|
+
#####################################################################
|
1036
|
+
def getTrustedLoginUrl
|
1037
|
+
secureurl + "wlogin.srf"
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
#####################################################################
|
1041
|
+
# Returns the trusted sign-out URL to use for the Windows Live Login
|
1042
|
+
# server.
|
1043
|
+
#####################################################################
|
1044
|
+
def getTrustedLogoutUrl
|
1045
|
+
secureurl + "logout.srf?appid=#{appid}"
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
#######################################################################
|
1050
|
+
# Helper methods.
|
1051
|
+
#######################################################################
|
1052
|
+
class WindowsLiveLogin
|
1053
|
+
|
1054
|
+
#######################################################################
|
1055
|
+
# Function to parse the settings file.
|
1056
|
+
#######################################################################
|
1057
|
+
def parseSettings(settingsFile)
|
1058
|
+
settings = {}
|
1059
|
+
begin
|
1060
|
+
file = File.new(settingsFile)
|
1061
|
+
doc = REXML::Document.new file
|
1062
|
+
root = doc.root
|
1063
|
+
root.each_element{|e|
|
1064
|
+
settings[e.name] = e.text
|
1065
|
+
}
|
1066
|
+
rescue Exception => e
|
1067
|
+
fatal("Error: parseSettings: Error while reading #{settingsFile}: #{e}")
|
1068
|
+
end
|
1069
|
+
return settings
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
#####################################################################
|
1073
|
+
# Derives the key, given the secret key and prefix as described in the
|
1074
|
+
# Web Authentication SDK documentation.
|
1075
|
+
#####################################################################
|
1076
|
+
def derive(secret, prefix)
|
1077
|
+
begin
|
1078
|
+
fatal("Nil/empty secret.") if (secret.nil? or secret.empty?)
|
1079
|
+
key = prefix + secret
|
1080
|
+
key = OpenSSL::Digest::SHA256.digest(key)
|
1081
|
+
return key[0..15]
|
1082
|
+
rescue Exception => e
|
1083
|
+
debug("Error: derive: #{e}")
|
1084
|
+
return
|
1085
|
+
end
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
#####################################################################
|
1089
|
+
# Parses query string and return a table
|
1090
|
+
# {String=>String}
|
1091
|
+
#
|
1092
|
+
# If a table is passed in from CGI.params, we convert it from
|
1093
|
+
# {String=>[]} to {String=>String}. I believe Rails uses symbols
|
1094
|
+
# instead of strings in general, so we convert from symbols to
|
1095
|
+
# strings here also.
|
1096
|
+
#####################################################################
|
1097
|
+
def parse(input)
|
1098
|
+
if (input.nil? or input.empty?)
|
1099
|
+
debug("Error: parse: Nil/empty input.")
|
1100
|
+
return
|
1101
|
+
end
|
1102
|
+
|
1103
|
+
pairs = {}
|
1104
|
+
if (input.class == String)
|
1105
|
+
input = input.split('&')
|
1106
|
+
input.each{|pair|
|
1107
|
+
k, v = pair.split('=')
|
1108
|
+
pairs[k] = v
|
1109
|
+
}
|
1110
|
+
else
|
1111
|
+
input.each{|k, v|
|
1112
|
+
v = v[0] if (v.class == Array)
|
1113
|
+
pairs[k.to_s] = v.to_s
|
1114
|
+
}
|
1115
|
+
end
|
1116
|
+
return pairs
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
#####################################################################
|
1120
|
+
# Generates a time stamp suitable for the application verifier token.
|
1121
|
+
#####################################################################
|
1122
|
+
def timestamp
|
1123
|
+
Time.now.to_i.to_s
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
#####################################################################
|
1127
|
+
# Base64-encodes and URL-escapes a string.
|
1128
|
+
#####################################################################
|
1129
|
+
def e64(s)
|
1130
|
+
return unless s
|
1131
|
+
CGI.escape Base64.encode64(s)
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
#####################################################################
|
1135
|
+
# URL-unescapes and Base64-decodes a string.
|
1136
|
+
#####################################################################
|
1137
|
+
def u64(s)
|
1138
|
+
return unless s
|
1139
|
+
Base64.decode64 CGI.unescape(s)
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
#####################################################################
|
1143
|
+
# Fetches the contents given a URL.
|
1144
|
+
#####################################################################
|
1145
|
+
def fetch(url)
|
1146
|
+
url = URI.parse url
|
1147
|
+
http = Net::HTTP.new(url.host, url.port)
|
1148
|
+
http.use_ssl = (url.scheme == "https")
|
1149
|
+
http.request_get url.request_uri
|
1150
|
+
end
|
1151
|
+
end
|