diff --git a/.gitignore b/.gitignore index ba6c570..6bffd30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # Custom test_*.sh +build.sh +deploy.sh +.pypirc # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 230d76f..c887645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ # Changelog -## 0.9.3 (2019-02-25) +## 0.9.4 (2020-03-23) +* #24 Add support for SSL + +## 0.9.3 (2020-02-25) * Fix incorrect release on pypi following jupytab split -## 0.9.2 (2019-02-24) +## 0.9.2 (2020-02-24) * #19 Create a tool library for jupytab, allowing separated download for jupytab util * #7 Jupytab must be install in kernel as no-deps * #16 notebook.js use parent path instead of same level path diff --git a/README.md b/README.md index e9a0f6a..af0a9fc 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ You need to create a `config.ini` file in order to tell Jupytab which notebooks listen_port = 8765 security_token = myToken notebooks = AirFlights|RealEstateCrime +ssl_enabled = True +ssl_key = /etc/pki/tls/certs/file.crt +ssl_cert = /etc/pki/tls/private/file.key [AirFlights] name = Air Flights @@ -112,6 +115,9 @@ There is only one mandatory section, `main`, which contains: and separated by the `|` (pipe) symbol. This must be a simple name compliant with [configparser](https://docs.python.org/3/library/configparser.html) sections. * `security_token` (optional): If provided, an encrypted security token will be required for all exchanges with Jupytab. +* `ssl_enabled` (optional): Enable or disable SSL +* `ssl_key` (mandatory if ssl_enabled is true): The path name of the server private key file +* `ssl_cert` (mandatory if ssl_enabled is true): The path name of the server public key certificate file Additional sections contain information about each notebook to be run: diff --git a/VERSION b/VERSION index b3ec163..2bd77c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.3 \ No newline at end of file +0.9.4 \ No newline at end of file diff --git a/jupytab-server/jupytab_server/jupytab.py b/jupytab-server/jupytab_server/jupytab.py index 577c9be..8f13226 100644 --- a/jupytab-server/jupytab_server/jupytab.py +++ b/jupytab-server/jupytab_server/jupytab.py @@ -11,9 +11,9 @@ from tornado.ioloop import IOLoop from tornado.web import StaticFileHandler, Application -from jupytab_server.kernel_executor import KernelExecutor from jupytab_server.jupytab_api import RestartHandler, APIHandler, ReverseProxyHandler, root, \ api_kernel, access_kernel, restart_kernel +from jupytab_server.kernel_executor import KernelExecutor logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) @@ -28,20 +28,18 @@ def __extract_item(config, notebook_key, item_key, default=None): raise ValueError(f'Expecting {item_key} in section {notebook_key}') -def parse_config(config_file): - if not os.path.isfile(config_file): - raise FileNotFoundError(f"missing configuration: {config_file}") +def config_listen_port(config): + return config.getint('main', 'listen_port') - config = ConfigParser() - config.optionxform = str - config.read(config_file) - listen_port = config.getint('main', 'listen_port') +def config_security_token(config): try: - security_token = config.get('main', 'security_token') + return config.get('main', 'security_token') except (NoSectionError, NoOptionError): - security_token = None + return None + +def config_notebooks(config): notebooks = config.get('main', 'notebooks') notebook_dict = {} @@ -56,14 +54,59 @@ def parse_config(config_file): 'description': nb_description, 'cwd': nb_cwd} + return notebook_dict + + +def config_ssl(config): + try: + ssl_enabled = config.getboolean('main', 'ssl_enabled') + except (NoSectionError, NoOptionError): + ssl_enabled = False + + if ssl_enabled: + ssl_cert = config.get('main', 'ssl_cert') + ssl_key = config.get('main', 'ssl_key') + + if ssl_enabled and not os.path.isfile(ssl_cert): + raise FileNotFoundError(f"SSL enabled but missing ssl_cert file: {ssl_cert}") + if ssl_enabled and not os.path.isfile(ssl_key): + raise FileNotFoundError(f"SSL enabled but missing ssl_key file: {ssl_key}") + + return { + "certfile": ssl_cert, + "keyfile": ssl_key + } + else: + return None + + +def parse_config(config_file): + if not os.path.isfile(config_file): + raise FileNotFoundError(f"missing configuration: {config_file}") + + config = ConfigParser() + config.optionxform = str + config.read(config_file) + + listen_port = config_listen_port(config) + security_token = config_security_token(config) + notebooks = config_notebooks(config) + ssl = config_ssl(config) + + if not ssl: + print("SSL not enabled") + else: + print("SSL enabled") + return { 'listen_port': listen_port, 'security_token': security_token, - 'notebooks': notebook_dict + 'notebooks': notebooks, + 'ssl': ssl } -def create_server_app(listen_port, security_token, notebooks): +def create_server_app(listen_port, security_token, notebooks, ssl): notebook_store = {} for key, value in notebooks.items(): @@ -72,14 +115,17 @@ def create_server_app(listen_port, security_token, notebooks): for key, value in notebook_store.items(): value.start() + protocol = "https" if ssl else "http" + if security_token: token_digest = hashlib.sha224(security_token.encode('utf-8')).hexdigest() print(f"""Your token is {token_digest} - Please open : http://{socket.gethostname()}:{listen_port}/?security_token={token_digest}""") + Please open : {protocol}://{socket.gethostname()}:{listen_port}""" + f"/?security_token={token_digest}") else: token_digest = None print(f"""You have no defined token. Please note your process is not secured ! - Please open : http://{socket.gethostname()}:{listen_port}""") + Please open : {protocol}://{socket.gethostname()}:{listen_port}""") server_app = Application([ (r"/" + api_kernel, APIHandler, @@ -105,7 +151,7 @@ def main(input_args=None): app = create_server_app(**params) - app.listen(params['listen_port']) + app.listen(params['listen_port'], ssl_options=params['ssl']) IOLoop.instance().start() diff --git a/jupytab-server/tests/config/example.cert b/jupytab-server/tests/config/example.cert new file mode 100644 index 0000000..4365fd2 --- /dev/null +++ b/jupytab-server/tests/config/example.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe+gAwIBAgIJAK8X55pvCWVBMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV +BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMDAzMjMxMTM0MjhaFw0zMDAzMjExMTM0 +MjhaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAOoF0nX1ZfQ686G+G7jexm9pz39GRYnlPRg5g/oj9MJl +fpvkWdh3bBEmitsOpZDV8AgHKJlZD6rR6DX9w+Ay0g/QCd9lWKlVNusod8qyu2m2 +BM+oFbgex7JKObnd+CZq6Fe5/lWGDKqDkrTTijJes8QfkySEgevdYRUekgCT+ydI +nldZHRnIdckRp3LBFoGdIQkZqtVKht+ECQotSeACW6EE4a88cY+ezupi6AG2I3kM +Bm4UCUdD6x/q4ia6Cj7PKlg0jENdYBaOb6ZyPMKtyzYOikAWUrmA4txFc0uavgpZ +rIspgnR/alZMiVjUPf4l/23cac/epD6li1aQJi0sU40CAwEAAaNQME4wHQYDVR0O +BBYEFMIRQx8EBLmtn8sFy/XM3Ku23fzAMB8GA1UdIwQYMBaAFMIRQx8EBLmtn8sF +y/XM3Ku23fzAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAOeLHtKp +mzjLwzY1HZiLF/xKLYAeSNfQaFEso1BVLezswmMqh5UPQoWtSY6SexqkbGwewt16 +oYgI703Zs8SqXmBXTvVPbZ+GKviFat7sWCTZmQHBDjr46XZqbYvc+hbHNsl5Gu00 +uujR9GjYnogtvjARWmwLNRp0XCKOytGd2/ggnHV4o/92HoxdFBRp0qcpQJtu9gkd +C/f7ZEx5vtnR70a+aQ4CpMdhoFJTf5pb5iwoHDn3vm0N1yZicKT0IXbEs4FcBAiU +T6bc06cAnTzeIC/lbmTfcrpHw3J3JuG/Af3X1iIptJuYM0kcJOXPd2VU3hFu8xPW +qCUBbTRSF+z6fxk= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/jupytab-server/tests/config/example.key b/jupytab-server/tests/config/example.key new file mode 100644 index 0000000..4f1b774 --- /dev/null +++ b/jupytab-server/tests/config/example.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA6gXSdfVl9Drzob4buN7Gb2nPf0ZFieU9GDmD+iP0wmV+m+RZ +2HdsESaK2w6lkNXwCAcomVkPqtHoNf3D4DLSD9AJ32VYqVU26yh3yrK7abYEz6gV +uB7Hsko5ud34JmroV7n+VYYMqoOStNOKMl6zxB+TJISB691hFR6SAJP7J0ieV1kd +Gch1yRGncsEWgZ0hCRmq1UqG34QJCi1J4AJboQThrzxxj57O6mLoAbYjeQwGbhQJ +R0PrH+riJroKPs8qWDSMQ11gFo5vpnI8wq3LNg6KQBZSuYDi3EVzS5q+ClmsiymC +dH9qVkyJWNQ9/iX/bdxpz96kPqWLVpAmLSxTjQIDAQABAoIBAQCCzcRICGT3MOgy +VIc8OtChP3wqQIXnwIj4fFVnQCezbHVq/yS02HM/1tIwBKzIGrwyUIYByIT4TqFD +ZFbSfrVo/zg1dHktFKNAp3rlgic8u+9Ofj29jv7BiblgSVBFcOXy+tPMy8NSn34l +skOBSeuiyJ8+/w17X16/JjonNo9f8aYvB3HrucemHb5l3pN01G1PyTte742E8L+9 +MAKeMobrbbkSdfNXS8OhPTKmAV3VmVr47IkZL4DAa2K1uHTI6Zi2drx23q5eOe6v +2T6e7Kj7pPNvUgUb2N3CGr6ATBtHT+uoxNYmLEJVLCSCxjbnXgOlpyB1RxtxSt5s +93s2yd8FAoGBAP7lVk+zd/Rk4VwOpFEXC15kvKn3r2Iq3VWwFPOQ4s7WsGbN4jBd +ZN53OMM0LeeRFqLOhqYD9xkV/XoA33wXdeVnv+pd+DXEtciPh2zdbnSti8kipqMN +WFxqSGtGyCc0P1foOvkVa2r+p9ihnhTW6k8e03wWNSZ9py8+X0g0vrFfAoGBAOsJ +VopQkzH3akLwArx9Lk3o4gRBAzxS4Ok4e0iIdr02/AkatT2oggcAid4ckPpW0VEh +IbT9XicVc10cso5P1ozCyCdjbUcFyX63wr46ItwspyEqJTgyL6gEzwj2Rmoc/nXQ +Rwbr/JpeKQ6Q/iRq+zuLlfja868wRWo7zjbpo8aTAoGBALFpkLSytqg9WvoHGulx +/7C4rvQieEj8isesYjjRPHw4w9kaLff52U5abwC3HchSnQ2+b8u3cNJeEupLF0I4 +1g9RMiv/MdbCzsAE3n6wdMPzUxsw6gkNLdZNB5DbWE6pN/mIoxthhD2Zd9v5SZ05 +pSZiz1JL5ryesrHYWNtaEuxDAoGBAIxReN7+l8Ie6cuoqpmJSpmszTKo9ZuQB0J1 +O/Tjs6/nIbT1wvpanbY8dhKqj0tFhZWf6BW7pfhDcCpItbkMpRRIPWJ2k4jxRYhn +gNY8sw8rgWPlW28fVyBCLrA1B3jWcnw3qg/R1275hB10JqXrUK4N+a0mWpFeijKQ +Hd7ewa4NAoGAXsZSWXG8etwXTc8/i4f0oNItGxIcHdyYJaLTxjDHWyXbbd0rkyhU +PPenULrwLRBpTU5BytLpWjnJLmEj4hbSrFnr9M/o3O8ccBo5ghYQJZYfO/k5ylp3 +liQNI7MYUdZLF0D0csouFMnRn6kA2zGkaaImH+EnIEzZxO1XSLJExfY= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/jupytab-server/tests/config/ssl_bad_file.ini b/jupytab-server/tests/config/ssl_bad_file.ini new file mode 100644 index 0000000..eee4736 --- /dev/null +++ b/jupytab-server/tests/config/ssl_bad_file.ini @@ -0,0 +1,4 @@ +[main] +ssl_enabled = true +ssl_key = config/eXample.key +ssl_cert = config/eXample.cert \ No newline at end of file diff --git a/jupytab-server/tests/config/ssl_disabled.ini b/jupytab-server/tests/config/ssl_disabled.ini new file mode 100644 index 0000000..38cd08f --- /dev/null +++ b/jupytab-server/tests/config/ssl_disabled.ini @@ -0,0 +1,4 @@ +[main] +ssl_enabled = false +ssl_key = config/example.key +ssl_cert = config/example.cert \ No newline at end of file diff --git a/jupytab-server/tests/config/ssl_none.ini b/jupytab-server/tests/config/ssl_none.ini new file mode 100644 index 0000000..9280d27 --- /dev/null +++ b/jupytab-server/tests/config/ssl_none.ini @@ -0,0 +1 @@ +[main] \ No newline at end of file diff --git a/jupytab-server/tests/config/ssl_ok.ini b/jupytab-server/tests/config/ssl_ok.ini new file mode 100644 index 0000000..c577e4d --- /dev/null +++ b/jupytab-server/tests/config/ssl_ok.ini @@ -0,0 +1,4 @@ +[main] +ssl_enabled = true +ssl_key = config/example.key +ssl_cert = config/example.cert \ No newline at end of file diff --git a/jupytab-server/tests/test_config.py b/jupytab-server/tests/test_config.py new file mode 100644 index 0000000..4d00422 --- /dev/null +++ b/jupytab-server/tests/test_config.py @@ -0,0 +1,25 @@ +import os +from configparser import ConfigParser + +import pytest + +from jupytab_server.jupytab import config_ssl + +dir_path = os.path.dirname(os.path.realpath(__file__)) +os.chdir(dir_path) + + +def config(config_file): + config = ConfigParser() + config.optionxform = str + config.read(config_file) + return config + + +def test_ssl(): + with pytest.raises(FileNotFoundError): + config_ssl(config('config/ssl_bad_file.ini')) + assert config_ssl(config('config/ssl_disabled.ini')) is None + assert config_ssl(config('config/ssl_none.ini')) is None + assert config_ssl(config('config/ssl_ok.ini')) == {'certfile': 'config/example.cert', + 'keyfile': 'config/example.key'}