qubicl-cli 0.1.0 → 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.
@@ -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
- start_as_user env DISPLAY="$DISPLAY" x11vnc -display "$DISPLAY" -forever -shared -viewonly -localhost -nopw -rfbport 5900 -o /tmp/x11vnc-view.log
222
- start_as_user env DISPLAY="$DISPLAY" x11vnc -display "$DISPLAY" -forever -shared -localhost -nopw -rfbport 5901 -o /tmp/x11vnc-control.log
223
- start_as_user websockify --web=/usr/share/novnc 6080 localhost:5900
224
- start_as_user websockify 6081 localhost:5901
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.1.0",
70
- "revision": "960bfafcc5e114f8c518e4a931b5172b7277292c",
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.1.0",
74
- "revision": "960bfafcc5e114f8c518e4a931b5172b7277292c",
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.1.0",
40
- "revision": "960bfafcc5e114f8c518e4a931b5172b7277292c",
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.1.0",
76
- "revision": "960bfafcc5e114f8c518e4a931b5172b7277292c",
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"]