diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index ae58e7ac8..fa58c2b25 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -32,6 +32,8 @@ from IPython.utils.path import filefind from IPython.utils.py3compat import string_types from IPython.html.utils import is_hidden, url_path_join, url_escape +from IPython.html.services.security import csp_report_uri + #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -45,17 +47,22 @@ class AuthenticatedHandler(web.RequestHandler): def set_default_headers(self): headers = self.settings.get('headers', {}) - if "X-Frame-Options" not in headers: - headers["X-Frame-Options"] = "SAMEORIGIN" + if "Content-Security-Policy" not in headers: + headers["Content-Security-Policy"] = ( + "frame-ancestors 'self'; " + # Make sure the report-uri is relative to the base_url + "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";" + ) + # Allow for overriding headers for header_name,value in headers.items() : try: self.set_header(header_name, value) - except Exception: + except Exception as e: # tornado raise Exception (not a subclass) # if method is unsupported (websocket and Access-Control-Allow-Origin # for example, so just ignore) - pass + self.log.debug(e) def clear_login_cookie(self): self.clear_cookie(self.cookie_name) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index e21db1aea..24d18e89f 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -225,7 +225,7 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('services.sessions.handlers')) handlers.extend(load_handlers('services.nbconvert.handlers')) handlers.extend(load_handlers('services.kernelspecs.handlers')) - + handlers.extend(load_handlers('services.security.handlers')) handlers.append( (r"/nbextensions/(.*)", FileFindHandler, { 'path': settings['nbextensions_path'], diff --git a/IPython/html/services/config/__init__.py b/IPython/html/services/config/__init__.py index e69de29bb..d8d938020 100644 --- a/IPython/html/services/config/__init__.py +++ b/IPython/html/services/config/__init__.py @@ -0,0 +1 @@ +from .manager import ConfigManager diff --git a/IPython/html/services/kernels/tests/test_kernels_api.py b/IPython/html/services/kernels/tests/test_kernels_api.py index 8f29a07f6..b33142c92 100644 --- a/IPython/html/services/kernels/tests/test_kernels_api.py +++ b/IPython/html/services/kernels/tests/test_kernels_api.py @@ -65,7 +65,10 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.status_code, 201) self.assertIsInstance(kern1, dict) - self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN") + self.assertEqual(r.headers['Content-Security-Policy'], ( + "frame-ancestors 'self'; " + "report-uri /api/security/csp-report;" + )) def test_main_kernel_handler(self): # POST request @@ -75,7 +78,10 @@ class KernelAPITest(NotebookTestBase): self.assertEqual(r.status_code, 201) self.assertIsInstance(kern1, dict) - self.assertEqual(r.headers['x-frame-options'], "SAMEORIGIN") + self.assertEqual(r.headers['Content-Security-Policy'], ( + "frame-ancestors 'self'; " + "report-uri /api/security/csp-report;" + )) # GET request r = self.kern_api.list() diff --git a/IPython/html/services/security/__init__.py b/IPython/html/services/security/__init__.py new file mode 100644 index 000000000..9cf0d476b --- /dev/null +++ b/IPython/html/services/security/__init__.py @@ -0,0 +1,4 @@ +# URI for the CSP Report. Included here to prevent a cyclic dependency. +# csp_report_uri is needed both by the BaseHandler (for setting the report-uri) +# and by the CSPReportHandler (which depends on the BaseHandler). +csp_report_uri = r"/api/security/csp-report" diff --git a/IPython/html/services/security/handlers.py b/IPython/html/services/security/handlers.py new file mode 100644 index 000000000..18f7874cd --- /dev/null +++ b/IPython/html/services/security/handlers.py @@ -0,0 +1,23 @@ +"""Tornado handlers for security logging.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import gen, web + +from ...base.handlers import IPythonHandler, json_errors +from . import csp_report_uri + +class CSPReportHandler(IPythonHandler): + '''Accepts a content security policy violation report''' + @web.authenticated + @json_errors + def post(self): + '''Log a content security policy violation report''' + csp_report = self.get_json_body() + self.log.warn("Content security violation: %s", + self.request.body.decode('utf8', 'replace')) + +default_handlers = [ + (csp_report_uri, CSPReportHandler) +] diff --git a/IPython/html/static/services/kernels/kernel.js b/IPython/html/static/services/kernels/kernel.js index 28b77ca47..dba13157e 100644 --- a/IPython/html/static/services/kernels/kernel.js +++ b/IPython/html/static/services/kernels/kernel.js @@ -286,16 +286,16 @@ define([ }); }; - /** - * POST /api/kernels/[:kernel_id]/restart - * - * Restart the kernel. - * - * @function interrupt - * @param {function} [success] - function executed on ajax success - * @param {function} [error] - functon executed on ajax error - */ Kernel.prototype.restart = function (success, error) { + /** + * POST /api/kernels/[:kernel_id]/restart + * + * Restart the kernel. + * + * @function interrupt + * @param {function} [success] - function executed on ajax success + * @param {function} [error] - functon executed on ajax error + */ this.events.trigger('kernel_restarting.Kernel', {kernel: this}); this.stop_channels(); @@ -327,14 +327,14 @@ define([ }); }; - /** - * Reconnect to a disconnected kernel. This is not actually a - * standard HTTP request, but useful function nonetheless for - * reconnecting to the kernel if the connection is somehow lost. - * - * @function reconnect - */ Kernel.prototype.reconnect = function () { + /** + * Reconnect to a disconnected kernel. This is not actually a + * standard HTTP request, but useful function nonetheless for + * reconnecting to the kernel if the connection is somehow lost. + * + * @function reconnect + */ if (this.is_connected()) { return; } @@ -346,15 +346,15 @@ define([ this.start_channels(); }; - /** - * Handle a successful AJAX request by updating the kernel id and - * name from the response, and then optionally calling a provided - * callback. - * - * @function _on_success - * @param {function} success - callback - */ Kernel.prototype._on_success = function (success) { + /** + * Handle a successful AJAX request by updating the kernel id and + * name from the response, and then optionally calling a provided + * callback. + * + * @function _on_success + * @param {function} success - callback + */ var that = this; return function (data, status, xhr) { if (data) { @@ -368,14 +368,14 @@ define([ }; }; - /** - * Handle a failed AJAX request by logging the error message, and - * then optionally calling a provided callback. - * - * @function _on_error - * @param {function} error - callback - */ Kernel.prototype._on_error = function (error) { + /** + * Handle a failed AJAX request by logging the error message, and + * then optionally calling a provided callback. + * + * @function _on_error + * @param {function} error - callback + */ return function (xhr, status, err) { utils.log_ajax_error(xhr, status, err); if (error) { @@ -384,27 +384,27 @@ define([ }; }; - /** - * Perform necessary tasks once the kernel has been started, - * including actually connecting to the kernel. - * - * @function _kernel_created - * @param {Object} data - information about the kernel including id - */ Kernel.prototype._kernel_created = function (data) { + /** + * Perform necessary tasks once the kernel has been started, + * including actually connecting to the kernel. + * + * @function _kernel_created + * @param {Object} data - information about the kernel including id + */ this.id = data.id; this.kernel_url = utils.url_join_encode(this.kernel_service_url, this.id); this.start_channels(); }; - /** - * Perform necessary tasks once the connection to the kernel has - * been established. This includes requesting information about - * the kernel. - * - * @function _kernel_connected - */ Kernel.prototype._kernel_connected = function () { + /** + * Perform necessary tasks once the connection to the kernel has + * been established. This includes requesting information about + * the kernel. + * + * @function _kernel_connected + */ this.events.trigger('kernel_connected.Kernel', {kernel: this}); this.events.trigger('kernel_starting.Kernel', {kernel: this}); // get kernel info so we know what state the kernel is in @@ -415,24 +415,24 @@ define([ }); }; - /** - * Perform necessary tasks after the kernel has died. This closing - * communication channels to the kernel if they are still somehow - * open. - * - * @function _kernel_dead - */ Kernel.prototype._kernel_dead = function () { + /** + * Perform necessary tasks after the kernel has died. This closing + * communication channels to the kernel if they are still somehow + * open. + * + * @function _kernel_dead + */ this.stop_channels(); }; - /** - * Start the `shell`and `iopub` channels. - * Will stop and restart them if they already exist. - * - * @function start_channels - */ Kernel.prototype.start_channels = function () { + /** + * Start the `shell`and `iopub` channels. + * Will stop and restart them if they already exist. + * + * @function start_channels + */ var that = this; this.stop_channels(); var ws_host_url = this.ws_url + this.kernel_url; @@ -506,29 +506,29 @@ define([ this.channels.stdin.onmessage = $.proxy(this._handle_input_request, this); }; - /** - * Handle a websocket entering the open state, - * signaling that the kernel is connected when all channels are open. - * - * @function _ws_opened - */ Kernel.prototype._ws_opened = function (evt) { + /** + * Handle a websocket entering the open state, + * signaling that the kernel is connected when all channels are open. + * + * @function _ws_opened + */ if (this.is_connected()) { // all events ready, trigger started event. this._kernel_connected(); } }; - /** - * Handle a websocket entering the closed state. This closes the - * other communication channels if they are open. If the websocket - * was not closed due to an error, try to reconnect to the kernel. - * - * @function _ws_closed - * @param {string} ws_url - the websocket url - * @param {bool} error - whether the connection was closed due to an error - */ Kernel.prototype._ws_closed = function(ws_url, error) { + /** + * Handle a websocket entering the closed state. This closes the + * other communication channels if they are open. If the websocket + * was not closed due to an error, try to reconnect to the kernel. + * + * @function _ws_closed + * @param {string} ws_url - the websocket url + * @param {bool} error - whether the connection was closed due to an error + */ this.stop_channels(); this.events.trigger('kernel_disconnected.Kernel', {kernel: this}); @@ -555,13 +555,13 @@ define([ } }; - /** - * Close the websocket channels. After successful close, the value - * in `this.channels[channel_name]` will be null. - * - * @function stop_channels - */ Kernel.prototype.stop_channels = function () { + /** + * Close the websocket channels. After successful close, the value + * in `this.channels[channel_name]` will be null. + * + * @function stop_channels + */ var that = this; var close = function (c) { return function () { @@ -582,15 +582,15 @@ define([ } }; - /** - * Check whether there is a connection to the kernel. This - * function only returns true if all channel objects have been - * created and have a state of WebSocket.OPEN. - * - * @function is_connected - * @returns {bool} - whether there is a connection - */ Kernel.prototype.is_connected = function () { + /** + * Check whether there is a connection to the kernel. This + * function only returns true if all channel objects have been + * created and have a state of WebSocket.OPEN. + * + * @function is_connected + * @returns {bool} - whether there is a connection + */ for (var c in this.channels) { // if any channel is not ready, then we're not connected if (this.channels[c] === null) { @@ -603,15 +603,15 @@ define([ return true; }; - /** - * Check whether the connection to the kernel has been completely - * severed. This function only returns true if all channel objects - * are null. - * - * @function is_fully_disconnected - * @returns {bool} - whether the kernel is fully disconnected - */ Kernel.prototype.is_fully_disconnected = function () { + /** + * Check whether the connection to the kernel has been completely + * severed. This function only returns true if all channel objects + * are null. + * + * @function is_fully_disconnected + * @returns {bool} - whether the kernel is fully disconnected + */ for (var c in this.channels) { if (this.channels[c] === null) { return true; @@ -620,12 +620,12 @@ define([ return false; }; - /** - * Send a message on the Kernel's shell channel - * - * @function send_shell_message - */ Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) { + /** + * Send a message on the Kernel's shell channel + * + * @function send_shell_message + */ if (!this.is_connected()) { throw new Error("kernel is not connected"); } @@ -635,17 +635,17 @@ define([ return msg.header.msg_id; }; - /** - * Get kernel info - * - * @function kernel_info - * @param callback {function} - * - * When calling this method, pass a callback function that expects one argument. - * The callback will be passed the complete `kernel_info_reply` message documented - * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info) - */ Kernel.prototype.kernel_info = function (callback) { + /** + * Get kernel info + * + * @function kernel_info + * @param callback {function} + * + * When calling this method, pass a callback function that expects one argument. + * The callback will be passed the complete `kernel_info_reply` message documented + * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info) + */ var callbacks; if (callback) { callbacks = { shell : { reply : callback } }; @@ -653,19 +653,19 @@ define([ return this.send_shell_message("kernel_info_request", {}, callbacks); }; - /** - * Get info on an object - * - * When calling this method, pass a callback function that expects one argument. - * The callback will be passed the complete `inspect_reply` message documented - * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information) - * - * @function inspect - * @param code {string} - * @param cursor_pos {integer} - * @param callback {function} - */ Kernel.prototype.inspect = function (code, cursor_pos, callback) { + /** + * Get info on an object + * + * When calling this method, pass a callback function that expects one argument. + * The callback will be passed the complete `inspect_reply` message documented + * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information) + * + * @function inspect + * @param code {string} + * @param cursor_pos {integer} + * @param callback {function} + */ var callbacks; if (callback) { callbacks = { shell : { reply : callback } }; @@ -679,56 +679,56 @@ define([ return this.send_shell_message("inspect_request", content, callbacks); }; - /** - * Execute given code into kernel, and pass result to callback. - * - * @async - * @function execute - * @param {string} code - * @param [callbacks] {Object} With the following keys (all optional) - * @param callbacks.shell.reply {function} - * @param callbacks.shell.payload.[payload_name] {function} - * @param callbacks.iopub.output {function} - * @param callbacks.iopub.clear_output {function} - * @param callbacks.input {function} - * @param {object} [options] - * @param [options.silent=false] {Boolean} - * @param [options.user_expressions=empty_dict] {Dict} - * @param [options.allow_stdin=false] {Boolean} true|false - * - * @example - * - * The options object should contain the options for the execute - * call. Its default values are: - * - * options = { - * silent : true, - * user_expressions : {}, - * allow_stdin : false - * } - * - * When calling this method pass a callbacks structure of the - * form: - * - * callbacks = { - * shell : { - * reply : execute_reply_callback, - * payload : { - * set_next_input : set_next_input_callback, - * } - * }, - * iopub : { - * output : output_callback, - * clear_output : clear_output_callback, - * }, - * input : raw_input_callback - * } - * - * Each callback will be passed the entire message as a single - * arugment. Payload handlers will be passed the corresponding - * payload and the execute_reply message. - */ Kernel.prototype.execute = function (code, callbacks, options) { + /** + * Execute given code into kernel, and pass result to callback. + * + * @async + * @function execute + * @param {string} code + * @param [callbacks] {Object} With the following keys (all optional) + * @param callbacks.shell.reply {function} + * @param callbacks.shell.payload.[payload_name] {function} + * @param callbacks.iopub.output {function} + * @param callbacks.iopub.clear_output {function} + * @param callbacks.input {function} + * @param {object} [options] + * @param [options.silent=false] {Boolean} + * @param [options.user_expressions=empty_dict] {Dict} + * @param [options.allow_stdin=false] {Boolean} true|false + * + * @example + * + * The options object should contain the options for the execute + * call. Its default values are: + * + * options = { + * silent : true, + * user_expressions : {}, + * allow_stdin : false + * } + * + * When calling this method pass a callbacks structure of the + * form: + * + * callbacks = { + * shell : { + * reply : execute_reply_callback, + * payload : { + * set_next_input : set_next_input_callback, + * } + * }, + * iopub : { + * output : output_callback, + * clear_output : clear_output_callback, + * }, + * input : raw_input_callback + * } + * + * Each callback will be passed the entire message as a single + * arugment. Payload handlers will be passed the corresponding + * payload and the execute_reply message. + */ var content = { code : code, silent : true, diff --git a/IPython/html/static/widgets/js/manager.js b/IPython/html/static/widgets/js/manager.js index e7ee68653..c0f17fbdb 100644 --- a/IPython/html/static/widgets/js/manager.js +++ b/IPython/html/static/widgets/js/manager.js @@ -101,8 +101,7 @@ define([ var parameters = {model: model, options: options}; var view = new ViewType(parameters); view.listenTo(model, 'destroy', view.remove); - view.render(); - return view; + return Promise.resolve(view.render()).then(function() {return view;}); }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true)); }); return model.state_change;