Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for HTTPS protocol #307

Merged
merged 12 commits into from Dec 10, 2016
2 changes: 2 additions & 0 deletions flexx/_config.py
Expand Up @@ -7,4 +7,6 @@
webruntime=('', str, 'The default web runtime to use. Default is xul/browser.'),
ws_timeout=(20, int, 'If the websocket is idle for this amount of seconds, '
'it is closed.'),
ssl_certfile=('', str, 'cert file for https server.'),
ssl_keyfile=('', str, 'key file for https server.'),
)
3 changes: 2 additions & 1 deletion flexx/app/_app.py
Expand Up @@ -91,8 +91,9 @@ def url(self):
raise RuntimeError('Cannot determine app url if the server is not '
'yet running.')
else:
proto = server.protocol
host, port = server.serving
return 'http://%s:%i/%s/' % (host, port, self._path)
return '%s://%s:%i/%s/' % (proto, host, port, self._path)

@property
def name(self):
Expand Down
5 changes: 4 additions & 1 deletion flexx/app/_clientcore.py
Expand Up @@ -123,10 +123,13 @@ def initSocket(self):

# Construct ws url (for nodejs the location is set by the flexx nodejs runtime)
if not self.ws_url:
proto = 'ws'
if window.location.protocol == 'https:':
proto = 'wss'
address = window.location.hostname
if window.location.port:
address += ':' + window.location.port
self.ws_url = 'ws://%s/flexx/ws/%s' % (address, self.app_name)
self.ws_url = '%s://%s/flexx/ws/%s' % (proto, address, self.app_name)

# Open web socket in binary mode
self.ws = ws = WebSocket(self.ws_url)
Expand Down
6 changes: 4 additions & 2 deletions flexx/app/_funcs.py
Expand Up @@ -93,9 +93,11 @@ def init_interactive(cls=None, runtime=None):

# Launch web runtime, the server will wait for the connection
server = current_server()
proto = server.protocol
host, port = server.serving
url = '%s:%i/%s/?session_id=%s' % (host, port, session.app_name, session.id)
session._runtime = launch('http://' + url, runtime=runtime)
url = '%s://%s:%i/%s/?session_id=%s' % (proto, host, port, session.app_name,
session.id)
session._runtime = launch(url, runtime=runtime)


class NoteBookHelper:
Expand Down
32 changes: 27 additions & 5 deletions flexx/app/_server.py
Expand Up @@ -11,7 +11,8 @@
_current_server = None


def create_server(host=None, port=None, new_loop=False, backend='tornado'):
def create_server(host=None, port=None, new_loop=False, backend='tornado',
**server_kwargs):
"""
Create a new server object. This is automatically called; users generally
don't need this, unless they want to explicitly specify host/port,
Expand All @@ -33,9 +34,24 @@ def create_server(host=None, port=None, new_loop=False, backend='tornado'):
which is made current when ``start()`` is called. If ``False``
(default) will use the current IOLoop for this thread.
backend (str): Stub argument; only Tornado is currently supported.
**server_kwargs: keyword arguments passed to the server constructor.

Returns:
server: The server object, see ``current_server()``.

Examples:

Configuring the server to use HTTPS with Tornado server:

.. code-block:: py

app.create_server(ssl_options = {'certfile' : '/path/to/certfile',
'keyfile' : '/path/to/keyfile'})
app.serve(Example, 'Example')
app.run()

Alternately, cert and key files can be provided through
``ssl_certfile`` and ``ssl_keyfile`` configuration variables.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of setting ssl options via the config. In fact, I think we should advocate that as the way to do it. We can keep the server_kwargs, but can you please please remove the example in this docstring (I know I asked you to add it earlier, sorry!).

"""
# Lazy load tornado, so that we can use anything we want there without
# preventing other parts of flexx.app from using *this* module.
Expand All @@ -53,7 +69,7 @@ def create_server(host=None, port=None, new_loop=False, backend='tornado'):
if _current_server:
_current_server.close()
# Start hosting
_current_server = TornadoServer(host, port, new_loop)
_current_server = TornadoServer(host, port, new_loop, **server_kwargs)
assert isinstance(_current_server, AbstractServer)
# Schedule pending calls
_current_server.call_later(0, _loop.loop.iter)
Expand Down Expand Up @@ -120,10 +136,10 @@ class AbstractServer:
port (int): the port to serve at. None or 0 mean to autoselect a port.
"""

def __init__(self, host, port):
def __init__(self, host, port, **kwargs):
self._serving = None
if host is not False:
self._open(host, port)
self._open(host, port, **kwargs)
assert self._serving # Check that subclass set private variable
self._running = False

Expand Down Expand Up @@ -151,7 +167,7 @@ def close(self):
self._serving = None
self._close()

def _open(self, host, port):
def _open(self, host, port, **kwargs):
raise NotImplementedError()

def _start(self):
Expand All @@ -174,3 +190,9 @@ def serving(self):
Or None if the server is not serving (anymore).
"""
return self._serving

@property
def protocol(self):
""" Get a string representing served protocol
"""
raise NotImplementedError
37 changes: 30 additions & 7 deletions flexx/app/_tornadoserver.py
Expand Up @@ -47,12 +47,12 @@ class TornadoServer(AbstractServer):
""" Flexx Server implemented in Tornado.
"""

def __init__(self, host, port, new_loop):
def __init__(self, host, port, new_loop, **kwargs):
self._new_loop = new_loop
self._app = None
self._server = None
self._get_io_loop()
super().__init__(host, port)
super().__init__(host, port, **kwargs)

def _get_io_loop(self):
# Get a new ioloop or the current ioloop for this thread
Expand All @@ -63,17 +63,30 @@ def _get_io_loop(self):
if self._loop is None:
self._loop = IOLoop(make_current=True)

def _open(self, host, port):
def _open(self, host, port, **kwargs):
# Note: does not get called if host is False. That way we can
# run Flexx in e.g. JLab's application.


# handle ssl, wether from configuration or given args
if 'ssl_certfile' in config and config.ssl_certfile:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That first test ssl_certfile' in config should not be necessary

if 'ssl_options' not in kwargs:
kwargs['ssl_options'] = {}
if 'certfile' not in kwargs['ssl_options']:
kwargs['ssl_options']['certfile'] = config.ssl_certfile

if 'ssl_keyfile' in config and config.ssl_keyfile:
if 'ssl_options' not in kwargs:
kwargs['ssl_options'] = {}
if 'keyfile' not in kwargs['ssl_options']:
kwargs['ssl_options']['keyfile'] = config.ssl_keyfile

# Create tornado application
self._app = Application([(r"/flexx/ws/(.*)", WSHandler),
(r"/flexx/(.*)", MainHandler),
(r"/(.*)", AppHandler), ])
# Create tornado server, bound to our own ioloop
self._server = HTTPServer(self._app, io_loop=self._loop)
self._server = HTTPServer(self._app, io_loop=self._loop, **kwargs)

# Start server (find free port number if port not given)
if port:
# Turn port into int, use hashed port number if a string was given
Expand All @@ -100,7 +113,10 @@ def _open(self, host, port):

# Notify address, so its easy to e.g. copy and paste in the browser
self._serving = self._app._flexx_serving = host, port
logger.info('Serving apps at http://%s:%i/' % (host, port))
proto = 'http'
if 'ssl_options' in kwargs:
proto = 'https'
logger.info('Serving apps at %s://%s:%i/' % (proto, host, port))

def _start(self):
# Ensure that our loop is the current loop for this thread
Expand Down Expand Up @@ -156,6 +172,13 @@ def server(self):
""" The Tornado HttpServer object being used."""
return self._server

@property
def protocol(self):
""" Get a string representing served protocol."""
if self._server.ssl_options is not None:
return 'https'

return 'http'

def port_hash(name):
""" Given a string, returns a port number between 49152 and 65535
Expand Down
33 changes: 33 additions & 0 deletions flexx/app/examples/serve_ssl.py
@@ -0,0 +1,33 @@
"""
Flexx can be configured to use SSL.

This example first creates a self-signed certificate and then uses it to create
a SSL enabled web server (through Tornado ssl_option argument).

Application served through this server is loaded on the browser with 'https'
protocol and its websocket is using 'wss'.
"""

from flexx import app, ui

# generate self-signed certificate for this example
import os

CERTFILE = '/tmp/self-signed.crt'
KEYFILE = '/tmp/self-signed.key'

os.system('openssl req -x509 -nodes -days 1 -batch -newkey rsa:2048 '
'-keyout %s -out %s' % (KEYFILE, CERTFILE))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, if Flexx ever supports a web framework alternative to Tornado, would the same key and cert files be usable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as this new web framework uses a certificate and key files, it should be fine. Since tornado's ssl_options is built from flex config variables id done in _tornadoserver.py, you would just need to use the same variables in the new server _open method.


# Some very secret Model
class Example(ui.Widget):
def init(self):
ui.Button(text='Secret Button')

# configure web server for SSL
app.create_server(ssl_options={'certfile' : CERTFILE,
'keyfile' : KEYFILE})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please use flexx.config here instead? I think that should be the approach to put forward as the way to enable https.

# run application
app.serve(Example, 'Example')
app.run()