mislav_contacts 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|