mopidy/tests/http/test_server.py
Nick Steel 10fafc0228 http: Add config option to control CSRF protection (Fixes: #1713)
Allows users to disable CSRF protection and revert to the HTTP server's
previous (less secure) behaviour. Users are advised to leave this config
value enabled if possible. However, if disabled this will:
  * Remove the requirement to set a ``Content-Type: application/json``
    header for JSON-RPC POST requests.
  * Disable all same-origin checks, effectively ignoring the ``allowed_origins``
    config since requests from any origin will be allowed.
  * Suppress all ``Access-Control-Allow-*`` response headers.
2018-10-15 17:24:45 +01:00

337 lines
11 KiB
Python

from __future__ import absolute_import, unicode_literals
import os
import mock
import tornado.testing
import tornado.wsgi
import mopidy
from mopidy.http import actor, handlers
class HttpServerTest(tornado.testing.AsyncHTTPTestCase):
def get_config(self):
return {
'http': {
'hostname': '127.0.0.1',
'port': 6680,
'static_dir': None,
'zeroconf': '',
'allowed_origins': [],
'csrf_protection': True,
}
}
def get_app(self):
core = mock.Mock()
core.get_version = mock.MagicMock(name='get_version')
core.get_version.return_value = mopidy.__version__
testapps = [dict(name='testapp')]
teststatics = [dict(name='teststatic')]
apps = [{
'name': 'mopidy',
'factory': handlers.make_mopidy_app_factory(testapps, teststatics),
}]
http_server = actor.HttpServer(
config=self.get_config(), core=core, sockets=[],
apps=apps, statics=[])
return tornado.web.Application(http_server._get_request_handlers())
class RootRedirectTest(HttpServerTest):
def test_should_redirect_to_mopidy_app(self):
response = self.fetch('/', method='GET', follow_redirects=False)
self.assertEqual(response.code, 302)
self.assertEqual(response.headers['Location'], '/mopidy/')
class LegacyStaticDirAppTest(HttpServerTest):
def get_config(self):
config = super(LegacyStaticDirAppTest, self).get_config()
config['http']['static_dir'] = os.path.dirname(__file__)
return config
def test_should_return_index(self):
response = self.fetch('/', method='GET', follow_redirects=False)
self.assertEqual(response.code, 404, 'No index.html in this dir')
def test_should_return_static_files(self):
response = self.fetch('/test_server.py', method='GET')
self.assertIn(
'test_should_return_static_files',
tornado.escape.to_unicode(response.body))
self.assertEqual(
response.headers['X-Mopidy-Version'], mopidy.__version__)
self.assertEqual(response.headers['Cache-Control'], 'no-cache')
class MopidyAppTest(HttpServerTest):
def test_should_return_index(self):
response = self.fetch('/mopidy/', method='GET')
body = tornado.escape.to_unicode(response.body)
self.assertIn(
'This web server is a part of the Mopidy music server.', body)
self.assertIn('testapp', body)
self.assertIn('teststatic', body)
self.assertEqual(
response.headers['X-Mopidy-Version'], mopidy.__version__)
self.assertEqual(response.headers['Cache-Control'], 'no-cache')
def test_without_slash_should_redirect(self):
response = self.fetch('/mopidy', method='GET', follow_redirects=False)
self.assertEqual(response.code, 301)
self.assertEqual(response.headers['Location'], '/mopidy/')
def test_should_return_static_files(self):
response = self.fetch('/mopidy/mopidy.js', method='GET')
self.assertIn(
'function Mopidy',
tornado.escape.to_unicode(response.body))
self.assertEqual(
response.headers['X-Mopidy-Version'], mopidy.__version__)
self.assertEqual(response.headers['Cache-Control'], 'no-cache')
class MopidyWebSocketHandlerTest(HttpServerTest):
def test_should_return_ws(self):
response = self.fetch('/mopidy/ws', method='GET')
self.assertEqual(
'Can "Upgrade" only to "WebSocket".',
tornado.escape.to_unicode(response.body))
def test_should_return_ws_old(self):
response = self.fetch('/mopidy/ws/', method='GET')
self.assertEqual(
'Can "Upgrade" only to "WebSocket".',
tornado.escape.to_unicode(response.body))
class MopidyRPCHandlerTest(HttpServerTest):
def test_should_return_rpc_error(self):
cmd = tornado.escape.json_encode({'action': 'get_version'})
response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
'Content-Type': 'application/json'})
self.assertEqual(
{'jsonrpc': '2.0', 'id': None, 'error':
{'message': 'Invalid Request', 'code': -32600,
'data': '"jsonrpc" member must be included'}},
tornado.escape.json_decode(response.body))
def test_should_return_parse_error(self):
cmd = '{[[[]}'
response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
'Content-Type': 'application/json'})
self.assertEqual(
{'jsonrpc': '2.0', 'id': None, 'error':
{'message': 'Parse error', 'code': -32700}},
tornado.escape.json_decode(response.body))
def test_should_return_mopidy_version(self):
cmd = tornado.escape.json_encode({
'method': 'core.get_version',
'params': [],
'jsonrpc': '2.0',
'id': 1,
})
response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
'Content-Type': 'application/json'})
self.assertEqual(
{'jsonrpc': '2.0', 'id': 1, 'result': mopidy.__version__},
tornado.escape.json_decode(response.body))
def test_should_return_extra_headers(self):
response = self.fetch('/mopidy/rpc', method='HEAD')
self.assertIn('Accept', response.headers)
self.assertIn('X-Mopidy-Version', response.headers)
self.assertIn('Cache-Control', response.headers)
self.assertIn('Content-Type', response.headers)
def test_should_require_correct_content_type(self):
cmd = tornado.escape.json_encode({
'method': 'core.get_version',
'params': [],
'jsonrpc': '2.0',
'id': 1,
})
response = self.fetch('/mopidy/rpc', method='POST', body=cmd, headers={
'Content-Type': 'text/plain'})
self.assertEqual(response.code, 415)
self.assertEqual(
response.reason, 'Content-Type must be application/json')
def test_different_origin_returns_access_denied(self):
response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
'Host': 'me:6680', 'Origin': 'http://evil:666'})
self.assertEqual(response.code, 403)
self.assertEqual(
response.reason, 'Access denied for origin http://evil:666')
def test_same_origin_returns_cors_headers(self):
response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
'Host': 'me:6680', 'Origin': 'http://me:6680'})
self.assertEqual(
response.headers['Access-Control-Allow-Origin'], 'http://me:6680')
self.assertEqual(
response.headers['Access-Control-Allow-Headers'], 'Content-Type')
class MopidyRPCHandlerNoCSRFProtectionTest(HttpServerTest):
def get_config(self):
config = super(MopidyRPCHandlerNoCSRFProtectionTest, self).get_config()
config['http']['csrf_protection'] = False
return config
def get_cmd(self):
return tornado.escape.json_encode({
'method': 'core.get_version',
'params': [],
'jsonrpc': '2.0',
'id': 1,
})
def test_should_ignore_incorrect_content_type(self):
response = self.fetch(
'/mopidy/rpc', method='POST', body=self.get_cmd(),
headers={'Content-Type': 'text/plain'})
self.assertEqual(response.code, 200)
def test_should_ignore_missing_content_type(self):
response = self.fetch(
'/mopidy/rpc', method='POST', body=self.get_cmd(), headers={})
self.assertEqual(response.code, 200)
def test_different_origin_returns_allowed(self):
response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
'Host': 'me:6680', 'Origin': 'http://evil:666'})
self.assertEqual(response.code, 204)
def test_should_not_return_cors_headers(self):
response = self.fetch('/mopidy/rpc', method='OPTIONS', headers={
'Host': 'me:6680', 'Origin': 'http://me:6680'})
self.assertNotIn('Access-Control-Allow-Origin', response.headers)
self.assertNotIn('Access-Control-Allow-Headers', response.headers)
class HttpServerWithStaticFilesTest(tornado.testing.AsyncHTTPTestCase):
def get_app(self):
config = {
'http': {
'hostname': '127.0.0.1',
'port': 6680,
'static_dir': None,
'zeroconf': '',
}
}
core = mock.Mock()
statics = [dict(name='static', path=os.path.dirname(__file__))]
http_server = actor.HttpServer(
config=config, core=core, sockets=[], apps=[], statics=statics)
return tornado.web.Application(http_server._get_request_handlers())
def test_without_slash_should_redirect(self):
response = self.fetch('/static', method='GET', follow_redirects=False)
self.assertEqual(response.code, 301)
self.assertEqual(response.headers['Location'], '/static/')
def test_can_serve_static_files(self):
response = self.fetch('/static/test_server.py', method='GET')
self.assertEqual(200, response.code)
self.assertEqual(
response.headers['X-Mopidy-Version'], mopidy.__version__)
self.assertEqual(
response.headers['Cache-Control'], 'no-cache')
def wsgi_app_factory(config, core):
def wsgi_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return ['Hello, world!\n']
return [
('(.*)', tornado.web.FallbackHandler, {
'fallback': tornado.wsgi.WSGIContainer(wsgi_app),
}),
]
class HttpServerWithWsgiAppTest(tornado.testing.AsyncHTTPTestCase):
def get_app(self):
config = {
'http': {
'hostname': '127.0.0.1',
'port': 6680,
'static_dir': None,
'zeroconf': '',
}
}
core = mock.Mock()
apps = [{
'name': 'wsgi',
'factory': wsgi_app_factory,
}]
http_server = actor.HttpServer(
config=config, core=core, sockets=[], apps=apps, statics=[])
return tornado.web.Application(http_server._get_request_handlers())
def test_without_slash_should_redirect(self):
response = self.fetch('/wsgi', method='GET', follow_redirects=False)
self.assertEqual(response.code, 301)
self.assertEqual(response.headers['Location'], '/wsgi/')
def test_can_wrap_wsgi_apps(self):
response = self.fetch('/wsgi/', method='GET')
self.assertEqual(200, response.code)
self.assertIn(
'Hello, world!', tornado.escape.to_unicode(response.body))