qubicl-cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -9
- package/dist/SBOM.spdx.json +226 -19
- package/dist/THIRD_PARTY_NOTICES.txt +392 -1
- package/dist/assets/computer/Dockerfile +27 -7
- package/dist/assets/computer/THIRD_PARTY_NOTICES.txt +73 -1
- package/dist/assets/computer/control.mjs +97 -86
- package/dist/assets/computer/entrypoint.sh +162 -4
- package/dist/assets/computer/manifests/browser.json +2 -2
- package/dist/assets/computer/manifests/computer.json +2 -2
- package/dist/assets/computer/manifests/file-system.json +2 -2
- package/dist/assets/computer/manifests/workstation.json +2 -2
- package/dist/assets/computer/qubicl_viewer_auth.py +63 -0
- package/dist/assets/computer/x11vnc-relay.sh +28 -0
- package/dist/assets/gateway/Dockerfile +6 -3
- package/dist/assets/gateway/gateway.mjs +44 -44
- package/dist/assets/image-catalog.json +59 -56
- package/dist/qubicl.mjs +220 -184
- package/package.json +3 -2
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
runtime_role="${QUBICL_RUNTIME_ROLE:-control}"
|
|
5
|
+
baked_viewer_authentication="${QUBICL_IMAGE_VIEWER_AUTHENTICATION:-legacy}"
|
|
6
|
+
runtime_viewer_authentication="${QUBICL_VIEWER_AUTHENTICATION:-}"
|
|
7
|
+
viewer_key_handoff="${QUBICL_VIEWER_KEY:-}"
|
|
8
|
+
unset QUBICL_VIEWER_AUTHENTICATION QUBICL_VIEWER_KEY
|
|
5
9
|
if [[ "$runtime_role" == egress || "$runtime_role" == web ]]; then
|
|
6
10
|
exec node /opt/qubicl/control.mjs
|
|
7
11
|
fi
|
|
@@ -61,6 +65,89 @@ if [[ "$requested_profile" != "$baked_profile" ]]; then
|
|
|
61
65
|
exit 78
|
|
62
66
|
fi
|
|
63
67
|
|
|
68
|
+
browser_home_ownership_error() {
|
|
69
|
+
local path="$1"
|
|
70
|
+
echo "Qubicl browser storage ownership does not match this host (${expected_owner}): ${path}" >&2
|
|
71
|
+
echo "Stop this computer, then run: qubicl repair ownership ${QUBICL_NAME:-<name>}" >&2
|
|
72
|
+
exit 78
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
prepare_browser_parent_directory() {
|
|
76
|
+
local path="$1" owner
|
|
77
|
+
if [[ -L "$path" ]] || { [[ -e "$path" ]] && [[ ! -d "$path" ]]; }; then
|
|
78
|
+
browser_home_ownership_error "$path"
|
|
79
|
+
fi
|
|
80
|
+
if [[ ! -e "$path" ]]; then
|
|
81
|
+
runuser -u qubicl -- install -d -m 0700 "$path"
|
|
82
|
+
fi
|
|
83
|
+
owner="$(stat -c '%u:%g' -- "$path")"
|
|
84
|
+
if [[ "$owner" == 0:0 && "$owner" != "$expected_owner" ]]; then
|
|
85
|
+
# v0.2 prerelease session supervisors could create this exact managed
|
|
86
|
+
# ancestry as root before dropping Chromium to the computer user. Repair
|
|
87
|
+
# only the directory entry itself; never recurse through durable data.
|
|
88
|
+
chown --no-dereference qubicl:qubicl "$path"
|
|
89
|
+
elif [[ "$owner" != "$expected_owner" ]]; then
|
|
90
|
+
browser_home_ownership_error "$path"
|
|
91
|
+
fi
|
|
92
|
+
runuser -u qubicl -- test -d "$path" -a -w "$path" -a -x "$path" || browser_home_ownership_error "$path"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
prepare_browser_leaf_directory() {
|
|
96
|
+
local path="$1" owner
|
|
97
|
+
if [[ -L "$path" ]] || { [[ -e "$path" ]] && [[ ! -d "$path" ]]; }; then
|
|
98
|
+
browser_home_ownership_error "$path"
|
|
99
|
+
fi
|
|
100
|
+
if [[ ! -e "$path" ]]; then
|
|
101
|
+
runuser -u qubicl -- install -d -m 0700 "$path"
|
|
102
|
+
fi
|
|
103
|
+
owner="$(stat -c '%u:%g' -- "$path")"
|
|
104
|
+
if [[ "$owner" == 0:0 && "$owner" != "$expected_owner" ]]; then
|
|
105
|
+
if find "$path" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
|
|
106
|
+
browser_home_ownership_error "$path"
|
|
107
|
+
fi
|
|
108
|
+
chown --no-dereference qubicl:qubicl "$path"
|
|
109
|
+
elif [[ "$owner" != "$expected_owner" ]]; then
|
|
110
|
+
browser_home_ownership_error "$path"
|
|
111
|
+
fi
|
|
112
|
+
runuser -u qubicl -- chmod 0700 "$path"
|
|
113
|
+
runuser -u qubicl -- test -d "$path" -a -w "$path" -a -x "$path" || browser_home_ownership_error "$path"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
prepare_browser_home() {
|
|
117
|
+
prepare_browser_parent_directory /home/qubicl/.local
|
|
118
|
+
prepare_browser_parent_directory /home/qubicl/.local/share
|
|
119
|
+
prepare_browser_parent_directory /home/qubicl/.local/share/qubicl
|
|
120
|
+
prepare_browser_leaf_directory /home/qubicl/.local/share/qubicl/browser-profile
|
|
121
|
+
prepare_browser_leaf_directory /home/qubicl/Downloads
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if [[ "$runtime_role" == session || "$runtime_role" == computer ]] && [[ "$baked_profile" != file-system ]]; then
|
|
125
|
+
prepare_browser_home
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
unused_viewer_id() {
|
|
129
|
+
local database="$1" reserved="$2" candidate
|
|
130
|
+
for candidate in $(seq 60000 -1 59000); do
|
|
131
|
+
if [[ "$candidate" != "$reserved" ]] && ! getent "$database" "$candidate" >/dev/null; then
|
|
132
|
+
printf '%s\n' "$candidate"
|
|
133
|
+
return
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
echo "Qubicl could not allocate an isolated viewer identity." >&2
|
|
137
|
+
exit 78
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if [[ "$baked_profile" != file-system ]]; then
|
|
141
|
+
viewer_gid="$(id -g qubicl-viewer)"
|
|
142
|
+
if [[ "$viewer_gid" == "$target_gid" ]]; then
|
|
143
|
+
groupmod --gid "$(unused_viewer_id group "$target_gid")" qubicl-viewer
|
|
144
|
+
fi
|
|
145
|
+
viewer_uid="$(id -u qubicl-viewer)"
|
|
146
|
+
if [[ "$viewer_uid" == "$target_uid" ]]; then
|
|
147
|
+
usermod --uid "$(unused_viewer_id passwd "$target_uid")" qubicl-viewer
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
|
|
64
151
|
pids=()
|
|
65
152
|
start_as_user() {
|
|
66
153
|
user_network_env=()
|
|
@@ -80,6 +167,15 @@ start_as_user() {
|
|
|
80
167
|
pids+=("$!")
|
|
81
168
|
}
|
|
82
169
|
|
|
170
|
+
start_as_viewer() {
|
|
171
|
+
runuser -u qubicl-viewer -- env -i \
|
|
172
|
+
HOME=/nonexistent USER=qubicl-viewer LOGNAME=qubicl-viewer SHELL=/usr/sbin/nologin \
|
|
173
|
+
PATH=/usr/local/bin:/usr/bin:/bin LANG=C.UTF-8 LC_ALL=C.UTF-8 \
|
|
174
|
+
DISPLAY="${DISPLAY:-:0}" \
|
|
175
|
+
"$@" &
|
|
176
|
+
pids+=("$!")
|
|
177
|
+
}
|
|
178
|
+
|
|
83
179
|
start_control_as_user() {
|
|
84
180
|
control_env=(
|
|
85
181
|
HOME=/home/qubicl USER=qubicl LOGNAME=qubicl SHELL=/bin/bash
|
|
@@ -110,6 +206,8 @@ start_control_as_user() {
|
|
|
110
206
|
)
|
|
111
207
|
if [[ -v QUBICL_SESSION_URL ]]; then control_env+=(QUBICL_SESSION_URL="$QUBICL_SESSION_URL"); fi
|
|
112
208
|
if [[ -v QUBICL_SESSION_KEY ]]; then control_env+=(QUBICL_SESSION_KEY="$QUBICL_SESSION_KEY"); fi
|
|
209
|
+
if [[ -v QUBICL_PREVIEW_ACCESS_PATH ]]; then control_env+=(QUBICL_PREVIEW_ACCESS_PATH="$QUBICL_PREVIEW_ACCESS_PATH"); fi
|
|
210
|
+
if [[ -v QUBICL_REMOTE_PREVIEW_BASE ]]; then control_env+=(QUBICL_REMOTE_PREVIEW_BASE="$QUBICL_REMOTE_PREVIEW_BASE"); fi
|
|
113
211
|
if [[ -v QUBICL_MANIFEST_PATH ]]; then control_env+=(QUBICL_MANIFEST_PATH="$QUBICL_MANIFEST_PATH"); fi
|
|
114
212
|
if [[ "${QUBICL_TOKEN_METRICS:-0}" == 1 ]]; then control_env+=(QUBICL_TOKEN_METRICS=1); fi
|
|
115
213
|
if [[ -v DISPLAY ]]; then control_env+=(DISPLAY="$DISPLAY"); fi
|
|
@@ -216,15 +314,75 @@ start_display() {
|
|
|
216
314
|
exit 1
|
|
217
315
|
}
|
|
218
316
|
|
|
317
|
+
prepare_viewer_runtime() {
|
|
318
|
+
install -d -m 0750 -o root -g qubicl-viewer /run/qubicl-viewer
|
|
319
|
+
install -d -m 0700 -o qubicl-viewer -g qubicl-viewer /run/qubicl-viewer/sockets
|
|
320
|
+
rm -f /run/qubicl-viewer/key
|
|
321
|
+
rm -f /run/qubicl-viewer/sockets/view.sock /run/qubicl-viewer/sockets/control.sock
|
|
322
|
+
case "$baked_viewer_authentication" in
|
|
323
|
+
legacy)
|
|
324
|
+
viewer_authentication=legacy
|
|
325
|
+
;;
|
|
326
|
+
header-v1)
|
|
327
|
+
if [[ "$runtime_viewer_authentication" != header-v1 ]]; then
|
|
328
|
+
echo "Qubicl viewer authentication does not match the baked image contract." >&2
|
|
329
|
+
exit 78
|
|
330
|
+
fi
|
|
331
|
+
viewer_authentication=header-v1
|
|
332
|
+
viewer_key="${viewer_key_handoff:?protected viewer key is missing}"
|
|
333
|
+
if [[ ! "$viewer_key" =~ ^[A-Za-z0-9_-]{43}$ ]]; then
|
|
334
|
+
echo "Qubicl received malformed protected viewer authentication material." >&2
|
|
335
|
+
exit 78
|
|
336
|
+
fi
|
|
337
|
+
(umask 077; printf '%s\n' "$viewer_key" >/run/qubicl-viewer/key)
|
|
338
|
+
chown root:qubicl-viewer /run/qubicl-viewer/key
|
|
339
|
+
chmod 0640 /run/qubicl-viewer/key
|
|
340
|
+
unset viewer_key
|
|
341
|
+
;;
|
|
342
|
+
*)
|
|
343
|
+
echo "Qubicl received an unsupported viewer authentication mode." >&2
|
|
344
|
+
exit 78
|
|
345
|
+
;;
|
|
346
|
+
esac
|
|
347
|
+
unset runtime_viewer_authentication viewer_key_handoff
|
|
348
|
+
}
|
|
349
|
+
|
|
219
350
|
start_viewer() {
|
|
220
351
|
runuser -u qubicl -- env DISPLAY="$DISPLAY" xset s off -dpms >/dev/null 2>&1 || true
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
352
|
+
start_as_viewer socat \
|
|
353
|
+
UNIX-LISTEN:/run/qubicl-viewer/sockets/view.sock,fork,mode=0600 \
|
|
354
|
+
EXEC:/usr/local/bin/qubicl-x11vnc-view,nofork
|
|
355
|
+
start_as_viewer socat \
|
|
356
|
+
UNIX-LISTEN:/run/qubicl-viewer/sockets/control.sock,fork,mode=0600 \
|
|
357
|
+
EXEC:/usr/local/bin/qubicl-x11vnc-control,nofork
|
|
358
|
+
for _ in $(seq 1 100); do
|
|
359
|
+
if [[ -S /run/qubicl-viewer/sockets/view.sock && -S /run/qubicl-viewer/sockets/control.sock ]]; then break; fi
|
|
360
|
+
sleep 0.1
|
|
361
|
+
done
|
|
362
|
+
if [[ ! -S /run/qubicl-viewer/sockets/view.sock || ! -S /run/qubicl-viewer/sockets/control.sock ]]; then
|
|
363
|
+
echo "Qubicl viewer relays did not become ready." >&2
|
|
364
|
+
exit 1
|
|
365
|
+
fi
|
|
366
|
+
viewer_auth_args=()
|
|
367
|
+
viewer_web_auth_args=()
|
|
368
|
+
if [[ "$viewer_authentication" == header-v1 ]]; then
|
|
369
|
+
viewer_auth_args=(--auth-plugin=qubicl_viewer_auth.HeaderKeyAuth --auth-source=/run/qubicl-viewer/key)
|
|
370
|
+
viewer_web_auth_args=(--web-auth)
|
|
371
|
+
fi
|
|
372
|
+
start_as_viewer /usr/bin/python3 -I /usr/bin/websockify \
|
|
373
|
+
--web=/usr/share/novnc \
|
|
374
|
+
"${viewer_web_auth_args[@]}" \
|
|
375
|
+
"${viewer_auth_args[@]}" \
|
|
376
|
+
--unix-target=/run/qubicl-viewer/sockets/view.sock \
|
|
377
|
+
0.0.0.0:6080
|
|
378
|
+
start_as_viewer /usr/bin/python3 -I /usr/bin/websockify \
|
|
379
|
+
"${viewer_auth_args[@]}" \
|
|
380
|
+
--unix-target=/run/qubicl-viewer/sockets/control.sock \
|
|
381
|
+
0.0.0.0:6081
|
|
225
382
|
}
|
|
226
383
|
|
|
227
384
|
if [[ "$runtime_role" == session || "$runtime_role" == computer ]]; then
|
|
385
|
+
if [[ "$baked_profile" != file-system ]]; then prepare_viewer_runtime; fi
|
|
228
386
|
case "$baked_profile" in
|
|
229
387
|
file-system)
|
|
230
388
|
unset DISPLAY
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
],
|
|
67
67
|
"viewer": true,
|
|
68
68
|
"controlProtocolVersion": 10,
|
|
69
|
-
"qubiclVersion": "0.
|
|
70
|
-
"revision": "
|
|
69
|
+
"qubiclVersion": "0.2.0",
|
|
70
|
+
"revision": "5e54dafceb7c28ca3bcf561e6281be3d7da976d5",
|
|
71
71
|
"startupProfile": "browser"
|
|
72
72
|
}
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
],
|
|
71
71
|
"viewer": true,
|
|
72
72
|
"controlProtocolVersion": 10,
|
|
73
|
-
"qubiclVersion": "0.
|
|
74
|
-
"revision": "
|
|
73
|
+
"qubiclVersion": "0.2.0",
|
|
74
|
+
"revision": "5e54dafceb7c28ca3bcf561e6281be3d7da976d5",
|
|
75
75
|
"startupProfile": "desktop"
|
|
76
76
|
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"viewer": false,
|
|
38
38
|
"controlProtocolVersion": 10,
|
|
39
|
-
"qubiclVersion": "0.
|
|
40
|
-
"revision": "
|
|
39
|
+
"qubiclVersion": "0.2.0",
|
|
40
|
+
"revision": "5e54dafceb7c28ca3bcf561e6281be3d7da976d5",
|
|
41
41
|
"startupProfile": "file-system"
|
|
42
42
|
}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
],
|
|
73
73
|
"viewer": true,
|
|
74
74
|
"controlProtocolVersion": 10,
|
|
75
|
-
"qubiclVersion": "0.
|
|
76
|
-
"revision": "
|
|
75
|
+
"qubiclVersion": "0.2.0",
|
|
76
|
+
"revision": "5e54dafceb7c28ca3bcf561e6281be3d7da976d5",
|
|
77
77
|
"startupProfile": "workstation"
|
|
78
78
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import stat
|
|
5
|
+
|
|
6
|
+
from websockify.auth_plugins import AuthenticationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
KEY_PATTERN = re.compile(r"^[A-Za-z0-9_-]{43}$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HeaderKeyAuth:
|
|
13
|
+
"""Authenticate a gateway-injected header against a protected key file."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, src=None):
|
|
16
|
+
self.source = src
|
|
17
|
+
|
|
18
|
+
def authenticate(self, headers, target_host, target_port):
|
|
19
|
+
del target_host, target_port
|
|
20
|
+
expected = self._read_key()
|
|
21
|
+
received = headers.get("X-Qubicl-Viewer-Key")
|
|
22
|
+
if (
|
|
23
|
+
not isinstance(received, str)
|
|
24
|
+
or not KEY_PATTERN.fullmatch(received)
|
|
25
|
+
or not hmac.compare_digest(received, expected)
|
|
26
|
+
):
|
|
27
|
+
raise AuthenticationError(
|
|
28
|
+
response_code=403,
|
|
29
|
+
response_msg="Forbidden",
|
|
30
|
+
log_msg="viewer authentication failed",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _read_key(self):
|
|
34
|
+
if not self.source:
|
|
35
|
+
self._unavailable()
|
|
36
|
+
flags = os.O_RDONLY | os.O_CLOEXEC
|
|
37
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
38
|
+
flags |= os.O_NOFOLLOW
|
|
39
|
+
try:
|
|
40
|
+
descriptor = os.open(self.source, flags)
|
|
41
|
+
try:
|
|
42
|
+
info = os.fstat(descriptor)
|
|
43
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 128:
|
|
44
|
+
self._unavailable()
|
|
45
|
+
with os.fdopen(descriptor, "r", encoding="ascii") as handle:
|
|
46
|
+
descriptor = -1
|
|
47
|
+
value = handle.read().removesuffix("\n")
|
|
48
|
+
finally:
|
|
49
|
+
if descriptor >= 0:
|
|
50
|
+
os.close(descriptor)
|
|
51
|
+
except (OSError, TypeError, UnicodeError):
|
|
52
|
+
self._unavailable()
|
|
53
|
+
if not KEY_PATTERN.fullmatch(value):
|
|
54
|
+
self._unavailable()
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _unavailable():
|
|
59
|
+
raise AuthenticationError(
|
|
60
|
+
response_code=503,
|
|
61
|
+
response_msg="Viewer unavailable",
|
|
62
|
+
log_msg="viewer authentication key unavailable",
|
|
63
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
case "${0##*/}" in
|
|
5
|
+
qubicl-x11vnc-view)
|
|
6
|
+
viewer_mode=(-viewonly)
|
|
7
|
+
;;
|
|
8
|
+
qubicl-x11vnc-control)
|
|
9
|
+
viewer_mode=()
|
|
10
|
+
;;
|
|
11
|
+
*)
|
|
12
|
+
exit 64
|
|
13
|
+
;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
# x11vnc's packaged Unix-listener support depends on a downstream libvncserver
|
|
17
|
+
# build detail. socat supplies a real connected socket on stdin/stdout instead,
|
|
18
|
+
# so inetd mode never opens a raw TCP RFB listener.
|
|
19
|
+
exec x11vnc \
|
|
20
|
+
-inetd \
|
|
21
|
+
-display "${DISPLAY:-:0}" \
|
|
22
|
+
-rfbport 0 \
|
|
23
|
+
-shared \
|
|
24
|
+
-nopw \
|
|
25
|
+
-noremote \
|
|
26
|
+
-nocmds \
|
|
27
|
+
-q \
|
|
28
|
+
"${viewer_mode[@]}"
|
|
@@ -6,13 +6,16 @@ ARG QUBICL_CREATED=unknown
|
|
|
6
6
|
ARG QUBICL_SOURCE=https://github.com/EldanRing/qubicl
|
|
7
7
|
|
|
8
8
|
LABEL org.opencontainers.image.title="Qubicl gateway" \
|
|
9
|
-
org.opencontainers.image.description="Authenticated local gateway for Qubicl computers" \
|
|
9
|
+
org.opencontainers.image.description="Authenticated local and optional TLS gateway for Qubicl computers" \
|
|
10
10
|
org.opencontainers.image.licenses="Apache-2.0" \
|
|
11
11
|
org.opencontainers.image.source="$QUBICL_SOURCE" \
|
|
12
12
|
org.opencontainers.image.url="$QUBICL_SOURCE" \
|
|
13
13
|
org.opencontainers.image.version="$QUBICL_VERSION" \
|
|
14
14
|
org.opencontainers.image.revision="$QUBICL_REVISION" \
|
|
15
|
-
org.opencontainers.image.created="$QUBICL_CREATED"
|
|
15
|
+
org.opencontainers.image.created="$QUBICL_CREATED" \
|
|
16
|
+
dev.qubicl.gateway-protocol-version="2" \
|
|
17
|
+
dev.qubicl.gateway-exposure="direct-tls-v1" \
|
|
18
|
+
dev.qubicl.viewer-authentication="header-v1"
|
|
16
19
|
|
|
17
20
|
WORKDIR /app
|
|
18
21
|
RUN apk upgrade --no-cache libcrypto3 libssl3 \
|
|
@@ -32,5 +35,5 @@ COPY --chown=node:node gateway.mjs /app/gateway.mjs
|
|
|
32
35
|
COPY LICENSE THIRD_PARTY_NOTICES.txt /usr/share/doc/qubicl/
|
|
33
36
|
|
|
34
37
|
USER node
|
|
35
|
-
EXPOSE 3211
|
|
38
|
+
EXPOSE 3211 3216
|
|
36
39
|
ENTRYPOINT ["node", "/app/gateway.mjs"]
|