document editing web UI, hardening, and CODE 26.04 - #5
Open
cyberb wants to merge 1 commit into
Open
Conversation
cyberb
force-pushed
the
feat/wopi-editor-spa-and-hardening
branch
9 times, most recently
from
August 30, 2026 03:40
96ed840 to
d1d30c5
Compare
Collabora Online is a WOPI client with no storage of its own, so today this app is only usable through Nextcloud Office. Upstream has nothing to change that: the built-in WOPI host and its documents.html picker (browser/html/documents.html, /wopi/files, /wopi/documents) are compiled out of release builds behind `#if ENABLE_DEBUG` in wsd/FileServer.cpp, and local filesystem storage is gated the same way (`ENABLE_LOCAL_FILESYSTEM` is 0 unless MOBILEAPP || ENABLE_DEBUG, config.h.in). The `storage.filesystem` setting we already ship is therefore dead config. So the app becomes the WOPI host, mirroring the shape of the onlyoffice Syncloud app but with WOPI in place of the ONLYOFFICE JS config. Backend (cli/backend, bin/backend) Two listeners with different trust levels: - a unix socket for nginx, serving /api/* and /oidc/* behind an Authelia OIDC session (auth-code + PKCE, nonce, opaque session cookie); - 127.0.0.1:9981 for coolwsd, serving the WOPI host (CheckFileInfo, GetFile, PutFile, LOCK/UNLOCK/REFRESH_LOCK/GET_LOCK), authorised only by an HMAC-SHA256 access token whose payload binds the file id, the user and an expiry. The two muxes never share a route, so the WOPI endpoints are not reachable from the public proxy at all. coolwsd cannot call back over the public hostname: plenty of devices cannot resolve their own subdomains (NAT loopback), and the platform CA is not in any trust store coolwsd uses. Pointing WOPISrc at a loopback port sidesteps both, and coolwsd has no unix socket support to use instead. OIDC discovery is non-fatal and retried in the background, over the platform authelia socket first with X-Forwarded-Proto/Host set so the advertised issuer matches the public one, falling back to HTTPS with the platform CA. A failed discovery must never take the whole app down with it. Editor URLs come from coolwsd's own /hosting/discovery rather than a guessed /browser/<hash>/cool.html path, and are rebased onto the public app URL so the scheme and host are right regardless of what coolwsd derives from a loopback request. Frontend (web/) Vue 3 + Element Plus + Vite SPA: document list, create from blank docx/xlsx/ pptx templates, upload, delete, and an editor view that POSTs the access token into the iframe as a form rather than putting it in the URL. nginx serves the built dist; the Nextcloud paths (/browser, /hosting/*, /cool/*) stay unauthenticated exactly as before. Hardening - coolwsd bound 0.0.0.0:9980 (`net.listen = any`) with the admin console credentials hardcoded to admin/admin, so anyone on the LAN could reach the admin console and the REST API directly, bypassing nginx and Authelia. net.listen is now loopback and the password is generated at install time into both coolwsd.xml and the nginx Authorization header. - storage.wopi.alias_groups moves from mode="first" to mode="groups". "first" pins whichever WOPI host connects first, which with two hosts in play (ours and Nextcloud's) would break whichever came second. The groups allow our loopback host and any host on this device's domain. An off-device Nextcloud is no longer accepted by default; that is a deliberate narrowing. - mutable state moves out of $SNAP_COMMON (nginx pid, temp paths) into $SNAP_DATA, leaving only the web.socket the platform contract requires. CODE 26.04 collabora/code:26.04.x is a Nix-built distroless image: no /bin/sh, so Drone cannot use it as a step image at all (`exec: "/bin/sh": stat /bin/sh: no such file or directory`) and the in-image `cp -r /usr /lib` build cannot run. Its /usr and /lib are also a symlink farm pointing at absolute /nix/store paths, which would be dangling anywhere but that image. app/Dockerfile therefore resolves those symlinks in a debian stage that has /nix mounted at the same path, and app/build.sh extracts the result over dind. The bundled-loader launchers gain a shared loader.sh because the amd64 loader moved to /lib64 and would no longer be found by the old glob. CI and tests - every step now calls a committed script (cli/build.sh, cli/test.sh, ci/test.sh, web/build.sh); the redundant top-level build.sh is gone - nginx and cli binaries are smoke-tested on bookworm and buster - test/upgrade.py had its entire body commented out and ended in `pass`, so the green test-upgrade step asserted nothing. It now seeds a document before the refresh and checks it survives, with e2e specs on both sides - new Playwright specs cover login, the file list, editing docx/xlsx/pptx, a type/save/reopen round trip that unzips the docx and greps for the phrase, and the security boundaries; every screen is screenshotted into artifact/
cyberb
force-pushed
the
feat/wopi-editor-spa-and-hardening
branch
from
August 30, 2026 04:15
d1d30c5 to
a5d80cf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Covers points 2, 3 and 4 from the review of #4, plus the CODE 26.04 migration that #4 had to drop.
CI: build 105 green on amd64 (22/22) and arm64 (16/16), including the full upgrade chain.
Why a WOPI host at all
Collabora Online has no storage of its own, so the app is currently only usable through Nextcloud Office. Upstream cannot help: its built-in WOPI host and
documents.htmlpicker are compiled out of release builds —— and local filesystem storage is gated the same way (
ENABLE_LOCAL_FILESYSTEMis 0 unlessMOBILEAPP || ENABLE_DEBUG,config.h.in), which also means thestorage.filesystementry we already shipped never did anything. So the app becomes the WOPI host.Security model
/api/*,/oidc/*/wopi/files/*127.0.0.1:9981/browser,/hosting/*,/cool/*/browser/dist/admin,/cool/adminwsauth_request, unchanged/wopi/through the public proxyreturn 404Separate muxes, so WOPI is unreachable from outside.
07-security.spec.tsasserts all of it, including that 9980/9981 are loopback-IPv4 only and that the real WOPI host rejects a forged token.WOPISrc points at a loopback port because coolwsd is the one dialling: devices often cannot resolve their own subdomains (NAT loopback) and coolwsd will not trust the platform CA. coolwsd has no unix-socket support.
Hardening (point 2)
net.listen: any->loopback+net.proto: all->IPv4. coolwsd bound0.0.0.0:9980with the admin console atadmin/admin, reachable from the LAN bypassing nginx and Authelia. The password is now generated at install. (proto=IPv4is required: withall+loopbackcoolwsd binds[::1]only, which upstream warns about in a comment directly above the setting — it caused a 502 in build 96.)alias_groupsfirst->groups.firstpins whichever WOPI host connects first, which breaks with two hosts in play. Groups now allow our loopback host and any host on this device's domain — an off-device Nextcloud is no longer accepted by default.home_mode.enable = true.configure.ac; CODE is built--with-welcome-url, which makeshome_modethe applicable switch.$SNAP_COMMONinto$SNAP_DATA, leaving only the platform-contractweb.socket.always_save_on_exit = true, so closing the editor flushes through the WOPI host rather than waiting on idle/auto-save.CODE 26.04 (why #4 could not ship it)
collabora/code:26.04.xis a Nix-built distroless image with no/bin/sh— Drone cannot use it as a step image (build 92:exec: "/bin/sh": ... no such file or directory). Its/usrand/libare a symlink farm into absolute/nix/storepaths that dangle anywhere else.app/Dockerfileresolves them in a debian stage with/nixmounted at the same path;app/build.shextracts over dind. Launchers gained a sharedloader.shbecause the amd64 loader moved to/lib64.Guide gaps (point 3)
cli/build.sh,cli/test.sh,ci/test.sh,web/build.sh); redundant top-levelbuild.shremovednginx testandcli teston bookworm and bustersnap.yaml: droppedconfinement/plugs, addedrestart-delay: 10s,clientry, backup/restore hookspackage.shusesapt.shtest/upgrade.pyhad its whole body commented out and ended inpass— the green step asserted nothing. Replaced with a realtest-upgrade-prev->e2e-before-upgrade->test-upgrade->e2e-after-upgradechain, all passing.Review notes
samples/blank.{docx,xlsx,pptx}are binary blanks copied from theonlyofficeapp repo, needed to create documents from templates. Verified valid OOXML.config/backend.envis all{{ }}placeholders; OIDC secret, WOPI key and admin password are generated at install into$SNAP_DATA/secret/.08-pre-upgrade.spec.tsto the UI path.artifact/e2e*/playwright/<project>/screenshot/.