Skip to content

Commit 7349076

Browse files
committed
Add Documentation for standalone
1 parent 1baa2fb commit 7349076

File tree

3 files changed

+167
-6
lines changed

3 files changed

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