Skip to content

Commit 87efc56

Browse files
committed
Add Documentation for standalone
1 parent 7f85c9f commit 87efc56

File tree

3 files changed

+179
-6
lines changed

3 files changed

+179
-6
lines changed

docs/source/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ install
3030
server-process
3131
launchers
3232
arbitrary-ports-hosts
33+
standalone
3334
```
3435

3536
## Convenience packages for popular applications

docs/source/standalone.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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 |

jupyter_server_proxy/standalone/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def run(
103103
def main():
104104
parser = argparse.ArgumentParser(
105105
"jupyter-standalone-proxy",
106-
description="Wrap an arbitrary WebApp so it can be used in place of 'jupyterhub-singleuser' in a JupyterHub setting.",
106+
description="Wrap an arbitrary web service so it can be used in place of 'jupyterhub-singleuser' in a JupyterHub setting.",
107107
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
108108
)
109109

@@ -112,22 +112,22 @@ def main():
112112
"--port",
113113
type=int,
114114
dest="port",
115-
help="Set port for the proxy server to listen on. Will use 'JUPYTERHUB_SERVICE_URL' or '127.0.0.1' by default.",
115+
help="Set port for the proxy server to listen on. Will use 'JUPYTERHUB_SERVICE_URL' or '8888' by default.",
116116
)
117117
parser.add_argument(
118118
"-a",
119119
"--address",
120120
type=str,
121121
dest="address",
122-
help="Set address for the proxy server to listen on. Will use 'JUPYTERHUB_SERVICE_URL' or '8888' by default.",
122+
help="Set address for the proxy server to listen on. Will use 'JUPYTERHUB_SERVICE_URL' or '127.0.0.1' by default.",
123123
)
124124
parser.add_argument(
125125
"-s",
126126
"--server-port",
127127
default=0,
128128
type=int,
129129
dest="server_port",
130-
help="Port for the WebApp should end up running on (0 for random open port).",
130+
help="Port for the web service should end up running on (0 for random open port).",
131131
)
132132
parser.add_argument(
133133
"--socket-path",
@@ -187,7 +187,7 @@ def main():
187187
"--activity-interval",
188188
default=300,
189189
type=int,
190-
help="Frequency to notify Hub that the WebApp is still running (In seconds, 0 for never).",
190+
help="Frequency to notify Hub that the service is still running (In seconds, 0 for never).",
191191
)
192192
# ToDo: Progressive Proxy
193193
# parser.add_argument(
@@ -203,7 +203,7 @@ def main():
203203
help="Max size of websocket data (leave at 0 for library defaults).",
204204
)
205205
parser.add_argument(
206-
"command", nargs="+", help="The command executed for starting the WebApp"
206+
"command", nargs="+", help="The command executed for starting the web service."
207207
)
208208

209209
args = parser.parse_args()

0 commit comments

Comments
 (0)