|
| 1 | +(standalone)= |
| 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, instead of 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, providing a unified way to access arbitrary applications securely. |
| 9 | + |
| 10 | +This works similarly 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 on 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, creating the voilà dashboard and rendering 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 proxy behavior. Execute `jupyter standaloneproxy --help` |
| 40 | +to get a complete list of all arguments. |
| 41 | + |
| 42 | +### Specify the address and port |
| 43 | + |
| 44 | +The proxy will try to extract the address and port from the `JUPYTERHUB_SERVICE_URL` environment variable. This variable |
| 45 | +will be set by JupyterHub. Otherwise, the server 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 trigger 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 | +### Configuration via traitlets |
| 62 | + |
| 63 | +Instead of using the commandline, a standalone proxy can also be configured via a `traitlets` configuration file. |
| 64 | +The configuration file can be loaded by running `jupyter standaloneproxy --config path/to/config.py`. |
| 65 | + |
| 66 | +The options mentioned above can also be configured in the config file: |
| 67 | + |
| 68 | +```python |
| 69 | +# Specify the command to execute |
| 70 | +c.StandaloneProxyServer.command = [ |
| 71 | + "voila", "--no-browser", "--port={port}", "/path/to/some/Notebook.ipynb" |
| 72 | +] |
| 73 | + |
| 74 | +# Specify address and port |
| 75 | +c.StandaloneProxyServer.address = "localhost" |
| 76 | +c.StandaloneProxyServer.port = 8000 |
| 77 | + |
| 78 | +# Disable authentication |
| 79 | +c.StandaloneProxyServer.skip_authentication = True |
| 80 | +``` |
| 81 | + |
| 82 | +A default config file can be emitted by running `jupyter standaloneproxy --generate-config` |
| 83 | + |
| 84 | +## Usage with JupyterHub |
| 85 | + |
| 86 | +To launch a standalone proxy with JupyterHub, you need to customize the `Spawner` inside the configuration |
| 87 | +using `traitlets`: |
| 88 | + |
| 89 | +```python |
| 90 | +c.Spawner.cmd = "jupyter-standaloneproxy" |
| 91 | +c.Spawner.args = ["--", "voila", "--no-browser", "--port={port}", "/path/to/some/Notebook.ipynb"] |
| 92 | +``` |
| 93 | + |
| 94 | +This will hard-code JupyterHub to launch voilà instead of `jupyterhub-singleuser`. In case you want to give the users |
| 95 | +of JupyterHub the ability to select which application to launch (like selecting either JupyterLab or voilà), |
| 96 | +you will want to make this configuration optional: |
| 97 | + |
| 98 | +```python |
| 99 | +# Let users select which application start |
| 100 | +c.Spawner.options_form = """ |
| 101 | + <label for="select-application">Choose Application: </label> |
| 102 | + <select name="application" required> |
| 103 | + <option value="lab">JupyterLab</option> |
| 104 | + <option value="voila">voila</option> |
| 105 | + </select> |
| 106 | + """ |
| 107 | + |
| 108 | +def select_application(spawner): |
| 109 | + application = spawner.user_options.get("application", ["lab"])[0] |
| 110 | + if application == "voila": |
| 111 | + spawner.cmd = "jupyter-standaloneproxy" |
| 112 | + spawner.args = ["--", "voila", "--no-browser", "--port={port}", "/path/to/some/Notebook.ipynb"] |
| 113 | + |
| 114 | +c.Spawner.pre_spawn_hook = select_application |
| 115 | +``` |
| 116 | + |
| 117 | +```{note} This is only a very basic implementation to show a possible approach. For a production setup, you can create |
| 118 | +a more rigorous implementation by creating a custom `Spawner` and overwriting the appropriate functions and/or |
| 119 | +creating a custom `spawner.html` page. |
| 120 | +``` |
| 121 | + |
| 122 | +## Technical Overview |
| 123 | + |
| 124 | +The following section should serve as an explanation to developers of the standalone feature of jupyter-server-proxy. |
| 125 | +It outlines the basic functionality and will explain the different components of the code in more depth. |
| 126 | + |
| 127 | +### JupyterHub and jupyterhub-singleuser |
| 128 | + |
| 129 | +By default, JupyterHub will use the `jupyterhub-singleuser` executable when launching a new instance for a user. |
| 130 | +This executable is usually a wrapper around the `JupyterLab` or `Notebook` application, with some |
| 131 | +additions regarding authentication and multi-user systems. |
| 132 | +In the standalone feature, we try to mimic these additions, but instead of using `JupyterLab` or `Notebook`, we |
| 133 | +will wrap them around an arbitrary web application. |
| 134 | +This will ensure direct, authenticated access to the application, without needing a Jupyter server to be running |
| 135 | +in the background. The different additions will be discussed in more detail below. |
| 136 | + |
| 137 | +### Structure |
| 138 | + |
| 139 | +The standalone feature is built on top of the `SuperviseAndProxyhandler`, which will spawn a process and proxy |
| 140 | +requests to this server. While this process is called _Server_ in the documentation, the term _Application_ will be |
| 141 | +used here, to avoid confusion with the other server where the `SuperviseAndProxyhandler` is attached to. |
| 142 | +When using jupyter-server-proxy, the proxies are attached to the Jupyter server and will proxy requests |
| 143 | +to the application. |
| 144 | +Since we do not want to use the Jupyter server here, we instead require an alternative server, which will be used |
| 145 | +to attach the `SuperviseAndProxyhandler` and all the required additions from `jupyterhub-singleuser`. |
| 146 | +For that, we use tornado `HTTPServer`. |
| 147 | + |
| 148 | +### Login and Authentication |
| 149 | + |
| 150 | +One central component is the authentication with the JupyterHub Server. |
| 151 | +Any client accessing the application will need to authenticate with the JupyterHub API, which will ensure only |
| 152 | +users themselves (or otherwise allowed users, e.g., admins) can access the application. |
| 153 | +The Login process is started by deriving our `StandaloneProxyHandler` from |
| 154 | +[jupyterhub.services.auth.HubOAuthenticated](https://github.com/jupyterhub/jupyterhub/blob/5.0.0/jupyterhub/services/auth.py#L1541) |
| 155 | +and decorating any methods we want to authenticate with `tornado.web.authenticated`. |
| 156 | +For the proxy, we just decorate the `proxy` method with `web.authenticated`, which will authenticate all routes on all HTTP Methods. |
| 157 | +`HubOAuthenticated` will automatically provide the login URL for the authentication process and any |
| 158 | +client accessing any path of our server will be redirected to the JupyterHub API. |
| 159 | + |
| 160 | +After a client has been authenticated with the JupyterHub API, they will be redirected back to our server. |
| 161 | +This redirect will be received on the `/oauth_callback` path, from where we need to redirect the client back to the |
| 162 | +root of the application. |
| 163 | +We use the [HubOAuthCallbackHandler](https://github.com/jupyterhub/jupyterhub/blob/5.0.0/jupyterhub/services/auth.py#L1547), |
| 164 | +another handler from the JupyterHub package, for this. |
| 165 | +It will also cache the received OAuth state from the login so that we can skip authentication for the next requests |
| 166 | +and do not need to go through the whole login process for each request. |
| 167 | + |
| 168 | +### SSL certificates |
| 169 | + |
| 170 | +In some JupyterHub configurations, the launched application will be configured to use an SSL certificate for requests |
| 171 | +between the JupyterLab / Notebook and the JupyterHub API. The path of the certificate is given in the |
| 172 | +`JUPYTERHUB_SSL_*` environment variables. We use these variables to create a new SSL Context for both |
| 173 | +the `AsyncHTTPClient` (used for Activity Notification, see below) and the `HTTPServer`. |
| 174 | + |
| 175 | +### Activity Notifications |
| 176 | + |
| 177 | +The `jupyterhub-singleuser` will periodically send an activity notification to the JupyterHub API and inform it that |
| 178 | +the currently running application is still active. Whether this information is used or not depends on the specific |
| 179 | +configuration of this JupyterHub. |
| 180 | + |
| 181 | +### Environment Variables |
| 182 | + |
| 183 | +JupyterHub uses a lot of environment variables to specify how the launched app should be run. |
| 184 | +This list is a small overview of all used variables and what they contain and are used for. |
| 185 | + |
| 186 | +| Variable | Explanation | Typical Value | |
| 187 | +| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | |
| 188 | +| `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` | |
| 189 | +| `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 application should be `/user/name/index.html` | `/services/service-name/` or `/user/name/` | |
| 190 | +| `JUPYTERHUB_ACTIVITY_URL` | URL where to send activity notifications to. | `$JUPYTERHUB_API_URL/user/name/activity` | |
| 191 | +| `JUPYTERHUB_API_TOKEN` | Authorization Token for requests to the JupyterHub API. | | |
| 192 | +| `JUPYTERHUB_SERVER_NAME` | A name given to all apps launched by the JupyterHub. | | |
| 193 | +| `JUPYTERHUB_SSL_KEYFILE`, `JUPYTERHUB_SSL_CERTFILE`, `JUPYTERHUB_SSL_CLIENT_CA` | Paths to keyfile, certfile and client CA for the SSL configuration | | |
| 194 | +| `JUPYTERHUB_USER`, `JUPYTERHUB_GROUP` | Name and Group of the user for this application. Required for Authentication | |
0 commit comments