Skip to content

Commit

Permalink
Add SSL configuration for jupytab (#26)
Browse files Browse the repository at this point in the history
SSL configuration for jupytab
  • Loading branch information
btribonde authored Mar 23, 2020
1 parent 07d61f3 commit f420ba1
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Custom
test_*.sh
build.sh
deploy.sh
.pypirc

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.3
0.9.4
76 changes: 61 additions & 15 deletions jupytab-server/jupytab_server/jupytab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 = {}

Expand All @@ -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():
Expand All @@ -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,
Expand All @@ -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()

Expand Down
19 changes: 19 additions & 0 deletions jupytab-server/tests/config/example.cert
Original file line number Diff line number Diff line change
@@ -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-----
27 changes: 27 additions & 0 deletions jupytab-server/tests/config/example.key
Original file line number Diff line number Diff line change
@@ -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-----
4 changes: 4 additions & 0 deletions jupytab-server/tests/config/ssl_bad_file.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[main]
ssl_enabled = true
ssl_key = config/eXample.key
ssl_cert = config/eXample.cert
4 changes: 4 additions & 0 deletions jupytab-server/tests/config/ssl_disabled.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[main]
ssl_enabled = false
ssl_key = config/example.key
ssl_cert = config/example.cert
1 change: 1 addition & 0 deletions jupytab-server/tests/config/ssl_none.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[main]
4 changes: 4 additions & 0 deletions jupytab-server/tests/config/ssl_ok.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[main]
ssl_enabled = true
ssl_key = config/example.key
ssl_cert = config/example.cert
25 changes: 25 additions & 0 deletions jupytab-server/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -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'}

0 comments on commit f420ba1

Please sign in to comment.