diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c02f57ae6610..014b432f581c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
+Blades: Refactor stub implementation of LTI Provider. BLD-601.
+
LMS: In left accordion and progress page, due dates are now displayed in time
zone specified by settings.TIME_ZONE, instead of UTC always
diff --git a/common/djangoapps/terrain/start_stubs.py b/common/djangoapps/terrain/start_stubs.py
index 7d115d04a703..ad9ffbbce3ab 100644
--- a/common/djangoapps/terrain/start_stubs.py
+++ b/common/djangoapps/terrain/start_stubs.py
@@ -6,11 +6,12 @@
from django.conf import settings
from terrain.stubs.youtube import StubYouTubeService
from terrain.stubs.xqueue import StubXQueueService
-
+from terrain.stubs.lti import StubLtiService
SERVICES = {
"youtube": {"port": settings.YOUTUBE_PORT, "class": StubYouTubeService},
"xqueue": {"port": settings.XQUEUE_PORT, "class": StubXQueueService},
+ "lti": {"port": settings.LTI_PORT, "class": StubLtiService},
}
diff --git a/common/djangoapps/terrain/stubs/http.py b/common/djangoapps/terrain/stubs/http.py
index 7a6f71cadd65..621b1eb0293b 100644
--- a/common/djangoapps/terrain/stubs/http.py
+++ b/common/djangoapps/terrain/stubs/http.py
@@ -236,7 +236,7 @@ def __init__(self, port_num=0):
Configure the server to listen on localhost.
Default is to choose an arbitrary open port.
"""
- address = ('127.0.0.1', port_num)
+ address = ('0.0.0.0', port_num)
HTTPServer.__init__(self, address, self.HANDLER_CLASS)
# Create a dict to store configuration values set by the client
diff --git a/common/djangoapps/terrain/stubs/lti.py b/common/djangoapps/terrain/stubs/lti.py
new file mode 100644
index 000000000000..d2ca7998bc51
--- /dev/null
+++ b/common/djangoapps/terrain/stubs/lti.py
@@ -0,0 +1,237 @@
+"""
+Stub implementation of LTI Provider.
+
+What is supported:
+------------------
+
+1.) This LTI Provider can service only one Tool Consumer at the same time. It is
+not possible to have this LTI multiple times on a single page in LMS.
+
+"""
+
+from uuid import uuid4
+import textwrap
+import urllib
+import re
+from oauthlib.oauth1.rfc5849 import signature
+import oauthlib.oauth1
+import hashlib
+import base64
+import mock
+import requests
+from http import StubHttpRequestHandler, StubHttpService
+
+class StubLtiHandler(StubHttpRequestHandler):
+ """
+ A handler for LTI POST and GET requests.
+ """
+ DEFAULT_CLIENT_KEY = 'test_client_key'
+ DEFAULT_CLIENT_SECRET = 'test_client_secret'
+ DEFAULT_LTI_ENDPOINT = 'correct_lti_endpoint'
+ DEFAULT_LTI_ADDRESS = 'http://127.0.0.1:{port}/'
+
+ def do_GET(self):
+ """
+ Handle a GET request from the client and sends response back.
+
+ Used for checking LTI Provider started correctly.
+ """
+ self.send_response(200, 'This is LTI Provider.', {'Content-type': 'text/plain'})
+
+ def do_POST(self):
+ """
+ Handle a POST request from the client and sends response back.
+ """
+ if 'grade' in self.path and self._send_graded_result().status_code == 200:
+ status_message = 'LTI consumer (edX) responded with XML content:
' + self.server.grade_data['TC answer']
+ content = self._create_content(status_message)
+ self.send_response(200, content)
+
+ # Respond to request with correct lti endpoint
+ elif self._is_correct_lti_request():
+ params = {k: v for k, v in self.post_dict.items() if k != 'oauth_signature'}
+
+ if self._check_oauth_signature(params, self.post_dict.get('oauth_signature', "")):
+ status_message = "This is LTI tool. Success."
+
+ # Set data for grades what need to be stored as server data
+ if 'lis_outcome_service_url' in self.post_dict:
+ self.server.grade_data = {
+ 'callback_url': self.post_dict.get('lis_outcome_service_url'),
+ 'sourcedId': self.post_dict.get('lis_result_sourcedid')
+ }
+
+ submit_url = '//{}:{}'.format(*self.server.server_address)
+ content = self._create_content(status_message, submit_url)
+ self.send_response(200, content)
+
+ else:
+ content = self._create_content("Wrong LTI signature")
+ self.send_response(200, content)
+ else:
+ content = self._create_content("Invalid request URL")
+ self.send_response(500, content)
+
+ def _send_graded_result(self):
+ """
+ Send grade request.
+ """
+ values = {
+ 'textString': 0.5,
+ 'sourcedId': self.server.grade_data['sourcedId'],
+ 'imsx_messageIdentifier': uuid4().hex,
+ }
+ payload = textwrap.dedent("""
+
+
+
+
+ V1.0
+ {imsx_messageIdentifier} /
+
+
+
+
+
+
+ {sourcedId}
+
+
+
+ en-us
+ {textString}
+
+
+
+
+
+
+ """)
+
+ data = payload.format(**values)
+ url = self.server.grade_data['callback_url']
+ headers = {
+ 'Content-Type': 'application/xml',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Authorization': self._oauth_sign(url, data)
+ }
+
+ # Send request ignoring verifirecation of SSL certificate
+ response = requests.post(url, data=data, headers=headers, verify=False)
+
+ self.server.grade_data['TC answer'] = response.content
+ return response
+
+ def _create_content(self, response_text, submit_url=None):
+ """
+ Return content (str) either for launch, send grade or get result from TC.
+ """
+ if submit_url:
+ submit_form = textwrap.dedent("""
+
+ """).format(submit_url)
+ else:
+ submit_form = ''
+
+ # Show roles only for LTI launch.
+ if self.post_dict.get('roles'):
+ role = 'Role: {}
'.format(self.post_dict['roles'])
+ else:
+ role = ''
+
+ response_str = textwrap.dedent("""
+
+
+ TEST TITLE
+
+
+
+
IFrame loaded
+ Server response is:
+ {response}
+ {role}
+
+ {submit_form}
+
+
+ """).format(response=response_text, role=role, submit_form=submit_form)
+
+ # Currently LTI module doublequotes the lis_result_sourcedid parameter.
+ # Unquote response two times.
+ return urllib.unquote(urllib.unquote(response_str))
+
+ def _is_correct_lti_request(self):
+ """
+ Return a boolean indicating whether the URL path is a valid LTI end-point.
+ """
+ lti_endpoint = self.server.config.get('lti_endpoint', self.DEFAULT_LTI_ENDPOINT)
+ return lti_endpoint in self.path
+
+ def _oauth_sign(self, url, body):
+ """
+ Signs request and returns signed body and headers.
+ """
+ client_key = self.server.config.get('client_key', self.DEFAULT_CLIENT_KEY)
+ client_secret = self.server.config.get('client_secret', self.DEFAULT_CLIENT_SECRET)
+ client = oauthlib.oauth1.Client(
+ client_key=unicode(client_key),
+ client_secret=unicode(client_secret)
+ )
+ headers = {
+ # This is needed for body encoding:
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+
+ # Calculate and encode body hash. See http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+ sha1 = hashlib.sha1()
+ sha1.update(body)
+ oauth_body_hash = base64.b64encode(sha1.digest())
+ __, headers, __ = client.sign(
+ unicode(url.strip()),
+ http_method=u'POST',
+ body={u'oauth_body_hash': oauth_body_hash},
+ headers=headers
+ )
+ headers = headers['Authorization'] + ', oauth_body_hash="{}"'.format(oauth_body_hash)
+ return headers
+
+ def _check_oauth_signature(self, params, client_signature):
+ """
+ Checks oauth signature from client.
+
+ `params` are params from post request except signature,
+ `client_signature` is signature from request.
+
+ Builds mocked request and verifies hmac-sha1 signing::
+ 1. builds string to sign from `params`, `url` and `http_method`.
+ 2. signs it with `client_secret` which comes from server settings.
+ 3. obtains signature after sign and then compares it with request.signature
+ (request signature comes form client in request)
+
+ Returns `True` if signatures are correct, otherwise `False`.
+
+ """
+ client_secret = unicode(self.server.config.get('client_secret', self.DEFAULT_CLIENT_SECRET))
+
+ port = self.server.server_address[1]
+ lti_base = self.DEFAULT_LTI_ADDRESS.format(port=port)
+ lti_endpoint = self.server.config.get('lti_endpoint', self.DEFAULT_LTI_ENDPOINT)
+ url = lti_base + lti_endpoint
+
+ request = mock.Mock()
+ request.params = [(unicode(k), unicode(v)) for k, v in params.items()]
+ request.uri = unicode(url)
+ request.http_method = u'POST'
+ request.signature = unicode(client_signature)
+ return signature.verify_hmac_sha1(request, client_secret)
+
+
+class StubLtiService(StubHttpService):
+ """
+ A stub LTI provider server that responds
+ to POST and GET requests to localhost.
+ """
+
+ HANDLER_CLASS = StubLtiHandler
diff --git a/common/djangoapps/terrain/stubs/start.py b/common/djangoapps/terrain/stubs/start.py
index e58ecf243be2..f9fdf4a388e9 100644
--- a/common/djangoapps/terrain/stubs/start.py
+++ b/common/djangoapps/terrain/stubs/start.py
@@ -8,6 +8,7 @@
from .xqueue import StubXQueueService
from .youtube import StubYouTubeService
from .ora import StubOraService
+from .lti import StubLtiService
USAGE = "USAGE: python -m stubs.start SERVICE_NAME PORT_NUM [CONFIG_KEY=CONFIG_VAL, ...]"
@@ -17,6 +18,7 @@
'youtube': StubYouTubeService,
'ora': StubOraService,
'comments': StubCommentsService,
+ 'lti': StubLtiService,
}
# Log to stdout, including debug messages
diff --git a/common/djangoapps/terrain/stubs/tests/test_lti_stub.py b/common/djangoapps/terrain/stubs/tests/test_lti_stub.py
new file mode 100644
index 000000000000..40a5bf37b1ec
--- /dev/null
+++ b/common/djangoapps/terrain/stubs/tests/test_lti_stub.py
@@ -0,0 +1,72 @@
+"""
+Unit tests for stub LTI implementation.
+"""
+from mock import Mock, patch
+import unittest
+import urllib2
+import requests
+from terrain.stubs.lti import StubLtiService
+
+class StubLtiServiceTest(unittest.TestCase):
+ """
+ A stub of the LTI provider that listens on a local
+ port and responds with pre-defined grade messages.
+
+ Used for lettuce BDD tests in lms/courseware/features/lti.feature
+ """
+ def setUp(self):
+ self.server = StubLtiService()
+ self.uri = 'http://127.0.0.1:{}/'.format(self.server.port)
+ self.launch_uri = self.uri + 'correct_lti_endpoint'
+ self.addCleanup(self.server.shutdown)
+ self.payload = {
+ 'user_id': 'default_user_id',
+ 'roles': 'Student',
+ 'oauth_nonce': '',
+ 'oauth_timestamp': '',
+ 'oauth_consumer_key': 'test_client_key',
+ 'lti_version': 'LTI-1p0',
+ 'oauth_signature_method': 'HMAC-SHA1',
+ 'oauth_version': '1.0',
+ 'oauth_signature': '',
+ 'lti_message_type': 'basic-lti-launch-request',
+ 'oauth_callback': 'about:blank',
+ 'launch_presentation_return_url': '',
+ 'lis_outcome_service_url': 'http://localhost:8001/test_callback',
+ 'lis_result_sourcedid': '',
+ 'resource_link_id':'',
+ }
+
+ def test_invalid_request_url(self):
+ """
+ Tests that LTI server processes request with right program path but with wrong header.
+ """
+ self.launch_uri = self.uri + 'wrong_lti_endpoint'
+ response = requests.post(self.launch_uri, data=self.payload)
+ self.assertIn('Invalid request URL', response.content)
+
+ def test_wrong_signature(self):
+ """
+ Tests that LTI server processes request with right program
+ path and responses with incorrect signature.
+ """
+ response = requests.post(self.launch_uri, data=self.payload)
+ self.assertIn('Wrong LTI signature', response.content)
+
+ @patch('terrain.stubs.lti.signature.verify_hmac_sha1', return_value=True)
+ def test_success_response_launch_lti(self, check_oauth):
+ """
+ Success lti launch.
+ """
+ response = requests.post(self.launch_uri, data=self.payload)
+ self.assertIn('This is LTI tool. Success.', response.content)
+
+ @patch('terrain.stubs.lti.signature.verify_hmac_sha1', return_value=True)
+ def test_send_graded_result(self, verify_hmac):
+ response = requests.post(self.launch_uri, data=self.payload)
+ self.assertIn('This is LTI tool. Success.', response.content)
+ grade_uri = self.uri + 'grade'
+ with patch('terrain.stubs.lti.requests.post') as mocked_post:
+ mocked_post.return_value = Mock(content='Test response', status_code=200)
+ response = urllib2.urlopen(grade_uri, data='')
+ self.assertIn('Test response', response.read())
diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py
index f963c148a9d9..8865193b098b 100644
--- a/common/lib/xmodule/xmodule/lti_module.py
+++ b/common/lib/xmodule/xmodule/lti_module.py
@@ -289,7 +289,7 @@ def get_outcome_service_url(self):
While testing locally and on Jenkins, mock_lti_server use http.referer
to obtain scheme, so it is ok to have http(s) anyway.
"""
- scheme = 'http' if 'sandbox' in self.system.hostname else 'https'
+ scheme = 'http' if 'sandbox' in self.system.hostname or self.system.debug else 'https'
uri = '{scheme}://{host}{path}'.format(
scheme=scheme,
host=self.system.hostname,
@@ -325,7 +325,11 @@ def get_lis_result_sourcedid(self):
the link being launched.
lti_id should be context_id by meaning.
"""
- return u':'.join(urllib.quote(i) for i in (self.lti_id, self.get_resource_link_id(), self.get_user_id()))
+ return "{id}:{resource_link}:{user_id}".format(
+ id=urllib.quote(self.lti_id),
+ resource_link=urllib.quote(self.get_resource_link_id()),
+ user_id=urllib.quote(self.get_user_id())
+ )
def get_course(self):
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_lti_unit.py b/common/lib/xmodule/xmodule/tests/test_lti_unit.py
index 718c738a8b75..141fe6645c91 100644
--- a/common/lib/xmodule/xmodule/tests/test_lti_unit.py
+++ b/common/lib/xmodule/xmodule/tests/test_lti_unit.py
@@ -246,7 +246,8 @@ def test_user_id(self):
self.assertEqual(real_user_id, expected_user_id)
def test_outcome_service_url(self):
- expected_outcome_service_url = 'https://{host}{path}'.format(
+ expected_outcome_service_url = '{scheme}://{host}{path}'.format(
+ scheme='http' if self.xmodule.runtime.debug else 'https',
host=self.xmodule.runtime.hostname,
path=self.xmodule.runtime.handler_url(self.xmodule, 'grade_handler', thirdparty=True).rstrip('/?')
)
diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py
index 5c47c6ea85d8..a2560a9e61ed 100644
--- a/lms/djangoapps/courseware/features/lti.py
+++ b/lms/djangoapps/courseware/features/lti.py
@@ -9,6 +9,7 @@
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
+from django.conf import settings
from lettuce import world, step
from lettuce.django import django_url
@@ -81,10 +82,7 @@ def incorrect_lti_is_rendered(_step):
def set_correct_lti_passport(_step, user='Instructor'):
coursenum = 'test_course'
metadata = {
- 'lti_passports': ["correct_lti_id:{}:{}".format(
- world.lti_server.oauth_settings['client_key'],
- world.lti_server.oauth_settings['client_secret']
- )]
+ 'lti_passports': ["correct_lti_id:test_client_key:test_client_secret"]
}
i_am_registered_for_the_course(coursenum, metadata, user)
@@ -94,10 +92,7 @@ def set_correct_lti_passport(_step, user='Instructor'):
def set_incorrect_lti_passport(_step):
coursenum = 'test_course'
metadata = {
- 'lti_passports': ["test_lti_id:{}:{}".format(
- world.lti_server.oauth_settings['client_key'],
- "incorrect_lti_secret_key"
- )]
+ 'lti_passports': ["test_lti_id:test_client_key:incorrect_lti_secret_key"]
}
i_am_registered_for_the_course(coursenum, metadata)
@@ -108,7 +103,7 @@ def add_correct_lti_to_course(_step, fields):
category = 'lti'
metadata = {
'lti_id': 'correct_lti_id',
- 'launch_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint'],
+ 'launch_url': 'http://127.0.0.1:{}/correct_lti_endpoint'.format(settings.LTI_PORT),
}
if fields.strip() == 'incorrect_lti_id': # incorrect fields
diff --git a/lms/djangoapps/courseware/features/lti_setup.py b/lms/djangoapps/courseware/features/lti_setup.py
deleted file mode 100644
index 51078102f60a..000000000000
--- a/lms/djangoapps/courseware/features/lti_setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#pylint: disable=C0111
-#pylint: disable=W0621
-
-from courseware.mock_lti_server.mock_lti_server import MockLTIServer
-from lettuce import before, after, world
-from django.conf import settings
-import threading
-
-from logging import getLogger
-logger = getLogger(__name__)
-
-
-@before.all
-def setup_mock_lti_server():
-
- server_host = '127.0.0.1'
- server_port = settings.LTI_PORT
-
- address = (server_host, server_port)
-
- # Create the mock server instance
- server = MockLTIServer(address)
- logger.debug("LTI server started at {} port".format(str(server_port)))
- # Start the server running in a separate daemon thread
- # Because the thread is a daemon, it will terminate
- # when the main thread terminates.
- server_thread = threading.Thread(target=server.serve_forever)
- server_thread.daemon = True
- server_thread.start()
-
- server.server_host = server_host
- server.oauth_settings = {
- 'client_key': 'test_client_key',
- 'client_secret': 'test_client_secret',
- 'lti_base': 'http://{}:{}/'.format(server_host, server_port),
- 'lti_endpoint': 'correct_lti_endpoint'
- }
-
- # For testing on localhost make callback url using referer host.
- server.real_callback_url_on = False
-
- # Store the server instance in lettuce's world
- # so that other steps can access it
- # (and we can shut it down later)
- world.lti_server = server
-
-
-@after.all
-def teardown_mock_lti_server(total):
-
- # Stop the LTI server and free up the port
- world.lti_server.shutdown()
diff --git a/lms/djangoapps/courseware/mock_lti_server/__init__.py b/lms/djangoapps/courseware/mock_lti_server/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py b/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py
deleted file mode 100644
index 235e2f851fa4..000000000000
--- a/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py
+++ /dev/null
@@ -1,310 +0,0 @@
-"""
-LTI Server
-
-What is supported:
-------------------
-
-1.) This LTI Provider can service only one Tool Consumer at the same time. It is
-not possible to have this LTI multiple times on a single page in LMS.
-
-"""
-from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
-from uuid import uuid4
-import textwrap
-import urlparse
-from oauthlib.oauth1.rfc5849 import signature
-import oauthlib.oauth1
-import hashlib
-import base64
-import mock
-import sys
-import requests
-import textwrap
-
-from logging import getLogger
-logger = getLogger(__name__)
-
-
-class MockLTIRequestHandler(BaseHTTPRequestHandler):
- '''
- A handler for LTI POST requests.
- '''
-
- protocol = "HTTP/1.0"
- callback_url = None
-
- def log_message(self, format, *args):
- """Log an arbitrary message."""
- # Code copied from BaseHTTPServer.py. Changed to write to sys.stdout
- # so that messages won't pollute test output.
- sys.stdout.write("%s - - [%s] %s\n" %
- (self.client_address[0],
- self.log_date_time_string(),
- format % args))
-
- def do_GET(self):
- '''
- Handle a GET request from the client and sends response back.
-
- Used for checking LTI Provider started correctly.
- '''
- self.send_response(200, 'OK')
- self.send_header('Content-type', 'html')
- self.end_headers()
- response_str = """TEST TITLE
- This is LTI Provider."""
- self.wfile.write(response_str)
-
- def do_POST(self):
- '''
- Handle a POST request from the client and sends response back.
- '''
- if 'grade' in self.path and self._send_graded_result().status_code == 200:
- status_message = 'LTI consumer (edX) responded with XML content:
' + self.server.grade_data['TC answer']
- self.server.grade_data = None
- self._send_response(status_message, 200)
- # Respond to request with correct lti endpoint:
- elif self._is_correct_lti_request():
- self.post_dict = self._post_dict()
- params = {k: v for k, v in self.post_dict.items() if k != 'oauth_signature'}
- if self.server.check_oauth_signature(params, self.post_dict.get('oauth_signature', "")):
- status_message = "This is LTI tool. Success."
- # set data for grades what need to be stored as server data
- if 'lis_outcome_service_url' in self.post_dict:
- self.server.grade_data = {
- 'callback_url': self.post_dict.get('lis_outcome_service_url'),
- 'sourcedId': self.post_dict.get('lis_result_sourcedid')
- }
- else:
- status_message = "Wrong LTI signature"
- self._send_response(status_message, 200)
- else:
- status_message = "Invalid request URL"
- self._send_response(status_message, 500)
-
- def _send_head(self, status_code):
- '''
- Send the response code and MIME headers
- '''
- self.send_response(status_code)
- self.send_header('Content-type', 'text/html')
- self.end_headers()
-
- def _post_dict(self):
- '''
- Retrieve the POST parameters from the client as a dictionary
- '''
- try:
- length = int(self.headers.getheader('content-length'))
- post_dict = urlparse.parse_qs(self.rfile.read(length), keep_blank_values=True)
- # The POST dict will contain a list of values for each key.
- # None of our parameters are lists, however, so we map [val] --> val.
- # If the list contains multiple entries, we pick the first one
- post_dict = {key: val[0] for key, val in post_dict.items()}
- except:
- # We return an empty dict here, on the assumption
- # that when we later check that the request has
- # the correct fields, it won't find them,
- # and will therefore send an error response
- return {}
- try:
- cookie = self.headers.getheader('cookie')
- self.server.cookie = {k.strip(): v[0] for k, v in urlparse.parse_qs(cookie).items()}
- except:
- self.server.cookie = {}
- referer = urlparse.urlparse(self.headers.getheader('referer'))
- self.server.referer_host = "{}://{}".format(referer.scheme, referer.netloc)
- return post_dict
-
- def _send_graded_result(self):
- """
- Send grade request.
- """
- values = {
- 'textString': 0.5,
- 'sourcedId': self.server.grade_data['sourcedId'],
- 'imsx_messageIdentifier': uuid4().hex,
- }
- payload = textwrap.dedent("""
-
-
-
-
- V1.0
- {imsx_messageIdentifier} /
-
-
-
-
-
-
- {sourcedId}
-
-
-
- en-us
- {textString}
-
-
-
-
-
-
- """)
- data = payload.format(**values)
- if getattr(self.server, 'use_real_callback_url', None):
- # Use exact URL that was sent from TC when using this Stub LTI server
- # as TP in real standalone environment.
- url = self.server.grade_data['callback_url']
- else:
- # Use relative URL when using TP locally for manual testing or jenkins.
- relative_url = urlparse.urlparse(self.server.grade_data['callback_url']).path
- url = self.server.referer_host + relative_url
-
- headers = {'Content-Type': 'application/xml', 'X-Requested-With': 'XMLHttpRequest'}
- headers['Authorization'] = self.oauth_sign(url, data)
-
- # We can't mock requests in unit tests, because we use them, but we need
- # them to be mocked only for this one case.
- if getattr(self.server, 'run_inside_unittest_flag', None):
- response = mock.Mock(status_code=200, url=url, data=data, headers=headers)
- return response
- # Send request ignoring verification of SSL certificate
- response = requests.post(
- url,
- data=data,
- headers=headers,
- verify=False
- )
- self.server.grade_data['TC answer'] = response.content
- return response
-
- def _send_response(self, message, status_code):
- '''
- Send message back to the client
- '''
- self._send_head(status_code)
- if getattr(self.server, 'grade_data', False): # lti can be graded
- url = "//%s:%s" % self.server.server_address
- response_str = textwrap.dedent("""
-
-
- TEST TITLE
-
-
-
-
Graded IFrame loaded
- Server response is:
- {}
- Role: {role}
-
-
-
-
- """).format(message, role=self.post_dict['roles'], url=url)
- else: # lti can't be graded
- response_str = textwrap.dedent("""
-
-
- TEST TITLE
-
-
-
-
IFrame loaded
- Server response is:
- {}
-
-
-
-
- """).format(message)
-
- logger.debug("LTI: sent response {}".format(response_str))
- self.wfile.write(response_str)
-
- def _is_correct_lti_request(self):
- '''
- If url to LTI tool is correct.
- '''
- return self.server.oauth_settings['lti_endpoint'] in self.path
-
- def oauth_sign(self, url, body):
- """
- Signs request and returns signed body and headers.
- """
- client = oauthlib.oauth1.Client(
- client_key=unicode(self.server.oauth_settings['client_key']),
- client_secret=unicode(self.server.oauth_settings['client_secret'])
- )
- headers = {
- # This is needed for body encoding:
- 'Content-Type': 'application/x-www-form-urlencoded',
- }
-
- #Calculate and encode body hash. See http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
- sha1 = hashlib.sha1()
- sha1.update(body)
- oauth_body_hash = base64.b64encode(sha1.digest())
- __, headers, __ = client.sign(
- unicode(url.strip()),
- http_method=u'POST',
- body={u'oauth_body_hash': oauth_body_hash},
- headers=headers
- )
- headers = headers['Authorization'] + ', oauth_body_hash="{}"'.format(oauth_body_hash)
- return headers
-
-
-class MockLTIServer(HTTPServer):
- '''
- A mock LTI provider server that responds
- to POST requests to localhost.
- '''
-
- def __init__(self, address):
- '''
- Initialize the mock XQueue server instance.
-
- *address* is the (host, host's port to listen to) tuple.
- '''
- handler = MockLTIRequestHandler
- HTTPServer.__init__(self, address, handler)
-
- def shutdown(self):
- '''
- Stop the server and free up the port
- '''
- # First call superclass shutdown()
- HTTPServer.shutdown(self)
- # We also need to manually close the socket
- self.socket.close()
-
- def check_oauth_signature(self, params, client_signature):
- '''
- Checks oauth signature from client.
-
- `params` are params from post request except signature,
- `client_signature` is signature from request.
-
- Builds mocked request and verifies hmac-sha1 signing::
- 1. builds string to sign from `params`, `url` and `http_method`.
- 2. signs it with `client_secret` which comes from server settings.
- 3. obtains signature after sign and then compares it with request.signature
- (request signature comes form client in request)
-
- Returns `True` if signatures are correct, otherwise `False`.
-
- '''
- client_secret = unicode(self.oauth_settings['client_secret'])
- url = self.oauth_settings['lti_base'] + self.oauth_settings['lti_endpoint']
-
- request = mock.Mock()
-
- request.params = [(unicode(k), unicode(v)) for k, v in params.items()]
- request.uri = unicode(url)
- request.http_method = u'POST'
- request.signature = unicode(client_signature)
- return signature.verify_hmac_sha1(request, client_secret)
-
diff --git a/lms/djangoapps/courseware/mock_lti_server/server_start.py b/lms/djangoapps/courseware/mock_lti_server/server_start.py
deleted file mode 100644
index 8e37350b4b06..000000000000
--- a/lms/djangoapps/courseware/mock_lti_server/server_start.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Mock LTI server for manual testing.
-
-Used for manual testing and testing on sandbox.
-"""
-
-from mock_lti_server import MockLTIServer
-
-server_port = 8034
-server_host = 'localhost'
-address = (server_host, server_port)
-
-server = MockLTIServer(address)
-server.oauth_settings = {
- 'client_key': 'test_client_key',
- 'client_secret': 'test_client_secret',
- 'lti_base': 'http://{}:{}/'.format(server_host, server_port),
- 'lti_endpoint': 'correct_lti_endpoint'
-}
-server.server_host = server_host
-server.server_port = server_port
-
-# For testing on localhost make callback url using referer host.
-server.use_real_callback_url = False
-
-try:
- server.serve_forever()
-except KeyboardInterrupt:
- print('^C received, shutting down server')
- server.socket.close()
diff --git a/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py b/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py
deleted file mode 100644
index 34d6f6d37cc4..000000000000
--- a/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py
+++ /dev/null
@@ -1,154 +0,0 @@
-"""
-Test for Mock_LTI_Server
-"""
-from mock import Mock
-import unittest
-import threading
-import requests
-from mock_lti_server import MockLTIServer
-
-
-class MockLTIServerTest(unittest.TestCase):
- '''
- A mock version of the LTI provider server that listens on a local
- port and responds with pre-defined grade messages.
-
- Used for lettuce BDD tests in lms/courseware/features/lti.feature
- '''
-
- def setUp(self):
-
- # Create the server
- server_port = 8034
- server_host = 'localhost'
- address = (server_host, server_port)
- self.server = MockLTIServer(address)
- self.server.oauth_settings = {
- 'client_key': 'test_client_key',
- 'client_secret': 'test_client_secret',
- 'lti_base': 'http://{}:{}/'.format(server_host, server_port),
- 'lti_endpoint': 'correct_lti_endpoint'
- }
- self.server.run_inside_unittest_flag = True
- #flag for creating right callback_url
- self.server.test_mode = True
-
- self.server.server_host = server_host
- self.server.server_port = server_port
-
- # Start the server in a separate daemon thread
- server_thread = threading.Thread(target=self.server.serve_forever)
- server_thread.daemon = True
- server_thread.start()
-
- def tearDown(self):
-
- # Stop the server, freeing up the port
- self.server.shutdown()
-
-
- def test_wrong_header(self):
- """
- Tests that LTI server processes request with right program path but with wrong header.
- """
- #wrong number of params and no signature
- payload = {
- 'user_id': 'default_user_id',
- 'roles': 'Student',
- 'oauth_nonce': '',
- 'oauth_timestamp': '',
- }
- uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
- headers = {'referer': 'http://localhost:8000/'}
- response = requests.post(uri, data=payload, headers=headers)
- self.assertIn('Wrong LTI signature', response.content)
-
- def test_wrong_signature(self):
- """
- Tests that LTI server processes request with right program
- path and responses with incorrect signature.
- """
- payload = {
- 'user_id': 'default_user_id',
- 'roles': 'Student',
- 'oauth_nonce': '',
- 'oauth_timestamp': '',
- 'oauth_consumer_key': 'test_client_key',
- 'lti_version': 'LTI-1p0',
- 'oauth_signature_method': 'HMAC-SHA1',
- 'oauth_version': '1.0',
- 'oauth_signature': '',
- 'lti_message_type': 'basic-lti-launch-request',
- 'oauth_callback': 'about:blank',
- 'launch_presentation_return_url': '',
- 'lis_outcome_service_url': '',
- 'lis_result_sourcedid': '',
- 'resource_link_id':'',
- }
- uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
- headers = {'referer': 'http://localhost:8000/'}
- response = requests.post(uri, data=payload, headers=headers)
- self.assertIn('Wrong LTI signature', response.content)
-
-
- def test_success_response_launch_lti(self):
- """
- Success lti launch.
- """
- payload = {
- 'user_id': 'default_user_id',
- 'roles': 'Student',
- 'oauth_nonce': '',
- 'oauth_timestamp': '',
- 'oauth_consumer_key': 'test_client_key',
- 'lti_version': 'LTI-1p0',
- 'oauth_signature_method': 'HMAC-SHA1',
- 'oauth_version': '1.0',
- 'oauth_signature': '',
- 'lti_message_type': 'basic-lti-launch-request',
- 'oauth_callback': 'about:blank',
- 'launch_presentation_return_url': '',
- 'lis_outcome_service_url': '',
- 'lis_result_sourcedid': '',
- 'resource_link_id':'',
- }
- self.server.check_oauth_signature = Mock(return_value=True)
-
- uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
- headers = {'referer': 'http://localhost:8000/'}
- response = requests.post(uri, data=payload, headers=headers)
- self.assertIn('This is LTI tool. Success.', response.content)
-
- def test_send_graded_result(self):
-
- payload = {
- 'user_id': 'default_user_id',
- 'roles': 'Student',
- 'oauth_nonce': '',
- 'oauth_timestamp': '',
- 'oauth_consumer_key': 'test_client_key',
- 'lti_version': 'LTI-1p0',
- 'oauth_signature_method': 'HMAC-SHA1',
- 'oauth_version': '1.0',
- 'oauth_signature': '',
- 'lti_message_type': 'basic-lti-launch-request',
- 'oauth_callback': 'about:blank',
- 'launch_presentation_return_url': '',
- 'lis_outcome_service_url': '',
- 'lis_result_sourcedid': '',
- 'resource_link_id':'',
- }
- self.server.check_oauth_signature = Mock(return_value=True)
-
- uri = self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint']
- #this is the uri for sending grade from lti
- headers = {'referer': 'http://localhost:8000/'}
- response = requests.post(uri, data=payload, headers=headers)
- self.assertIn('This is LTI tool. Success.', response.content)
-
- self.server.grade_data['TC answer'] = "Test response"
- graded_response = requests.post('http://127.0.0.1:8034/grade')
- self.assertIn('Test response', graded_response.content)
-
-
-
diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py
index eb04684e7f85..adc89fbbbd11 100644
--- a/lms/djangoapps/courseware/tests/test_lti_integration.py
+++ b/lms/djangoapps/courseware/tests/test_lti_integration.py
@@ -31,7 +31,11 @@ def setUp(self):
module_id = unicode(urllib.quote(self.item_module.id))
user_id = unicode(self.item_descriptor.xmodule_runtime.anonymous_student_id)
- sourcedId = u':'.join(urllib.quote(i) for i in (lti_id, module_id, user_id))
+ sourcedId = "{id}:{resource_link}:{user_id}".format(
+ id=urllib.quote(lti_id),
+ resource_link=urllib.quote(module_id),
+ user_id=urllib.quote(user_id)
+ )
lis_outcome_service_url = 'https://{host}{path}'.format(
host=self.item_descriptor.xmodule_runtime.hostname,
diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py
index 875b98107806..5f059782adef 100644
--- a/lms/envs/acceptance.py
+++ b/lms/envs/acceptance.py
@@ -13,6 +13,7 @@
# You need to start the server in debug mode,
# otherwise the browser will not render the pages correctly
DEBUG = True
+SITE_NAME = 'localhost:{}'.format(LETTUCE_SERVER_PORT)
# Output Django logs to a file
import logging
diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py
index b13100ad3dcf..bfb25c226c7e 100644
--- a/lms/envs/devstack.py
+++ b/lms/envs/devstack.py
@@ -7,6 +7,7 @@
DEBUG = True
USE_I18N = True
TEMPLATE_DEBUG = True
+SITE_NAME = 'localhost:8000'
# By default don't use a worker, execute tasks as if they were local functions
CELERY_ALWAYS_EAGER = True