8000 feat(shares): reach S3 and WebDAV through remote access, at /s3 and /dav by adeelahmad · Pull Request #402 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

feat(shares): reach S3 and WebDAV through remote access, at /s3 and /dav - #402

Merged
adeelahmad merged 12 commits into
mainfrom
claude/project-thread-ppx7di
Oct 17, 2026
Merged

adeelahmad merged 12 commits into
mainfrom
claude/project-thread-ppx7di

Conversation

@adeelahmad
@adeelahmad adeelahmad commented Oct 15, 2026 •
Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

Description

Before: the Cloudflare tunnel (Settings › Remote access) and HTTPS serve only the web app, and the web app forwards only the API, MCP and a few other paths. So the file shares (S3 on 9555, WebDAV on 9556) couldn't be reached from outside the network, and not at all on Cloudron, which publishes only the web app.

After: the web app relays them at its own address, S3 at /s3 and WebDAV at /dav, so they work wherever the web app opens. It's off until an admin turns on At the web app's address too in Settings › File shares (shares.remote, default off). Once it's on, a new access key's dialog gives https://<web app>/s3 and https://<web app>/dav/. Signing in is unchanged: a person's own access key, read-only.

MQTT, syslog, SMB and NFS aren't HTTP, so they stay on their own ports; docs/remote-access.md now has a table of what the tunnel reaches (the web app, API, MCP and HTTP sensor push already did). MQTT over WebSockets follows in its own PR.

How:

  • nextjs-frontend/lens-relay.js relays requests under /s3 and /dav before Next sees them, because Next answers only the usual methods (WebDAV needs PROPFIND) and redirects paths that end in a slash. lens-server.js uses it in the production image, and NODE_OPTIONS=--require adds it to next dev (dev compose) and next start (Cloudron).
  • The path and Host pass through unchanged, since S3 clients sign both. X-Forwarded-Prefix tells the share server the request came through the web app; it refuses such requests before sign-in unless shares.remote is on, strips the prefix, and puts it in WebDAV's links.
  • Failed WebDAV sign-ins are counted per visitor from X-Forwarded-For, as trusted proxies report it (server.trusted_proxies, LENS_TRUSTED_PROXY_HOSTS, loopback). That way one visitor can't lock out everyone who arrives through the web app. The LENS_TRUSTED_PROXY_HOSTS lookup moved from app/api/deps.py to ipgroups.proxy_hosts() so the worker can use it.
  • LENS_S3_URL and LENS_WEBDAV_URL say where the share servers are (http://worker:9555 and :9556 in every compose file). Unset, they default to this machine's ports, which fits Cloudron and microsandbox.

Motivation and Context

Adeel asked whether remote access also exposes S3, WebDAV, MCP and the other protocols, and to add what's missing. MCP already did; S3 and WebDAV didn't, and they can run over HTTPS with the same auth.

Steps to reproduce (if appropriate):

Verified locally with the real share servers over a seeded archive:

  • next dev with the preload, and lens-server.js: botocore listed buckets and objects, did a ranged get and fetched a presigned link at http://127.0.0.1:3000/s3.
  • rclone 1.75.2 ran lsd, ls and cat over both S3 (endpoint = …/s3) and WebDAV (url = …/dav/).
  • Raw PROPFIND on /dav/ and /dav/pods/Collections/General/ answered 207 with /dav/… hrefs, and normal pages still render.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change; ! in the title)

Checklist:

  • The title is a Conventional Commit.
  • My code follows the code style of this project (make hooks checks it on commit).
  • I added or updated tests.
  • My change requires documentation updates, and I have updated docs/ accordingly.
  • Users need more than the title to understand it, so I wrote it under ## Unreleased in CHANGELOG.md.

Done questions (AGENTS.md)

  • Goal: answers the question (MCP yes; S3 and WebDAV now yes; MQTT over WebSockets next; syslog, SMB and NFS can't go over an HTTP tunnel) and builds the feasible part. Merges on green.
  • Agents: PUT /api/v1/settings/shares {"remote": true} (admin), and the assistant's settings tool, which always asks first for shares. GET /api/v1/shares/s3 reports remote.
  • Zero setup: compose files set the share URLs; there's no new prerequisite or .env line.
  • Fewest clicks: one switch, and the key dialog fills in the right addresses.
  • Configurable: shares.remote is in Settings › File shares, default off, and not in the wizard.
  • Light: a few dozen lines of Node in the web app process, with no new service or dependency.
  • Private and secure: off by default; the same per-person read-only keys; refused before sign-in while off; throttled per visitor. The threat model (md, yaml, json) has the new entry point and setting, and the validator passes.
  • Cost: n/a, no model or external calls.
  • User decides: off by default, existing defaults unchanged, and the shares' own ports work as before.
  • Upgrades: the new settings key has a default and is merged on read, so no upgrade step is needed.
  • Honest: docs name the clients verified to take an endpoint with a path (rclone, the AWS CLI, boto3).
  • Tests: backend tests cover the relay path, the switch, and throttling per visitor; jest tests cover lens-relay.js. Docs are updated, and the OpenAPI schema and client were regenerated with tooling.
  • Open: S3 and WebDAV, with no new dependencies.
  • Refine later (docs/file-tree.md): virtual-host S3 addressing at the web app's address, for clients that can't take an endpoint with a path (MinIO's mc).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa

The Cloudflare tunnel and HTTPS serve only the web app, and the web app
forwarded only the API, MCP and a few other paths, so the file shares
(S3 on 9555, WebDAV on 9556) couldn't be reached from outside the
network, and not at all on Cloudron. Now the web app relays them at its
own address: /s3 to the S3 server and /dav to the WebDAV server.

Off until an admin turns on "At the web app's address too" in
Settings > File shares (shares.remote, default false). Until then the
share servers refuse requests that came through the web app before any
sign-in; the shares' own ports work as before.

Choices made:
- nextjs-frontend/lens-shares.js relays before Next sees the request:
  Next answers only the usual methods (WebDAV needs PROPFIND) and
  redirects paths ending in a slash. lens-server.js uses it in the
  production image; NODE_OPTIONS --require adds it to next dev (dev
  compose) and next start (Cloudron).
- Path and Host pass through unchanged, since S3 clients sign both;
  X-Forwarded-Prefix tells the share server the request came this way
  and which prefix to strip and to put in WebDAV's links.
- Failed WebDAV sign-ins are counted per visitor from X-Forwarded-For
  as trusted proxies report it (server.trusted_proxies,
  LENS_TRUSTED_PROXY_HOSTS, loopback), so one visitor can't lock out
  everyone arriving through the web app. The LENS_TRUSTED_PROXY_HOSTS
  lookup moved from app/api/deps.py to ipgroups.proxy_hosts() so the
  worker can use it.
- LENS_S3_URL / LENS_WEBDAV_URL say where the share servers are
  (http://worker:9555 and :9556 in every compose file); unset, this
  machine's ports, which fits Cloudron and microsandbox.
- GET /api/v1/shares/s3 gains `remote`; a new access key's dialog
  then gives https://<web app>/s3 and https://<web app>/dav/.
- MQTT, syslog, SMB and NFS aren't HTTP and stay on their own ports;
  docs/remote-access.md now lists what the tunnel reaches.
- Stored data: a new settings key with a default, merged on read; no
  upgrade step.

Verified: botocore (list, ranged get, presigned link) and rclone (S3
and WebDAV) through next dev with the preload and through
lens-server.js; backend tests use a relay that does what lens-shares.js
does.

Refine later (docs/file-tree.md): virtual-host S3 addressing at the web
app's address, for clients that can't take an endpoint with a path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
# Conflicts:
#	CHANGELOG.md
@adeelahmad adeelahmad self-assigned this Oct 15, 2026
…hares

The web app's relay will carry other servers Lens runs besides the API
(MQTT over WebSockets next), so its name says what it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
…-ppx7di

# Conflicts:
#	CHANGELOG.md
#	threat-model.yaml
…-ppx7di

# Conflicts:
#	CHANGELOG.md
#	threat-model.yaml
…-ppx7di

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
…-ppx7di

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
…them, so a cut connection is tried again

The microsandbox install stopped at "Getting Node 22" when the connection to
nodejs.org was cut partway: curl can't retry once it has written to a pipe, so
tar got half an archive. SurrealDB and Node are now downloaded to a file (curl
retries any error), Node's checked against its SHASUMS256, then unpacked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
adeelahmad added a commit that referenced this pull request Oct 15, 2026
…pages and notes as built (#424)

Move usage analytics (#274) and assets and people (#273) out of the
README's In progress list, and add what merged since #383: a namespace's
own MCP server, chat completions and one-namespace API keys; editing and
merging entities and topics with undo, from agents too; entity pages at
wiki addresses; Connect with Google and rclone sign-in on the server;
the notes tree, today's note, pins, search and Recently deleted; backups
that keep Lens's files; and grouped, searchable settings. In progress now
lists the open feature PRs (#398, #400, #402, #408, #419), and the docs
index links usage analytics.


Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS

Co-authored-by: Adeel Ahmad <noreply@anthropic.com>
…-ppx7di

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
…-ppx7di

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
…-ppx7di

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
…-ppx7di

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXpr1EWCfUD5tb1SmSwfDa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0