Open Lucaszw opened 5 years ago
Hi there. I got some ideas to fit your case. By default, HTTP binds port 80, HTTPS binds port 443. Method I: better, easier and faster use nginx, convert http to https, and of course, you can configure nginx to serve SSL as well. Method II: you can build 2 app, app1 binds port 80, and get user's URI, then redirect the request to app2, with the same URI; app2 binds port 443, handle SSL requests. Have fun.
In heroku, all requests come in to the application as plain http, but with the header X-Forwarded-Proto
to know whether the original request was http or https.
Tornado is more of a toolkit than a solution, in my humble opinion - there's a couple of ways to do this in the application reasonably easily, and it should be done there. And in fact, there are recurring requests for this to work in a couple different ways for a few different deployment scenarios:
https://github.com/ipython/ipython/issues/1460/ https://github.com/tornadoweb/tornado/issues/523 https://github.com/jupyterhub/jupyterhub-deploy-docker/issues/40 https://github.com/jupyterhub/jupyterhub-deploy-docker/issues/74
It looks like jupyter has an option NotebookApp.trust_xheaders
which defaults to False
https://jupyter-notebook.readthedocs.io/en/stable/config.html
The routing framework doesn't currently know anything about http vs https because in non-proxied deployments, they're handled by entirely separate servers that can have their own rules. But in the proxied case with xheaders enabled, this becomes useful. It's pretty simple to do with the (relatively) new routing framework:
from tornado.httpclient import AsyncHTTPClient
from tornado.ioloop import IOLoop
from tornado.routing import Matcher
from tornado.web import Application, RequestHandler
class ProtocolMatcher(Matcher):
def __init__(self, protocol):
self.protocol = protocol
def match(self, request):
if request.protocol == self.protocol:
return {}
return None
class HTTPSRedirectHandler(RequestHandler):
def get(self):
# This is different from tornado.web.RedirectHandler because
# the substitution capabilities in that class aren't quite
# what we need (we don't get path extraction from ProtocolMatcher).
self.redirect("https://" + self.request.host + self.request.uri, permanent=True)
class MyHandler(RequestHandler):
def get(self):
self.write("hello %s" % self.request.protocol)
async def main():
app = Application(
[(ProtocolMatcher("http"), HTTPSRedirectHandler), ("/", MyHandler)]
)
app.listen(8080, address="localhost", xheaders=True)
client = AsyncHTTPClient()
resp = await client.fetch(
"http://localhost:8080/", follow_redirects=False, raise_error=False
)
assert resp.code == 301
assert resp.headers["Location"] == "https://localhost:8080/"
resp = await client.fetch(
"http://localhost:8080/",
follow_redirects=False,
raise_error=False,
headers={"X-Forwarded-Proto": "http"},
)
assert resp.code == 301
assert resp.headers["Location"] == "https://localhost:8080/"
resp = await client.fetch(
"http://localhost:8080/",
follow_redirects=False,
headers={"X-Forwarded-Proto": "https"},
)
assert resp.code == 200
assert resp.body == b"hello https"
if __name__ == "__main__":
IOLoop.current().run_sync(main)
Thanks for the recommendations! I ended up going with the nginx heroku buildpack and used the following config:
daemon off;
worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>;
events {
use epoll;
accept_mutex on;
worker_connections 1024;
}
http {
# Buffers
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 2 1k;
# Timeouts
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
# Gzip
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/x-javascript text/xml text/css application/xml;
# Logging
log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';
access_log off;
error_log logs/nginx/error.log;
# Other
server_tokens off;
include mime.types;
sendfile on;
default_type application/octet-stream;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream ml {
server 127.0.0.1:3000;
}
server {
listen <%= ENV["PORT"] %>;
server_name _;
if ($http_x_forwarded_proto != "https") {
return 301 https://$host$request_uri;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Referer http://localhost;
proxy_set_header Origin "";
}
}
}
with the following Procfile
web: bin/start-nginx jupyter lab --config=./config.py --ip 127.0.0.1 --port 3000
@Lucaszw solution works for me as we :+1:
I am running an instance of JupyterLab on Heroku. https://github.com/heroku/heroku-jupyterlab
Heroku handles the SSL certificate but requires the WebServer on the app level to handle the redirecting:
https://help.heroku.com/J2R1S4T8/can-heroku-force-an-application-to-use-ssl-tls
This seems to be possible with Django & Flask; but I can't seem to find any options for configuring Tornado to do the same (which is used to power JupyterLab).
It would be nice to have an option similar to in Rails and Django like
config.force_ssl = true
etc.