lti2_commons 1.0
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/Gemfile +4 -0
- data/License.pdf +0 -0
- data/README.md +7 -0
- data/Rakefile +1 -0
- data/lib/lti2_commons.rb +10 -0
- data/lib/lti2_commons/cache.rb +32 -0
- data/lib/lti2_commons/json_wrapper.rb +121 -0
- data/lib/lti2_commons/lti2_launch.rb +165 -0
- data/lib/lti2_commons/message_support.rb +197 -0
- data/lib/lti2_commons/oauth_request.rb +145 -0
- data/lib/lti2_commons/signer.rb +86 -0
- data/lib/lti2_commons/substitution_support.rb +90 -0
- data/lib/lti2_commons/utils.rb +33 -0
- data/lib/lti2_commons/version.rb +3 -0
- data/lib/lti2_commons/wire_log.rb +172 -0
- metadata +151 -0
data/Gemfile
ADDED
data/License.pdf
ADDED
Binary file
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/lti2_commons.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
require 'lti2_commons/cache'
|
3
|
+
require 'lti2_commons/json_wrapper'
|
4
|
+
require 'lti2_commons/message_support'
|
5
|
+
require 'lti2_commons/oauth_request'
|
6
|
+
require 'lti2_commons/signer'
|
7
|
+
require 'lti2_commons/substitution_support'
|
8
|
+
require 'lti2_commons/utils'
|
9
|
+
require 'lti2_commons/version'
|
10
|
+
require 'lti2_commons/wire_log'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
require "lrucache"
|
3
|
+
|
4
|
+
module Lti2Commons
|
5
|
+
|
6
|
+
# Cache adapter. This adapter wraps a simple LRUCache gem.
|
7
|
+
# A more scalable and cluster-friendly cache solution such as Redis or Memcache
|
8
|
+
# would probably be suitable in a production environment.
|
9
|
+
# This class is used to document the interface. In this particular case
|
10
|
+
# the interface exactly matches the protocol of the supplied implementation.
|
11
|
+
# Consequently, this adapter is not really required.
|
12
|
+
class Cache
|
13
|
+
# create cache.
|
14
|
+
# @params options [Hash] Should include :ttl => <expiry_time>
|
15
|
+
def initialize(options)
|
16
|
+
@cache = LRUCache.new options
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear
|
20
|
+
@cache.clear
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch(name)
|
24
|
+
@cache.fetch(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def store(name, value)
|
28
|
+
@cache.store(name, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'json'
|
4
|
+
require 'jsonpath'
|
5
|
+
|
6
|
+
module Lti2Commons
|
7
|
+
class JsonWrapper
|
8
|
+
attr_accessor :root
|
9
|
+
|
10
|
+
def initialize(json_str_or_obj)
|
11
|
+
@root = JsonPath.new('$').on(json_str_or_obj).first
|
12
|
+
end
|
13
|
+
|
14
|
+
def at(path)
|
15
|
+
JsonPath.new(path).on(@root)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Deep copy through reserialization. Only use to preserve immutability of source.
|
19
|
+
#
|
20
|
+
# @return [JsonObject] A full new version of self
|
21
|
+
def deep_copy
|
22
|
+
JsonWrapper.new(@root.to_json)
|
23
|
+
end
|
24
|
+
|
25
|
+
def each_leaf &block
|
26
|
+
JsonPath.new("$..*").on(@root).each { |node|
|
27
|
+
if node.is_a? String
|
28
|
+
yield node
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def first_at(path)
|
34
|
+
at(path).first
|
35
|
+
end
|
36
|
+
|
37
|
+
# Does this node, possibly repeating, match all elements of the constraint_hash
|
38
|
+
#
|
39
|
+
# @params candidate [Hash/Array] node or Array of nodes to match
|
40
|
+
# @params constraint_hash [Hash] Hash of value to match
|
41
|
+
# @return [TreeNode] Either self or immediate child that matches constraint_hash
|
42
|
+
def get_matching_node candidate, constraint_hash
|
43
|
+
if candidate.is_a? Hash
|
44
|
+
return candidate if is_hash_intersect(candidate, constraint_hash)
|
45
|
+
elsif candidate.is_a? Array
|
46
|
+
for child in candidate
|
47
|
+
# are there extraneous keys in constraint_hash
|
48
|
+
return child if is_hash_intersect(child, constraint_hash)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Convenience method to find a particular node with matching attributes to constraint_hash
|
55
|
+
# and then return specific element of that node.
|
56
|
+
# e.g., search for 'path' within the 'resource_handler' with constraint_hash attributes.
|
57
|
+
#
|
58
|
+
# @params path [JsonPath] path to parent of candidate array of nodes
|
59
|
+
# @params constraint_hash [Hash] Hash of values which must match target
|
60
|
+
# @params return_path [JsonPath] Path of element within matching target
|
61
|
+
# @return [Object] result_path within matching node or nil
|
62
|
+
def search(path, constraint_hash, return_path)
|
63
|
+
candidate = JsonPath.new(path).on(@root)
|
64
|
+
candidate = get_matching_node candidate.first, constraint_hash
|
65
|
+
return nil unless candidate
|
66
|
+
JsonPath.new(return_path).on(candidate).first
|
67
|
+
end
|
68
|
+
|
69
|
+
# Convenience method to find a particular node with matching attributes to constraint_hash
|
70
|
+
# and then return specific element of that node.
|
71
|
+
# e.g., search for 'path' within the 'resource_handler' with constraint_hash attributes.
|
72
|
+
#
|
73
|
+
# @params path [JsonPath] path to parent of candidate array of nodes
|
74
|
+
# @params constraint_hash [Hash] Hash of values which must match target
|
75
|
+
# @params return_path [JsonPath] Path of element within matching target
|
76
|
+
# @return [Object] result_path within matching node or nil
|
77
|
+
def select(path, selector, value, return_path)
|
78
|
+
candidates = JsonPath.new(path).on(@root)
|
79
|
+
for candidate in candidates
|
80
|
+
selector_node = JsonPath.new(selector).on(candidate.first)
|
81
|
+
if selector_node.include? value
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
85
|
+
if candidate
|
86
|
+
JsonPath.new(return_path).on(candidate.first).first
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def substitute_text_in_all_nodes(token_prefix, token_suffix, hash)
|
93
|
+
self.each_leaf { |v|
|
94
|
+
substitute_template_values_from_hash v, token_prefix, token_suffix, hash }
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_pretty_json
|
98
|
+
JSON.pretty_generate root
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def is_hash_intersect target_hash, constraint_hash
|
104
|
+
# failed search if constraint_hash as invalid keys
|
105
|
+
return nil if (constraint_hash.keys - target_hash.keys).length > 0
|
106
|
+
target_hash.each_pair {|k,v|
|
107
|
+
if constraint_hash.has_key? k
|
108
|
+
return false unless v == constraint_hash[k]
|
109
|
+
end
|
110
|
+
}
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def substitute_template_values_from_hash(source_string, prefix, suffix, hash)
|
115
|
+
hash.each { |k,v|
|
116
|
+
source_string.sub!(prefix+k+suffix, v) }
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
|
2
|
+
require "rubygems"
|
3
|
+
|
4
|
+
require 'lti2_commons/message_support'
|
5
|
+
require 'lti2_commons/signer'
|
6
|
+
require 'lti2_commons/substitution_support'
|
7
|
+
|
8
|
+
include Lti2Commons::Signer
|
9
|
+
include Lti2Commons::MessageSupport
|
10
|
+
include Lti2Commons::SubstitutionSupport
|
11
|
+
|
12
|
+
module Lti2Commons
|
13
|
+
|
14
|
+
module Core
|
15
|
+
def lti2_launch user, link, return_url
|
16
|
+
tool_proxy = link.resource.tool.get_tool_proxy
|
17
|
+
|
18
|
+
tool = link.resource.tool
|
19
|
+
tool_name = tool_proxy.first_at('tool_profile.product_instance.product_info.product_name.default_value')
|
20
|
+
raise "Tool #{tool_name} is currently disabled" unless tool.is_enabled
|
21
|
+
|
22
|
+
base_url = tool_proxy.select('tool_profile.base_url_choice',
|
23
|
+
"selector.applies_to", "MessageHandler", 'default_base_url')
|
24
|
+
resource_type = link.resource.resource_type
|
25
|
+
resource_handler_node = tool_proxy.search("tool_profile.resource_handler", {'resource_type' => resource_type}, "@")
|
26
|
+
resource_handler = JsonWrapper.new resource_handler_node
|
27
|
+
message = resource_handler.search("@..message", {'message_type' => 'basic-lti-launch-request'}, '@')
|
28
|
+
path = message['path']
|
29
|
+
tp_parameters = message['parameter']
|
30
|
+
service_endpoint = base_url + path
|
31
|
+
|
32
|
+
enrollment = Enrollment.where(:admin_user_id => user.id, :course_id => link.course.id).first
|
33
|
+
if enrollment
|
34
|
+
role = enrollment.role
|
35
|
+
else
|
36
|
+
role = user.role
|
37
|
+
end
|
38
|
+
|
39
|
+
tool_consumer_registry = Rails.application.config.tool_consumer_registry
|
40
|
+
parameters = {
|
41
|
+
'lti_version' => tool_proxy.first_at('lti_version'),
|
42
|
+
'lti_message_type' => 'basic-lti-launch-request',
|
43
|
+
'resource_link_id' => link.id.to_s,
|
44
|
+
'user_id' => user.id.to_s,
|
45
|
+
'roles' => role,
|
46
|
+
'launch_presentation_return_url' => "#{tool_consumer_registry.tc_deployment_url}#{return_url}",
|
47
|
+
|
48
|
+
# optional
|
49
|
+
'context_id' => link.course.id.to_s,
|
50
|
+
}
|
51
|
+
|
52
|
+
# add parameters from ToolProxy
|
53
|
+
for parameter in tp_parameters
|
54
|
+
name = parameter['name']
|
55
|
+
if parameter.has_key? 'fixed'
|
56
|
+
value = parameter['fixed']
|
57
|
+
elsif parameter.has_key? 'variable'
|
58
|
+
value = parameter['variable']
|
59
|
+
else
|
60
|
+
value = "[link-to-resolve]"
|
61
|
+
end
|
62
|
+
parameters[name] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
# add parameters from Link
|
66
|
+
link_parameter_json = link.link_parameters
|
67
|
+
if link_parameter_json
|
68
|
+
link_parameters = JSON.parse link_parameter_json
|
69
|
+
parameters.merge! link_parameters
|
70
|
+
end
|
71
|
+
|
72
|
+
# auto-create result (if required) and add to reference to parameters
|
73
|
+
capabilities = resource_handler.first_at('message[0].enabled_capability')
|
74
|
+
if capabilities and capabilities.include? "Result.autocreate"
|
75
|
+
if link.grade_item_id
|
76
|
+
grade_result = GradeResult.where(:link_id => link.id, :admin_user_id => user.id).first
|
77
|
+
# TEST ONLY
|
78
|
+
# grade_result.delete if grade_result
|
79
|
+
# grade_result = nil
|
80
|
+
#
|
81
|
+
|
82
|
+
unless grade_result
|
83
|
+
grade_result = GradeResult.new
|
84
|
+
grade_result.link_id = link.id
|
85
|
+
grade_result.admin_user_id = user.id
|
86
|
+
grade_result.save
|
87
|
+
else
|
88
|
+
# note that a nil grade_result still let's us through
|
89
|
+
unless grade_result.result.nil?
|
90
|
+
raise "Grade result associated with this tool launch has already been filled in"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# perform variable substition on all parameters
|
97
|
+
resolver = Resolver.new
|
98
|
+
resolver.add_resolver("$User", user.method(:user_resolver))
|
99
|
+
resolver.add_resolver("$Person", user.method(:person_resolver))
|
100
|
+
course = Course.find(parameters['context_id'])
|
101
|
+
resolver.add_resolver("$CourseOffering", course.method(:course_resolver))
|
102
|
+
resolver.add_resolver("$Result", grade_result.method(:grade_result_resolver)) if grade_result
|
103
|
+
|
104
|
+
# and resolve and normalize
|
105
|
+
final_parameters = {}
|
106
|
+
parameters.each { |k,v|
|
107
|
+
# only apply substitution when the value looks like a candidate
|
108
|
+
if v =~ /^\$\w+\.\w/
|
109
|
+
resolved_value = resolver.resolve(v)
|
110
|
+
else
|
111
|
+
resolved_value = v
|
112
|
+
end
|
113
|
+
if known_lti2_parameters.include? k or deprecated_lti_parameters.include? k
|
114
|
+
final_parameters[k] = v
|
115
|
+
else
|
116
|
+
name = 'custom_' + k
|
117
|
+
lti1_name = slugify(name)
|
118
|
+
unless name == lti1_name
|
119
|
+
final_parameters[lti1_name] = resolved_value
|
120
|
+
end
|
121
|
+
final_parameters[name] = resolved_value
|
122
|
+
end
|
123
|
+
|
124
|
+
}
|
125
|
+
|
126
|
+
key = link.resource.tool.key
|
127
|
+
secret = link.resource.tool.secret
|
128
|
+
|
129
|
+
signed_request = Signer::create_signed_request service_endpoint, 'post', key, secret, final_parameters
|
130
|
+
puts "TC Signed Request: #{signed_request.signature_base_string}"
|
131
|
+
puts "before"
|
132
|
+
puts Rails.application.config.wire_log.inspect
|
133
|
+
puts "after"
|
134
|
+
body = MessageSupport::create_lti_message_body(service_endpoint, final_parameters, Rails.application.config.wire_log, "Lti Launch")
|
135
|
+
puts body
|
136
|
+
body
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def deprecated_lti_parameters
|
142
|
+
["context_title", "context_label", "resource_link_title", "resource_link_description",
|
143
|
+
"lis_person_name_given", "lis_person_name_family", "lis_person_name_full", "lis_person_contact_email_primary",
|
144
|
+
"user_image", "lis_person_sourcedid", "lis_course_offering_sourcedid", "lis_course_section_sourcedid",
|
145
|
+
"tool_consumer_instance_name", "tool_consumer_instance_description", "tool_consumer_instance_url", "tool_consumer_instance_contact_email"]
|
146
|
+
end
|
147
|
+
|
148
|
+
def known_lti2_parameters
|
149
|
+
["lti_message_type", "lti_version", "user_id", "roles", "resource_link_id",
|
150
|
+
"context_id", "context_type",
|
151
|
+
"launch_presentation_locale", "launch_presentation_document_target", "launch_presentation_css_url",
|
152
|
+
"launch_presentation_width", "launch_presentation_height", "launch_presentation_return_url",
|
153
|
+
"reg_key", "reg_password", "tc_profile_url"]
|
154
|
+
end
|
155
|
+
|
156
|
+
def slugify name
|
157
|
+
result = ""
|
158
|
+
name.each_char { |c|
|
159
|
+
result << (c =~ /\w/ ? c.downcase : '_')
|
160
|
+
}
|
161
|
+
result
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
|
2
|
+
require "rubygems"
|
3
|
+
require "httparty"
|
4
|
+
|
5
|
+
module Lti2Commons
|
6
|
+
|
7
|
+
# Module to support LTI 1 and 2 secure messaging.
|
8
|
+
# This messaging is documented in the LTI 2 Security Document
|
9
|
+
#
|
10
|
+
# LTI 2 defines two types of LTI secure messaging:
|
11
|
+
# 1. LTI Messages
|
12
|
+
# This is the model used exclusively in LTI 1.
|
13
|
+
# It is also used in LTI 2 for user-submitted actions such as LtiLaunch and ToolDeployment.
|
14
|
+
#
|
15
|
+
# It works the following way:
|
16
|
+
# 1. LTI parameters are signed by OAuth.
|
17
|
+
# 2. The message is marshalled into an HTML Form with the params
|
18
|
+
# specified in form fields.
|
19
|
+
# 3. The form is sent to the browser with a redirect.
|
20
|
+
# 4. An attached Javascript script 'auto-submits' the form.
|
21
|
+
#
|
22
|
+
# This structure appears to the Tool Provider as a user submission with all user browser context
|
23
|
+
# intact.
|
24
|
+
#
|
25
|
+
# 2. LTI Services
|
26
|
+
# This is a standard REST web service with OAuth message security added.
|
27
|
+
# In LTI 2.0 Services are only defined for Tool Provider --> Tool Consumer services;
|
28
|
+
# such as, GetToolConsumerProfile, RegisterToolProxy, and LTI 2 Outcomes.
|
29
|
+
# LTI 2.x will add Tool Consumer --> Tool Provider services using the same machinery.
|
30
|
+
#
|
31
|
+
module MessageSupport
|
32
|
+
|
33
|
+
# Convenience method signs and then invokes create_lti_message_from_signed_request
|
34
|
+
#
|
35
|
+
# @params launch_url [String] Launch url
|
36
|
+
# @params parameters [Hash] Full set of params for message
|
37
|
+
# @return [String] Post body ready for use
|
38
|
+
def create_lti_message_body(launch_url, parameters, wire_log=nil, title=nil)
|
39
|
+
result = create_message_header(launch_url)
|
40
|
+
result += create_message_body parameters
|
41
|
+
result += create_message_footer
|
42
|
+
|
43
|
+
if wire_log
|
44
|
+
wire_log.timestamp
|
45
|
+
wire_log.raw_log((title.nil?) ? "LtiMessage" : "LtiMessage: #{title}")
|
46
|
+
wire_log.raw_log "LaunchUrl: #{launch_url}"
|
47
|
+
wire_log.raw_log result.strip
|
48
|
+
wire_log.newline
|
49
|
+
wire_log.flush
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates an LTI Message (POST body) ready for redirecting to the launch_url.
|
55
|
+
# Note that the is_include_oauth_params option specifies that 'oauth_' params are
|
56
|
+
# included in the body. This option should be false when sender is putting them in the
|
57
|
+
# HTTP Authorization header (now recommended).
|
58
|
+
#
|
59
|
+
# @param params [Hash] Full set of params for message (including OAuth provided params)
|
60
|
+
# @return [String] Post body ready for use
|
61
|
+
def create_lti_message_body_from_signed_request(signed_request, is_include_oauth_params=true)
|
62
|
+
result = create_message_header(signed_request.uri)
|
63
|
+
result += create_message_body signed_request.parameters, is_include_oauth_params
|
64
|
+
result += create_message_footer
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
# Invokes an LTI Service.
|
69
|
+
# This is fully-compliant REST request suitable for LTI server-to-server services.
|
70
|
+
#
|
71
|
+
# @param request [Request] Signed Request encapsulates everything needed for service.
|
72
|
+
def invoke_service(request, wire_log=nil, title=nil)
|
73
|
+
uri = request.uri.to_s
|
74
|
+
method = request.method.downcase
|
75
|
+
headers = {}
|
76
|
+
headers['Authorization'] = request.oauth_header
|
77
|
+
if ['post','put'].include? method
|
78
|
+
headers['Content-Type'] = request.content_type if request.content_type
|
79
|
+
headers['Content-Length'] = request.body.length.to_s if request.body
|
80
|
+
end
|
81
|
+
|
82
|
+
parameters = request.parameters
|
83
|
+
# need filtered params here
|
84
|
+
|
85
|
+
output_parameters = {}
|
86
|
+
(write_wirelog_header wire_log, title, request.method, uri, headers, request.parameters, request.body, output_parameters) if wire_log
|
87
|
+
|
88
|
+
full_uri = uri
|
89
|
+
full_uri += '?' unless uri.include? "?"
|
90
|
+
full_uri += '&' unless full_uri =~ /[?&]$/
|
91
|
+
output_parameters.each_pair do |key, value|
|
92
|
+
full_uri << '&' unless key == output_parameters.keys.first
|
93
|
+
full_uri << "#{URI.encode(key.to_s)}=#{URI.encode(output_parameters[key])}"
|
94
|
+
end
|
95
|
+
|
96
|
+
case request.method.downcase
|
97
|
+
when "get"
|
98
|
+
response = HTTParty.get full_uri, :headers => headers
|
99
|
+
when "post"
|
100
|
+
response = HTTParty.post full_uri, :body => request.body, :headers => headers
|
101
|
+
when "put"
|
102
|
+
response = HTTParty.put full_uri, :body => request.body, :headers => headers
|
103
|
+
when "delete"
|
104
|
+
response = HTTParty.delete full_uri, :headers => headers
|
105
|
+
end
|
106
|
+
wire_log.log_response response, title if wire_log
|
107
|
+
response
|
108
|
+
end
|
109
|
+
|
110
|
+
def invoke_unsigned_service uri, method, params={}, headers={}, data=nil, wire_log=nil, title=nil
|
111
|
+
full_uri = uri
|
112
|
+
full_uri += '?' unless uri.include? "?"
|
113
|
+
full_uri += '&' unless full_uri =~ /[?&]$/
|
114
|
+
params.each_pair do |key, value|
|
115
|
+
full_uri << '&' unless key == params.keys.first
|
116
|
+
full_uri << "#{URI.encode(key.to_s)}=#{URI.encode(params[key])}"
|
117
|
+
end
|
118
|
+
|
119
|
+
(write_wirelog_header wire_log, title, method, uri, headers, params, data, {}) if wire_log
|
120
|
+
|
121
|
+
case method
|
122
|
+
when "get"
|
123
|
+
response = HTTParty.get full_uri, :headers => headers, :timeout => 120
|
124
|
+
when "post"
|
125
|
+
response = HTTParty.post full_uri, :body => data, :headers => headers, :timeout => 120
|
126
|
+
when "put"
|
127
|
+
response = HTTParty.put full_uri, :body => data, :headers => headers, :timeout => 120
|
128
|
+
when "delete"
|
129
|
+
response = HTTParty.delete full_uri, :headers => headers, :timeout => 120
|
130
|
+
end
|
131
|
+
wire_log.log_response response, title if wire_log
|
132
|
+
response
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
|
138
|
+
def create_message_header(launch_url)
|
139
|
+
%Q{
|
140
|
+
<div id="ltiLaunchFormSubmitArea">
|
141
|
+
<form action="#{launch_url}"
|
142
|
+
name="ltiLaunchForm" id="ltiLaunchForm" method="post"
|
143
|
+
encType="application/x-www-form-urlencoded"
|
144
|
+
target="_blank">
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def create_message_body(params, is_include_oauth_params=true)
|
149
|
+
result = ""
|
150
|
+
params.each_pair do |k,v|
|
151
|
+
if is_include_oauth_params || ! (k =~ /^oauth_/)
|
152
|
+
result +=
|
153
|
+
%Q{ <input type="hidden" name="#{k}" value="#{v}"/>\n}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
result
|
157
|
+
end
|
158
|
+
|
159
|
+
def create_message_footer
|
160
|
+
%Q{ </form>
|
161
|
+
</div>
|
162
|
+
<script language="javascript">
|
163
|
+
document.ltiLaunchForm.submit();
|
164
|
+
</script>
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_http_headers(http, request)
|
169
|
+
http.headers['Authorization'] = request.oauth_header
|
170
|
+
http.headers['Content-Type'] = request.content_type if request.content_type
|
171
|
+
http.headers['Content-Length'] = request.body.length if request.body
|
172
|
+
end
|
173
|
+
|
174
|
+
def write_wirelog_header wire_log, title, method, uri, headers={}, parameters={}, body=nil, output_parameters={}
|
175
|
+
wire_log.timestamp
|
176
|
+
wire_log.raw_log((title.nil?) ? "LtiService" : "LtiService: #{title}")
|
177
|
+
wire_log.raw_log "#{method.upcase} #{uri}"
|
178
|
+
unless headers.blank?
|
179
|
+
wire_log.raw_log "Headers:"
|
180
|
+
headers.each { |k,v| wire_log.raw_log "#{k}: #{v}" }
|
181
|
+
end
|
182
|
+
parameters.each { |k,v| output_parameters[k] = v unless k =~ /^oauth_/ }
|
183
|
+
|
184
|
+
if output_parameters.length > 0
|
185
|
+
wire_log.raw_log "Parameters:"
|
186
|
+
output_parameters.each { |k,v| wire_log.raw_log "#{k}: #{v}" }
|
187
|
+
end
|
188
|
+
if body
|
189
|
+
wire_log.raw_log "Body:"
|
190
|
+
wire_log.raw_log body
|
191
|
+
end
|
192
|
+
wire_log.newline
|
193
|
+
wire_log.flush
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'oauth/request_proxy/base'
|
2
|
+
|
3
|
+
module OAuth
|
4
|
+
module OAuthProxy
|
5
|
+
# RequestProxy for Hashes to facilitate simpler signature creation.
|
6
|
+
# Usage:
|
7
|
+
# request = OAuth::RequestProxy.proxy \
|
8
|
+
# "method" => "iq",
|
9
|
+
# "uri" => [from, to] * "&",
|
10
|
+
# "parameters" => {
|
11
|
+
# "oauth_consumer_key" => oauth_consumer_key,
|
12
|
+
# "oauth_token" => oauth_token,
|
13
|
+
# "oauth_signature_method" => "HMAC-SHA1"
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
# signature = OAuth::Signature.sign \
|
17
|
+
# request,
|
18
|
+
# :consumer_secret => oauth_consumer_secret,
|
19
|
+
# :token_secret => oauth_token_secret,
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
class OAuthRequest < OAuth::RequestProxy::Base
|
24
|
+
proxies Hash
|
25
|
+
|
26
|
+
attr_accessor :body, :content_type
|
27
|
+
|
28
|
+
def self.collect_rack_parameters(rack_request)
|
29
|
+
parameters = HashWithIndifferentAccess.new
|
30
|
+
parameters.merge!(rack_request.query_parameters)
|
31
|
+
parameters.merge!(self.parse_authorization_header(rack_request.headers['HTTP_AUTHORIZATION']))
|
32
|
+
content_type = rack_request.headers['CONTENT_TYPE']
|
33
|
+
if content_type == "application/x-www-form-urlencoded"
|
34
|
+
parameters.merge!(rack_request.request_parameters)
|
35
|
+
end
|
36
|
+
parameters
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.create_from_rack_request(rack_request)
|
40
|
+
parameters = self.collect_rack_parameters rack_request
|
41
|
+
result = OAuth::OAuthProxy::OAuthRequest.new \
|
42
|
+
"method" => rack_request.method,
|
43
|
+
"uri" => rack_request.url,
|
44
|
+
"parameters" => parameters
|
45
|
+
rack_request.body.rewind
|
46
|
+
result.body = rack_request.body.read
|
47
|
+
rack_request.body.rewind
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.parse_authorization_header(authorization_header)
|
52
|
+
result = {}
|
53
|
+
if authorization_header =~ /^OAuth/
|
54
|
+
authorization_header[6..-1].split(',').inject({}) do |h,part|
|
55
|
+
parts = part.split('=')
|
56
|
+
name = parts[0].strip.intern
|
57
|
+
value = parts[1..-1].join('=').strip
|
58
|
+
value.gsub!(/\A['"]+|['"]+\Z/, "")
|
59
|
+
result[name] = Rack::Utils.unescape(value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def parameters
|
66
|
+
@request["parameters"]
|
67
|
+
end
|
68
|
+
|
69
|
+
def method
|
70
|
+
@request["method"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def normalized_uri
|
74
|
+
super
|
75
|
+
rescue
|
76
|
+
# if this is a non-standard URI, it may not parse properly
|
77
|
+
# in that case, assume that it's already been normalized
|
78
|
+
uri
|
79
|
+
end
|
80
|
+
|
81
|
+
def uri
|
82
|
+
@request["uri"]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Creates the value of an OAuth body hash
|
86
|
+
#
|
87
|
+
# @param launch_url [String] Content to be body signed
|
88
|
+
# @return [String] Signature base string (useful for debugging signature problems)
|
89
|
+
def compute_oauth_body_hash content
|
90
|
+
Base64.encode64(Digest::SHA1.digest content.chomp).gsub(/\n/,'')
|
91
|
+
end
|
92
|
+
|
93
|
+
# A shallow+1 copy
|
94
|
+
def copy
|
95
|
+
result = OAuth::OAuthProxy::OAuthRequest.new \
|
96
|
+
"method" => self.method.dup,
|
97
|
+
"uri" => self.uri.dup,
|
98
|
+
"parameters" => self.parameters.dup
|
99
|
+
result.body = self.body.dup if self.body
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
def is_timestamp_expired?(timestampString)
|
104
|
+
timestamp = Time.at(timestampString.to_i)
|
105
|
+
now = Time::now
|
106
|
+
(now - timestamp).abs > 300.seconds
|
107
|
+
end
|
108
|
+
|
109
|
+
# Validates and OAuth request using the OAuth Gem - https://github.com/oauth/oauth-ruby
|
110
|
+
#
|
111
|
+
# @return [Bool] Whether the request was valid
|
112
|
+
def verify_signature?(secret, nonce_cache, is_handle_error_not_raise_exception=true, ignore_timestamp_and_nonce=false)
|
113
|
+
test_request = self.copy
|
114
|
+
test_signature = test_request.sign :consumer_secret => secret
|
115
|
+
begin
|
116
|
+
unless self.oauth_signature == test_signature
|
117
|
+
# puts "Secret: #{secret}"
|
118
|
+
puts "Verify_signature--send_signature: #{self.oauth_signature} test_signature: #{test_signature}"
|
119
|
+
puts "Verify signature_base_string: #{self.signature_base_string}"
|
120
|
+
raise 'Invalid signature'
|
121
|
+
end
|
122
|
+
unless ignore_timestamp_and_nonce
|
123
|
+
raise 'Timestamp expired' if is_timestamp_expired? self.oauth_timestamp
|
124
|
+
raise 'Duplicate nonce to one already received' if nonce_cache.fetch self.oauth_nonce
|
125
|
+
end
|
126
|
+
nonce_cache.store self.oauth_nonce, "<who-cares>"
|
127
|
+
|
128
|
+
# check body-signing if oaut_body_signature
|
129
|
+
if self.body and self.parameters.has_key? 'oauth_body_hash'
|
130
|
+
raise 'Invalid signature of message body' unless compute_oauth_body_hash(self.body) == self.parameters['oauth_body_hash']
|
131
|
+
end
|
132
|
+
true
|
133
|
+
rescue Exception => e
|
134
|
+
# Utils::log(e.message)
|
135
|
+
if is_handle_error_not_raise_exception
|
136
|
+
false
|
137
|
+
else
|
138
|
+
raise e.message
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
|
2
|
+
require "rubygems"
|
3
|
+
require "uri"
|
4
|
+
require "oauth"
|
5
|
+
require File.expand_path('../../../lib/lti2_commons/oauth_request', __FILE__)
|
6
|
+
|
7
|
+
class Symbol
|
8
|
+
# monkey patch needed for OAuth library running in Ruby 1.8.7
|
9
|
+
def downcase
|
10
|
+
self.to_s.downcase.to_sym
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Lti2Commons
|
15
|
+
|
16
|
+
module Signer
|
17
|
+
|
18
|
+
# Creates an OAuth signed request using the OAuth Gem - https://github.com/oauth/oauth-ruby
|
19
|
+
#
|
20
|
+
# @param launch_url [String] Endpoint of service to be launched
|
21
|
+
# @param http_method [String] Http method ('get', 'post', 'put', 'delete')
|
22
|
+
# @param consumer_key [String] OAuth consumer key
|
23
|
+
# @param consumer_secret [String] OAuth consumer secret
|
24
|
+
# @param params [Hash] Non-auth parameters or oauth parameter default values
|
25
|
+
# oauth_timestamp => defaults to current time
|
26
|
+
# oauth_nonce => defaults to random number
|
27
|
+
# oauth_signature_method => defaults to HMAC-SHA1 (also RSA-SHA1 supported)
|
28
|
+
# @param body [String] Body content. Usually would include this for body-signing of non form-encoded data.
|
29
|
+
# @param content_type [String] HTTP CONTENT-TYPE header; defaults: 'application/x-www-form-urlencoded'
|
30
|
+
# @return [Request] Signed request
|
31
|
+
def create_signed_request(launch_url, http_method, consumer_key, consumer_secret, params={},
|
32
|
+
body=nil, content_type=nil)
|
33
|
+
params['oauth_consumer_key'] = consumer_key
|
34
|
+
params['oauth_nonce'] = (rand*10E12).to_i.to_s unless params.has_key? 'oauth_nonce'
|
35
|
+
params['oauth_signature_method'] = "HMAC-SHA1" unless params.has_key? 'oauth_signature_method'
|
36
|
+
params['oauth_timestamp'] = Time.now.to_i.to_s unless params.has_key? 'oauth_timestamp'
|
37
|
+
params['oauth_version'] = '1.0' unless params.has_key? 'oauth_version'
|
38
|
+
|
39
|
+
content_type = "application/x-www-form-urlencoded" unless content_type
|
40
|
+
|
41
|
+
uri = URI.parse(launch_url)
|
42
|
+
|
43
|
+
# prepare path
|
44
|
+
path = uri.path
|
45
|
+
path = '/' if path.empty?
|
46
|
+
|
47
|
+
# flatten in query string arrays
|
48
|
+
if uri.query && uri.query != ''
|
49
|
+
CGI.parse(uri.query).each do |query_key, query_values|
|
50
|
+
unless params[query_key]
|
51
|
+
params[query_key] = query_values.first
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
unless content_type == 'application/x-www-form-urlencoded'
|
57
|
+
params['oauth_body_hash'] = compute_oauth_body_hash body if body
|
58
|
+
end
|
59
|
+
|
60
|
+
request = OAuth::OAuthProxy::OAuthRequest.new \
|
61
|
+
"method" => http_method.to_s.upcase,
|
62
|
+
"uri" => uri,
|
63
|
+
"parameters" => params
|
64
|
+
|
65
|
+
request.body = body
|
66
|
+
request.content_type = content_type
|
67
|
+
|
68
|
+
request.sign! :consumer_secret => consumer_secret
|
69
|
+
|
70
|
+
# puts "Sender secret: #{consumer_secret}"
|
71
|
+
request
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Creates the value of an OAuth body hash
|
77
|
+
#
|
78
|
+
# @param launch_url [String] Content to be body signed
|
79
|
+
# @return [String] Signature base string (useful for debugging signature problems)
|
80
|
+
def compute_oauth_body_hash content
|
81
|
+
Base64.encode64(Digest::SHA1.digest content.chomp).gsub(/\n/,'')
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Lti2Commons
|
2
|
+
module SubstitutionSupport
|
3
|
+
|
4
|
+
# Resolver resolves values by name from a variety of object sources.
|
5
|
+
# It's useful for variable substitution.
|
6
|
+
#
|
7
|
+
# The supported object sources are:
|
8
|
+
# Hash ::= value by key
|
9
|
+
# Proc ::= single-argument block evaluation
|
10
|
+
# Method ::= single-argument method obect evaluation
|
11
|
+
# Resolver ::= a nested resolver. useful for scoping resolvers;
|
12
|
+
# i.e. a constant, global inner resolver, but a one-time outer-resolver
|
13
|
+
# Object ::= value by dynamic instrospection of any object's accessor
|
14
|
+
#
|
15
|
+
# See the accompanying tester for numerous examples
|
16
|
+
class Resolver
|
17
|
+
|
18
|
+
attr_accessor :resolvers
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@resolver_hash = Hash.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add some type of resolver object_source to the Resolver
|
25
|
+
#
|
26
|
+
# @param key [String] A dotted name with the leading zone indicating the object category
|
27
|
+
# @param resolver [ObjectSource] a raw object_source for resolving
|
28
|
+
# @returns [String] value. If no resolution, return the incoming name
|
29
|
+
def add_resolver key, resolver
|
30
|
+
unless resolver.is_a? Resolver
|
31
|
+
key_sym = key.to_sym
|
32
|
+
else
|
33
|
+
# Resolvers themselves should always be generic
|
34
|
+
key_sym = :*
|
35
|
+
end
|
36
|
+
unless @resolver_hash.has_key? key_sym
|
37
|
+
@resolver_hash[key_sym] = []
|
38
|
+
end
|
39
|
+
@resolver_hash[key_sym] << resolver
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve(full_name)
|
43
|
+
zones = full_name.split('.')
|
44
|
+
category = zones[0].to_sym
|
45
|
+
name = zones[1..-1].join('.')
|
46
|
+
|
47
|
+
# Find any hits within category
|
48
|
+
@resolver_hash.each_pair {|k, v|
|
49
|
+
if k == category
|
50
|
+
result = resolve_by_category full_name, name, v
|
51
|
+
return result if result
|
52
|
+
end
|
53
|
+
}
|
54
|
+
|
55
|
+
# Find any hits in global category
|
56
|
+
resolvers = @resolver_hash[:*]
|
57
|
+
result = resolve_by_category full_name, name, resolvers if resolvers
|
58
|
+
return result if result
|
59
|
+
|
60
|
+
return full_name
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
"Resolver for [#{@resolver_hash.keys}]"
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def resolve_by_category(full_name, name, resolvers)
|
70
|
+
for resolver in resolvers
|
71
|
+
if resolver.is_a? Hash
|
72
|
+
value = resolver[name]
|
73
|
+
elsif resolver.is_a? Proc
|
74
|
+
value = resolver.call(name)
|
75
|
+
elsif resolver.is_a? Method
|
76
|
+
value = resolver.call(name)
|
77
|
+
elsif resolver.is_a? Resolver
|
78
|
+
value = resolver.resolve(full_name)
|
79
|
+
elsif resolver.is_a? Object
|
80
|
+
value = resolver.send(name)
|
81
|
+
end
|
82
|
+
return value if value
|
83
|
+
end
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Lti2Commons
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'uuid'
|
5
|
+
|
6
|
+
module Utils
|
7
|
+
def is_hash_intersect node, constraint_hash
|
8
|
+
# failed search if constraint_hash as invalid keys
|
9
|
+
return nil if (constraint_hash.keys - node.keys).length > 0
|
10
|
+
node.each_pair {|k,v|
|
11
|
+
if constraint_hash.has_key? k
|
12
|
+
return false unless v == constraint_hash[k]
|
13
|
+
end
|
14
|
+
}
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def hash_to_query_string(hash)
|
19
|
+
hash.keys.inject('') do |query_string, key|
|
20
|
+
query_string << "&" unless key == hash.keys.first
|
21
|
+
query_string << "#{URI.encode(key.to_s)}=#{CGI.escape(hash[key])}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def log(msg)
|
26
|
+
# puts msg
|
27
|
+
end
|
28
|
+
|
29
|
+
def substitute_template_values_from_hash(source_string, prefix, suffix, hash)
|
30
|
+
hash.each { |k,v| source_string.sub!(prefix+k+suffix, v) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module Lti2Commons
|
5
|
+
|
6
|
+
module WireLogSupport
|
7
|
+
|
8
|
+
class WireLog
|
9
|
+
STATUS_CODES = {
|
10
|
+
100 => "Continue",
|
11
|
+
101 => "Switching Protocols",
|
12
|
+
102 => "Processing",
|
13
|
+
|
14
|
+
200 => "OK",
|
15
|
+
201 => "Created",
|
16
|
+
202 => "Accepted",
|
17
|
+
203 => "Non-Authoritative Information",
|
18
|
+
204 => "No Content",
|
19
|
+
205 => "Reset Content",
|
20
|
+
206 => "Partial Content",
|
21
|
+
207 => "Multi-Status",
|
22
|
+
226 => "IM Used",
|
23
|
+
|
24
|
+
300 => "Multiple Choices",
|
25
|
+
301 => "Moved Permanently",
|
26
|
+
302 => "Found",
|
27
|
+
303 => "See Other",
|
28
|
+
304 => "Not Modified",
|
29
|
+
305 => "Use Proxy",
|
30
|
+
307 => "Temporary Redirect",
|
31
|
+
|
32
|
+
400 => "Bad Request",
|
33
|
+
401 => "Unauthorized",
|
34
|
+
402 => "Payment Required",
|
35
|
+
403 => "Forbidden",
|
36
|
+
404 => "Not Found",
|
37
|
+
405 => "Method Not Allowed",
|
38
|
+
406 => "Not Acceptable",
|
39
|
+
407 => "Proxy Authentication Required",
|
40
|
+
408 => "Request Timeout",
|
41
|
+
409 => "Conflict",
|
42
|
+
410 => "Gone",
|
43
|
+
411 => "Length Required",
|
44
|
+
412 => "Precondition Failed",
|
45
|
+
413 => "Request Entity Too Large",
|
46
|
+
414 => "Request-URI Too Long",
|
47
|
+
415 => "Unsupported Media Type",
|
48
|
+
416 => "Requested Range Not Satisfiable",
|
49
|
+
417 => "Expectation Failed",
|
50
|
+
422 => "Unprocessable Entity",
|
51
|
+
423 => "Locked",
|
52
|
+
424 => "Failed Dependency",
|
53
|
+
426 => "Upgrade Required",
|
54
|
+
|
55
|
+
500 => "Internal Server Error",
|
56
|
+
501 => "Not Implemented",
|
57
|
+
502 => "Bad Gateway",
|
58
|
+
503 => "Service Unavailable",
|
59
|
+
504 => "Gateway Timeout",
|
60
|
+
505 => "HTTP Version Not Supported",
|
61
|
+
507 => "Insufficient Storage",
|
62
|
+
510 => "Not Extended"
|
63
|
+
}
|
64
|
+
|
65
|
+
attr_accessor :is_logging, :output_file_name
|
66
|
+
|
67
|
+
def initialize wire_log_name, output_file, is_html_output=true
|
68
|
+
@output_file_name = output_file
|
69
|
+
is_logging = true
|
70
|
+
@wire_log_name = wire_log_name
|
71
|
+
@log_buffer = nil
|
72
|
+
@is_html_output = is_html_output
|
73
|
+
end
|
74
|
+
|
75
|
+
def clear_log
|
76
|
+
output_file = File.open(@output_file_name, "a+")
|
77
|
+
output_file.truncate(0)
|
78
|
+
output_file.close
|
79
|
+
end
|
80
|
+
|
81
|
+
def flush options={}
|
82
|
+
output_file = File.open(@output_file_name, "a+")
|
83
|
+
@log_buffer.rewind
|
84
|
+
buffer = @log_buffer.read
|
85
|
+
if @is_html_output
|
86
|
+
oldbuffer = buffer.dup
|
87
|
+
buffer = ""
|
88
|
+
oldbuffer.each_char { |c|
|
89
|
+
if c == '<'
|
90
|
+
buffer << "<"
|
91
|
+
elsif c == '>'
|
92
|
+
buffer << ">"
|
93
|
+
else
|
94
|
+
buffer << c
|
95
|
+
end
|
96
|
+
}
|
97
|
+
end
|
98
|
+
if options.has_key? :css_class
|
99
|
+
css_class = options[:css_class]
|
100
|
+
else
|
101
|
+
css_class = "#{@wire_log_name}"
|
102
|
+
end
|
103
|
+
output_file.puts("<div class=\"#{css_class}\"><pre>") if @is_html_output
|
104
|
+
output_file.write(buffer)
|
105
|
+
output_file.puts("\n</div></pre>") if @is_html_output
|
106
|
+
output_file.close
|
107
|
+
@log_buffer = nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def log s
|
111
|
+
timestamp
|
112
|
+
raw_log "#{s}"
|
113
|
+
flush
|
114
|
+
end
|
115
|
+
|
116
|
+
def log_response response, title=nil
|
117
|
+
timestamp
|
118
|
+
raw_log(title.nil? ? "Response" : "Response: #{title}")
|
119
|
+
raw_log "Status: #{response.code} #{STATUS_CODES[response.code]}"
|
120
|
+
headers = response.headers
|
121
|
+
unless headers.blank?
|
122
|
+
raw_log "Headers:"
|
123
|
+
headers.each { |k,v| raw_log "#{k}: #{v}" if k.downcase =~ /^content/ }
|
124
|
+
end
|
125
|
+
|
126
|
+
if response.body
|
127
|
+
# the following is expensive so do only when needed
|
128
|
+
if is_logging
|
129
|
+
raw_log "Body:"
|
130
|
+
end
|
131
|
+
begin
|
132
|
+
json_obj = JSON.load(response.body)
|
133
|
+
raw_log JSON.pretty_generate json_obj
|
134
|
+
rescue
|
135
|
+
raw_log "#{response.body}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
newline
|
139
|
+
flush :css_class => "#{@wire_log_name}Response"
|
140
|
+
end
|
141
|
+
|
142
|
+
def newline
|
143
|
+
raw_log "\n"
|
144
|
+
end
|
145
|
+
|
146
|
+
def log_buffer
|
147
|
+
# put in the css header if file doesn't exist
|
148
|
+
unless File.size? @output_file_name
|
149
|
+
@output_file = File.open @output_file_name, "a"
|
150
|
+
@output_file.puts '<link rel="stylesheet" type="text/css" href="wirelog.css" />'
|
151
|
+
@output_file.puts ""
|
152
|
+
@output_file.close
|
153
|
+
end
|
154
|
+
unless @log_buffer
|
155
|
+
@log_buffer = StringIO.new
|
156
|
+
end
|
157
|
+
@log_buffer
|
158
|
+
end
|
159
|
+
|
160
|
+
def raw_log s
|
161
|
+
@log_buffer = log_buffer
|
162
|
+
@log_buffer.puts(s)
|
163
|
+
end
|
164
|
+
|
165
|
+
def timestamp
|
166
|
+
raw_log(Time.new)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lti2_commons
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: "1.0"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- John Tibbetts
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2013-01-16 00:00:00 Z
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: oauth
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 5
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 4
|
31
|
+
- 5
|
32
|
+
version: 0.4.5
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: omniauth
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 15
|
44
|
+
segments:
|
45
|
+
- 1
|
46
|
+
- 0
|
47
|
+
version: "1.0"
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: curb
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
type: :runtime
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: httparty
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
type: :runtime
|
77
|
+
version_requirements: *id004
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: lrucache
|
80
|
+
prerelease: false
|
81
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 3
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
type: :runtime
|
91
|
+
version_requirements: *id005
|
92
|
+
description: LTI common utilities
|
93
|
+
email:
|
94
|
+
- jtibbetts@kinexis.com
|
95
|
+
executables: []
|
96
|
+
|
97
|
+
extensions: []
|
98
|
+
|
99
|
+
extra_rdoc_files: []
|
100
|
+
|
101
|
+
files:
|
102
|
+
- lib/lti2_commons/cache.rb
|
103
|
+
- lib/lti2_commons/json_wrapper.rb
|
104
|
+
- lib/lti2_commons/lti2_launch.rb
|
105
|
+
- lib/lti2_commons/message_support.rb
|
106
|
+
- lib/lti2_commons/oauth_request.rb
|
107
|
+
- lib/lti2_commons/signer.rb
|
108
|
+
- lib/lti2_commons/substitution_support.rb
|
109
|
+
- lib/lti2_commons/utils.rb
|
110
|
+
- lib/lti2_commons/version.rb
|
111
|
+
- lib/lti2_commons/wire_log.rb
|
112
|
+
- lib/lti2_commons.rb
|
113
|
+
- Gemfile
|
114
|
+
- License.pdf
|
115
|
+
- Rakefile
|
116
|
+
- README.md
|
117
|
+
homepage:
|
118
|
+
licenses: []
|
119
|
+
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
hash: 3
|
131
|
+
segments:
|
132
|
+
- 0
|
133
|
+
version: "0"
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
hash: 3
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
version: "0"
|
143
|
+
requirements: []
|
144
|
+
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 1.8.24
|
147
|
+
signing_key:
|
148
|
+
specification_version: 3
|
149
|
+
summary: LTI common utilities
|
150
|
+
test_files: []
|
151
|
+
|