voila-dashboards / voila

Voilà turns Jupyter notebooks into standalone web applications
https://voila.readthedocs.io
Other
5.4k stars 501 forks source link

Serving voila through reverse proxy #576

Closed stefanmeili closed 4 years ago

stefanmeili commented 4 years ago

Sorry if this isn't an appropriate forum for this question.

I'm trying put a user authentication page between my voila dashboard and the world. I've created a webpage using flask and am trying to use requests-html as a proxy to serve the dashboard which is behind a firewall (well my router actually).

Here's what I'm trying to do in broad strokes:

from flask import Blueprint
from flask_login import login_required, current_user
from requests_html import HTMLSession

main = Blueprint('main', __name__)

....

@main.route('/dashboard<Voila_key>')
@login_required
def dashboard(Voila_key):
    #make sure current_user is authorized to access this content
    #look up Voila_url in dict using Voila_key
    session = HTMLSession()
    return session.get(Voila_url).content

when I access the /dashboard route, the voila server spins up an instance, but the content is blocked and I get a blank page with only the 'Voila: dashboard name" title.

the flask development server spits out the following to terminal:

127.0.0.1 - - [09/Apr/2020 11:44:21] "GET /dashboard HTTP/1.1" 200 -
127.0.0.1 - - [09/Apr/2020 11:44:21] "GET /voila/static/require.min.js HTTP/1.1" 404 -
127.0.0.1 - - [09/Apr/2020 11:44:21] "GET /voila/static/index.css HTTP/1.1" 404 -
127.0.0.1 - - [09/Apr/2020 11:44:21] "GET /voila/static/theme-light.css HTTP/1.1" 404 -

It looks like some static content is either blocked or can't be found. Any suggestions?

stefanmeili commented 4 years ago

Actually I think what I want is a 'reverse proxy'.

aleave commented 4 years ago

I have seemingly the same issue: my dashboard runs fine when accessed locally or directly using the ip address of the server, but when accessing through a reverse proxy server (nginx) the dashboard runs but the plots do never appear, only the markdown cells in the notebook

stefanmeili commented 4 years ago

I've managed to get this to work - I found you need two proxy_pass locations in the NGINX .conf file for your project.

    location /backend/ {
        proxy_pass http://192.168.XXX.XXX:8866;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }

    location ~* /backend(api/kernels/[^/]+/(channels|iopub|shell|stdin)|terminals/websocket)/? {
        proxy_pass http://192.168.XXX.XXX:8866;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }

Here's the voila.json file I have in my dashboard folder

{
    "Voila": {
        "open_browser": false,
        "port": 8866,
        "base_url": "/backend/my_dashboard/",
        "notebook_path": "Some Amazing Dashboard.ipynb",
        "static_root": "/home/user/dashboards/my_dashboard/voila/static/"

    },
    "VoilaConfiguration": {
        "file_whitelist": [".*\\.(html|xlsx)"]
    },
    "MappingKernelManager": {
        "cull_idle_timeout": 1200,
        "cull_interval": 120
    }
}

and I also copied the contents of /home/username/miniconda3/pkgs/voila-0.1.21-py_0/share/jupyter/voila/templates/default/static to /home/user/dashboards/my_dashboard/voila/static/

This works in that I can serve the dashboard though a reverse proxy, but it doesn't allow me to restrict access to specific users. I'm still trying to work out a solution with requests that I can do entirely through my flask webpage.

jtpio commented 4 years ago

@stefanmeili Thanks for sharing your solution about the nginx config.

Do you think we could add more information about this to the documentation? https://voila.readthedocs.io/en/latest/deploy.html#running-voila-on-a-private-server

This works in that I can serve the dashboard though a reverse proxy, but it doesn't allow me to restrict access to specific users. I'm still trying to work out a solution with requests that I can do entirely through my flask webpage.

Is the authentication happening on the flask server? Just thinking out loud whether a JupyterHub instance with the TLJH gallery plugin could be an alternative?

stefanmeili commented 4 years ago

Hi @jtpio,

That looks complete. Did you just add that? I swear I read through that page 8 times. I thought the second location block was required based on https://github.com/jupyter/notebook/issues/1311, but I just tried commenting it out now and it works. It also looks like I don't need to copy the static content folder or add the "static_root" line in the voila.json, so the second half of my last comment can be safely ignored.

Yeah authentication is on the flask server. I want access to my dashboards to be user / account specific with one login / pass in one database through a common page and with a consistent look. Thanks for the suggestion, I'll take a look.

Maybe it'll just be cleaner to set up an authentication server and set up NGINX like this: https://www.ritchievink.com/blog/2019/03/17/save-some-time-embedding-jupyter-notebook-in-an-iframe-and-serve-as-a-reverse-proxy-behind-nginx/

jtpio commented 4 years ago

That looks complete. Did you just add that?

It was added in https://github.com/voila-dashboards/voila/pull/357. Maybe we should improve the docs to make it more visible?

stefanmeili commented 4 years ago

hmm.... searching google for documentation on voila points at https://voila.readthedocs.io/en/stable/deploy.html which is pretty bare bones. I think that explains why I didn't see it.

jtpio commented 4 years ago

Right, we should maybe make latest the default in the docs (the link in the README points to latest).

roxit commented 4 years ago

For those who don't have access to the reverse proxy settings, here's a way to allow requests from all origins.

{
  "Voila": {
    "tornado_settings": {
      "allow_origin": "*"
    }
  }
}

The check actually happens at https://github.com/jupyter/jupyter_server/blob/master/jupyter_server/base/handlers.py#L278

brichet commented 3 years ago

A little trick for people who want to reverse proxy several Voilà applications (each one using a specific port) running at the same time. The idea is to write the port in the "base_url" option, catch it using a regular expression in NGINX configuration file, and redirect to that port.

voila --port=9999 --no-browser --Voila.base_url=/backend/my_dashboard_9999 notebook.ipynb

Then in NGINX configuration file :

    location ~ ^/backend/my_dashboard_([0-9]*) {
        proxy_pass http://192.168.XXX.XXX:$1;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }
GregSilverman commented 3 years ago

@stefanmeili did you ever solve your Flask authentication issue? We are trying to do the same thing.

stefanmeili commented 3 years ago

Hi @GregSilverman,

Yes, I did get it up and running. I believe the key was this blogpost here: https://www.ritchievink.com/blog/2019/03/17/save-some-time-embedding-jupyter-notebook-in-an-iframe-and-serve-as-a-reverse-proxy-behind-nginx/

GregSilverman commented 3 years ago

@stefanmeili , do you have your full app in a repo somewhere?

Thanks in advance!

stefanmeili commented 3 years ago

@GregSilverman Sorry, not in a public one. Where are you getting stuck?

GregSilverman commented 3 years ago

@stefanmeili Not stuck, was just curious. It does seem rather straightforward.

jmurray6 commented 3 years ago

A little trick for people who want to reverse proxy several Voilà applications (each one using a specific port) running at the same time. The idea is to write the port in the "base_url" option, catch it using a regular expression in NGINX configuration file, and redirect to that port.

voila --port=9999 --no-browser --Voila.base_url=/backend/my_dashboard_9999 notebook.ipynb

Then in NGINX configuration file :

    location ~ ^/backend/my_dashboard_([0-9]*) {
        proxy_pass http://192.168.XXX.XXX:$1;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }

@brichet I'm wondering if you have anything else in the Nginx config file for this? As in, do you also need a location /backend/ directive similar to the example provided by @stefanmeili ?

I tried both of these solutions with no luck. I'm trying to put my voila on Nginx in order to restrict IP traffic to it. So far I have been able to hit the kernel (I see [Voila] Kernel started: in the logs) but only see a spinning wheel and then a blank screen.

jmurray6 commented 3 years ago

Was able to figure it out. I had to use 0.0.0.0 for proxy_pass instead of the IP. I was also able to serve 2 different voila instances on the same server with the following setup.

nginx config:

        location /backend1/ {
           proxy_pass http://0.0.0.0:8866;
           proxy_set_header Connection "upgrade";
           proxy_http_version 1.1;
           proxy_set_header X-Forwarded-Proto $scheme;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header Host $host:$server_port;
           proxy_buffering off;
        }

        location /backend2/ {
           proxy_pass http://0.0.0.0:8877;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection "upgrade";
           proxy_http_version 1.1;
           proxy_set_header X-Forwarded-Proto $scheme;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header Host $host:$server_port;
           proxy_buffering off;
        }

voila.json for each instance in the same directory as the notebook:

{
    "VoilaConfiguration": {
        "file_whitelist": [".*"]
    }   
}

voila command: voila --port=<port> --no-browser --MappingKernelManager.cull_interval=60 --MappingKernelManager.cull_idle_timeout=120 --Voila.tornado_settings 'allow_origin'='*' --Voila.base_ url=/<location>/ lens_selector.ipynb

Just be sure to match your port and your base_url location to your nginx config.

brichet commented 3 years ago

@jmurray6 it should also work with proxy_pass http://127.0.0.1:8866. To use the IP address I think it must be bind with the --Voila.ip option, I didn't mention it. Now if you want to serve more than 2 applications, you can write the port in the --Voila.base_url option and catch it using regular expression in the nginx location section.

GregSilverman commented 3 years ago

@stefanmeili , we got it working, but are running into a strange problem. The src parameter in the iframe is sometimes empty when it passes the para from thee view to the html template. This works without issee in a development environment, but it's sporadic in production. If I keep doing a hard reset at the browser it eventually ends up passing the src url and rendering as expected.

Not sure what is going on? No errors are thrown in nginx, the api or voila, or in the web browser console? It's just sporadically dropping the param value for the iframe src.