|
| 1 | +(standanlone)= |
| 2 | + |
| 3 | +# Spawning and proxying a web service from JupyterHub |
| 4 | + |
| 5 | +The `standalone` feature of Jupyter Server Proxy enables JupyterHub Admins to launch and proxy arbitrary web services |
| 6 | +directly, in place of the JupyterLab or Notebook. You can use Jupyter Server Proxy to spawn a single proxy, |
| 7 | +without it being attached to a Jupyter server. The proxy securely authenticates and restricts access to authorized |
| 8 | +users through JupyterHub, giving a unified way to securely provide arbitrary applications. |
| 9 | + |
| 10 | +This works similar to {ref}`proxying Server Processes <server-process>`, where a server process is started and proxied. |
| 11 | +The Proxy is usually started from the command line, often by modifying the `Spawner.cmd` in your |
| 12 | +[JupyterHub Configuration](https://jupyterhub.readthedocs.io/en/stable/tutorial/getting-started/spawners-basics.html). |
| 13 | + |
| 14 | +This feature builds upon the work of [Dan Lester](https://github.com/danlester), who originally developed it in the |
| 15 | +[jhsingle-native-proxy](https://github.com/ideonate/jhsingle-native-proxy) package. |
| 16 | + |
| 17 | +## Installation |
| 18 | + |
| 19 | +This feature has a dependency to JupyterHub and must be explicitly installed via an optional dependency: |
| 20 | + |
| 21 | +```shell |
| 22 | +pip install jupyter-server-proxy[standalone] |
| 23 | +``` |
| 24 | + |
| 25 | +## Usage |
| 26 | + |
| 27 | +The standalone proxy is controlled with the `jupyter standaloneproxy` command. You always need to specify the |
| 28 | +{ref}`command <server-process:cmd>` of the web service that will be launched and proxied. Let's use |
| 29 | +[voilà](https://github.com/voila-dashboards/voila) as an example here: |
| 30 | + |
| 31 | +```shell |
| 32 | +jupyter standaloneproxy -- voila --no-browser --port={port} /path/to/some/Notebook.ipynb |
| 33 | +``` |
| 34 | + |
| 35 | +Executing this command will spawn a new HTTP Server, which will spawn the voilà dashboard and render the notebook. |
| 36 | +Any template strings (like the `--port={port}`) inside the command will be automatically replaced when the command is |
| 37 | +executed. |
| 38 | + |
| 39 | +The CLI has multiple advanced options to customize the behavior of the proxy. Execute `jupyter standaloneproxy --help` |
| 40 | +to get a complete list of all arguments. |
| 41 | + |
| 42 | +### Specify address and port |
| 43 | + |
| 44 | +The proxy will try to extract the address and port from the `JUPYTERHUB_SERVICE_URL` environment variable, which is |
| 45 | +set if an application is launched by JupyterHub. Otherwise, it will be launched on `127.0.0.1:8888`. |
| 46 | +You can also explicitly overwrite these values: |
| 47 | + |
| 48 | +```shell |
| 49 | +jupyter standaloneproxy --address=localhost --port=8000 ... |
| 50 | +``` |
| 51 | + |
| 52 | +### Disable Authentication |
| 53 | + |
| 54 | +For testing, it can be useful to disable the authentication with JupyterHub. Passing `--skip-authentication` will |
| 55 | +not triggering the login process when accessing the application. |
| 56 | + |
| 57 | +```{warning} Disabling authentication will leave the application open to anyone! Be careful with it, |
| 58 | +especially on multi-user systems. |
| 59 | +``` |
| 60 | + |
| 61 | +## Usage with JupyterHub |
| 62 | + |
| 63 | +To launch a standalone proxy with JupyterHub, you need to customize the `Spawner` inside the configuration |
| 64 | +using traitlets: |
| 65 | + |
| 66 | +```python |
| 67 | +c.Spawner.cmd = "jupyter-standaloneproxy" |
| 68 | +c.Spawner.args = ["--", "voila", "--no-browser", "--port={port}", "/path/to/some/Notebook.ipynb"] |
| 69 | +``` |
| 70 | + |
| 71 | +This will hard-code JupyterHub to launch voilà instead of `jupyterhub-singleuser`. In case you want to give the users |
| 72 | +of the JupyterHub the ability to select which application to launch (like selecting either JupyterLab or voilà), |
| 73 | +you will want to make this configuration optional: |
| 74 | + |
| 75 | +```python |
| 76 | +# Let users select which application start |
| 77 | +c.Spawner.options_form = """ |
| 78 | + <label for="select-application">Choose Application: </label> |
| 79 | + <select name="application" required> |
| 80 | + <option value="lab">JupyterLab</option> |
| 81 | + <option value="voila">voila</option> |
| 82 | + </select> |
| 83 | + """ |
| 84 | + |
| 85 | +def select_application(spawner): |
| 86 | + application = spawner.user_options.get("application", ["lab"])[0] |
| 87 | + if application == "voila": |
| 88 | + spawner.cmd = "jupyter-standaloneproxy" |
| 89 | + spawner.args = ["--", "voila", "--no-browser", "--port={port}", "/path/to/some/Notebook.ipynb"] |
| 90 | + |
| 91 | +c.Spawner.pre_spawn_hook = select_application |
| 92 | +``` |
| 93 | + |
| 94 | +```{note} This is only a very basic implementation to show a possible approach. For a production setup, you can create |
| 95 | +a more rigorous implementation by creating a custom `Spawner` and overwriting the appropriate functions and/or |
| 96 | +creating a custom `spawner.html` page. |
| 97 | +``` |
| 98 | + |
| 99 | +## Technical Overview |
| 100 | + |
| 101 | +The following section should serve as an explanation to developers of the standalone feature of jupyter-server-proxy. |
| 102 | +It outlines the basic functionality and will explain the different components of the code in more depth. |
| 103 | + |
| 104 | +### JupyterHub and jupyterhub-singleuser |
| 105 | + |
| 106 | +By default, JupyterHub will use the `jupyterhub-singleuser` executable when launching a new instance for a user. |
| 107 | +This executable is usually a wrapper around the `JupyterLab` or `Notebook` application, with some |
| 108 | +additions regarding authentication and multi-user systems. |
| 109 | +In the standalone feature, we try to mimic these additions, but instead of using `JupyterLab` or `Notebook`, we |
| 110 | +will wrap them around an arbitrary web application. |
| 111 | +This will ensure only authenticated access to the application, while providing direct access to the application |
| 112 | +without needing a Jupyter server to be running in the background. |
| 113 | +The different additions will be discussed in more detail below. |
| 114 | + |
| 115 | +### Structure |
| 116 | + |
| 117 | +The standalone feature is built on top of the `SuperviseAndProxyhandler`, which will spawn a process and proxy |
| 118 | +requests to this server. While this process is called _Server_ in the documentation, I will call it _Application_ |
| 119 | +here, to avoid confusion with the other server where the `SuperviseAndProxyhandler` is attached to. |
| 120 | +When using jupyter-server-proxy, the proxies are attached to the Jupyter server and will proxy requests |
| 121 | +to the application. |
| 122 | +Since we do not want to use the Jupyter server here, we instead require an alternative server, which will be used |
| 123 | +to attach the `SuperviseAndProxyhandler` and all the required additions from `jupyterhub-singleuser`. |
| 124 | +For that, we use tornado `HTTPServer`. |
| 125 | + |
| 126 | +### Login and Authentication |
| 127 | + |
| 128 | +One central component is the authentication with the JupyterHub Server. |
| 129 | +Any client accessing the application will need to authenticate with the JupyterHub API, which will ensure only |
| 130 | +the user themselves (or otherwise allowed users, e.g., admins) can access the application. |
| 131 | +The Login process is started by deriving our `StandaloneProxyHandler` from |
| 132 | +[jupyterub.services.auth.HubOAuthenticated](https://github.com/jupyterhub/jupyterhub/blob/5.0.0/jupyterhub/services/auth.py#L1541) |
| 133 | +and decorating any methods we want to authenticate with `tornado.web.authenticated`. |
| 134 | +For the proxy, we just decorate the `proxy` method with `web.authenticated`, which will authenticate all routes on all HTTP Methods. |
| 135 | +`HubOAuthenticated` will automatically provide the login URL for the authentication process and any |
| 136 | +client accessing any path of our server will be redirected to the JupyterHub API. |
| 137 | + |
| 138 | +After a client has been authenticated with the JupyterHub API, they will be redirected back to our server. |
| 139 | +This redirect will be received on the `/oauth_callback` path, from where we need to redirect the client back to the |
| 140 | +root of the application. |
| 141 | +We use the [HubOAuthCallbackHander](https://github.com/jupyterhub/jupyterhub/blob/5.0.0/jupyterhub/services/auth.py#L1547), |
| 142 | +another handler from the JupyterHub package, for this. |
| 143 | +It will also cache the received OAuth state from the login, so that we can skip authentication for the next requests |
| 144 | +and do not need to go through the whole login process for each request. |
| 145 | + |
| 146 | +### SSL certificates |
| 147 | + |
| 148 | +In some JupyterHub configurations, the launched application will be configured to use an SSL certificate for request |
| 149 | +between the JupyterLab / Notebook and the JupyterHub API. The path of the certificate is given in the |
| 150 | +`JUPYTERHUB_SSL_*` environment variables. We use these variables to create a new SSL Context for both |
| 151 | +the `AsyncHTTPClient` (used for Activity Notification, see below) and the `HTTPServer`. |
| 152 | + |
| 153 | +### Activity Notifications |
| 154 | + |
| 155 | +The `jupyterhub-singleuser` will periodically send an activity notification to the JupyterHub API and inform it that |
| 156 | +the currently running application is still active. Whether this information is actually used or not depends on the |
| 157 | +specific configuration of this JupyterHub. |
| 158 | + |
| 159 | +### Environment Variables |
| 160 | + |
| 161 | +JupyterHub uses a lot of environment variables to specify how the launched app should be run. |
| 162 | +This list is a small overview of all used variables and what they contain and are used for. |
| 163 | + |
| 164 | +| Variable | Explanation | Typical Value | |
| 165 | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | |
| 166 | +| `JUPYTERHUB_SERVICE_URL` | URL where the server should be listening. Used to find the Address and Port to start the server on. | `http://127.0.0.1:5555` | |
| 167 | +| `JUPYTERHUB_SERVICE_PREFIX` | An URL Prefix where the root of the launched application should be hosted. E.g., when set to `/user/name/`, then the root of the proxied aplication should be `/user/name/index.html` | `/services/service-name/` or `/user/name/` | |
| 168 | +| `JUPYTERHUB_ACTIVITY_URL` | URL where to send activity notifications to. | `$JUPYTERHUB_API_URL/user/name/activity` | |
| 169 | +| `JUPYTERHUB_API_TOKEN` | Authorization Token for requests to the JupyterHub API. | | |
| 170 | +| `JUPYTERHUB_SERVER_NAME` | A name given to all apps launched by the JupyterHub. | | |
| 171 | +| `JUPYTERHUB_SSL_KEYFILE`, `JUPYTERHUB_SSL_CERTFILE`, `JUPYTERHUB_SSL_CLIENT_CA` | Paths to keyfile, certfile and client CA for the SSL configuration | | |
| 172 | +| `JUPYTERHUB_USER`, `JUPYTERHUB_GROUP` | Name and Group of the user for this application. Required for Authentication | |
0 commit comments