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