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 ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in omniauth-oauthverifier.gemspec
4
+ gemspec
Binary file
@@ -0,0 +1,7 @@
1
+ lti2_commons -- LTI2 Common functions
2
+ =====================================
3
+
4
+ lti2_commons is a Ruby gem that implementas a variety of LTI2 utility functions.
5
+
6
+ This gem is dynamically loaded by lti2_tc and lti2_tp using the ruby bundler capability.
7
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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,3 @@
1
+ module Lti2Commons
2
+ VERSION = "0.0.7"
3
+ 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 << "&lt;"
91
+ elsif c == '>'
92
+ buffer << "&gt;"
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
+