lti2_commons 1.0

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