Skip to content

feat(fileeconnect): introduce Filee Connect for local device pairing and synchronization - #1307

Open
rohitLovesKrishna wants to merge 4 commits into
KnifeLemon:mainfrom
rohitLovesKrishna:feature/filee-connect
Open

rohitLovesKrishna wants to merge 4 commits into
KnifeLemon:mainfrom
rohitLovesKrishna:feature/filee-connect

Conversation

@rohitLovesKrishna

Copy link
Copy Markdown
Contributor
1794203752566442_compressed.mp4

Hi, @KnifeLemon , here is the new feature which supports our local/offline policy. No data out.

Overview

Introduces Filee Connect, enabling seamless, local zero-cloud pairing between Filee on desktop (Windows, macOS, Linux) and any mobile device (iOS / Android) on the same local Wi-Fi / LAN network.

No third-party cloud accounts, telemetry, or external relay servers are needed--communication is direct, fast, and entirely private.


Key Features & User Flow

1. Global Non-File Shortcut & Gesture Detection

  • Default Trigger: Ctrl + Alt + Left Click (configurable in Settings -> Shortcuts).
  • Collision-free: The shortcut functions anywhere across the screen, including over open application windows, but automatically yields when clicking directly over files-ensuring existing file drag-and-drop donut gestures continue uninterrupted.

2. Streamlined Pairing Modal (QR Code)

  • Selecting Filee Connect launches a centered modal with an embedded QR code, direct LAN URL, and network guidance (Please make sure both devices are on the same network).
  • Scanning the QR code or navigating to the IP URL in mobile Safari/Chrome immediately pairs the device.
  • As soon as the mobile device connects, the QR modal automatically closes and opens the Filee Connect management window.

3. Text Synchronization & Clipboard

  • Real-time Bidirectional Text: Send notes, URLs, or code snippets instantly between PC and mobile via WebSockets.
  • Clipboard Integration: One-click "Paste Clipboard Text" to read directly from the clipboard, and "Copy" buttons on each history message.
  • Session History: Retains the message stream for the duration of the session.

4. Shared Folder File Transfer

  • Dedicated Shared Folder tab for bidirectional local file exchange.
  • Upload files directly from mobile to the desktop's shared workspace, and browse/download files from the desktop on mobile with real-time status.

5. Responsive Mobile Web Client

  • Matches the official Filee desktop design language and typography.
  • Displays the official vector Filee logo.

6. Internationalization (i18n)

  • All new UI strings, triggers, and status messages are fully localized across all 22 supported languages in src/Filee.App/Assets/i18n/*.json.

7. Documentation

  • Added detailed architecture, pairing workflow, and network security documentation in docs/FILEE-CONNECT.md and linked it in docs/ARCHITECTURE.md.

Verification

  • Formatting: Passed dotnet format --verify-no-changes --severity warn with 0 warnings.
  • Unit Tests: Added test coverage in tests/Filee.App.Tests/FileeConnectTests.cs and tests/Filee.App.Tests/GestureDetectorTests.cs (all passed).
  • Build & Packaging: Built clean Windows release and portable installer via build/build-installer.ps1 -Portable.

ScreenShot:
Screenshot 2026-11-09 111827

@github-actions github-actions Bot added the feature New features label Nov 9, 2026

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @rohitLovesKrishna, thank you for this! Sending text and files from a phone without any cloud fits Filee's offline idea really well, and the mobile page looks clean. I read through the whole change, and I'd like to get it in. Before that, though, the network side needs to be much stricter: this PR opens a server that every device on the Wi-Fi (and every web page open in a browser) can reach, so we have to treat everything that comes in as untrusted.

Security (must fix)

  1. The pairing token isn't enforced yet.
    • /, /mobile and /connect serve the page with the real token injected into it (window.ACTIVE_TOKEN). /mobile/{token} never checks the token, and /api/session returns it to anyone.
    • ValidateAuth returns true when no token is sent. /preview, /download and /folder.zip don't check at all. /ws accepts an empty token.
    • Please drop /api/session and the token injection, and reject every /api/* and /ws request that doesn't carry a valid device token. Compare tokens with CryptographicOperations.FixedTimeEquals.
    • Pairing should work two ways:
      • QR code: the QR carries a random pairing secret for this session. /mobile/{secret} checks it (404 otherwise) and hands the phone its own device token.
      • PIN: the QR window also shows a short PIN (e.g. 6 digits) for typing the address by hand. The PIN must work like an OTP: it refreshes on a timer (e.g. every 30 seconds) and is used up as soon as one device pairs with it. After a few wrong tries, pause PIN pairing for a while, so nobody on the Wi-Fi can guess it.
    • A paired device should show up in the Connect window with a way to disconnect it, which also revokes its token.
  2. Please remove the Access-Control-Allow-*: * middleware and check Origin/Host on /ws and on every state-changing request. The page and the API share one origin, so CORS isn't needed. Right now any web site can call http://localhost:3000/api/session, read the token, and then read the chat history or the shared files.
  3. Please don't open received files with UseShellExecute. Clicking a file name in the Connect window runs the uploaded file (FileeConnectWindow.axaml.cs:27-39). The extension and MIME type both come from the sender, so photo.exe sent as image/png shows a picture icon and runs when clicked.
    • Safe types open directly: pictures, PDF, plain text, audio and video. Decide the type on the PC from a sanitized extension, not from the sender's MIME type.
    • Everything else is revealed in the file manager (selected in Explorer / Finder / the Linux file manager) instead of being opened. That covers programs, scripts, shortcuts and anything unknown.
    • Mark files that came from the phone with a Zone.Identifier (Internet zone) on Windows, and com.apple.quarantine on macOS, so the OS warns if someone runs one later.
    • Choose the icon on the server side from the same sanitized extension.
  4. Please bind to the chosen LAN address only (options.Listen(lanIp, port)), not ListenAnyIP, and refuse to start when that address isn't private (RFC 1918 / ULA / link-local).
  5. Preview responses: set the MIME type on the server, and add X-Content-Type-Options: nosniff, Content-Security-Policy: sandbox, and Content-Disposition: attachment for anything that isn't an image or a PDF. An uploaded .html or .svg currently runs in the Connect origin.
  6. Please remove UseDeveloperExceptionPage() and build the host with WebApplication.CreateSlimBuilder(new WebApplicationOptions { ContentRootPath = AppContext.BaseDirectory, EnvironmentName = "Production" }), so it doesn't read appsettings.json from the working directory or ASPNETCORE_* variables.
  7. Some smaller hardening:
    • Stream folder.zip straight to the response instead of building it in a MemoryStream (it can reach 2 GB).
    • Reserve the quota under the lock.
    • Cap WebSocket message size.
    • Don't let a client end the session for everyone or choose its own role.

Bugs

  • Pressing the shortcut again ends the session. ShowFileeConnect() calls StartSessionAsync() before it checks whether the window is already open. That restarts the server, and the queued SessionEnded then closes the window, whose Closed handler stops the new session too. Please check for the open window first and don't restart while IsRunning.
  • A Ctrl+Alt drag can still fire the click. After the pointer leaves the 24 px box, _armed becomes null, and the next move goes through DetectLateModifier, which arms the click again at the new spot. Releasing there opens the donut. The test only sends one move, so it passes. Arming the click only in MouseDown, and not arming it again during that press, should fix it. A test with several moves would help.
  • Concurrent SendAsync on one WebSocket isn't allowed. Broadcast starts several at once, and the exception is swallowed, so messages get lost. A per-client send queue or a SemaphoreSlim would fix it.
  • _clients is read without the lock in IsMobileConnected and in initial-state while Kestrel threads change it.
  • Upload limits: the phone sends every selected file in one request, but the request limit is 210 MB. Multipart's default 128 MB limit also makes 128–200 MB files fail with a 500 (ReadFormAsync is outside the try). One request per file and FormOptions.MultipartBodyLengthLimit would fix both.
  • The Windows SetClipboardText calls OpenClipboard(NULL) and then EmptyClipboard, and the docs say SetClipboardData fails after that. It also leaks the GlobalAlloc handle on failure. Because the view model already uses Avalonia's IClipboard, I'd remove the new clipboard methods from IPlatformServices and the P/Invokes.

Keeping the PR to one thing

  • The i18n files were reformatted: the blank lines between groups were removed everywhere, and it.json was re-indented to 4 spaces. That's where the −2060 lines come from, and it's why 20 of the 22 files conflict with main now. No existing key or value changed (I checked), but please rebase onto main and add only the new keys.
  • WindowsPlatformServices.CurrentModifiers() changes how every existing mouse gesture reads modifiers on Windows (the hook mask was used before). Please leave it out, or explain in a separate PR why it's needed.
  • App.axaml.cs: a Hold or keyboard shortcut set to "Anywhere" that finds no selected files now opens Filee Connect instead of doing nothing. Please keep the click gesture as the only way in.

Smaller things

  • Hard-coded English in the new windows:

    • "Session Messages", the input placeholder, "Shared folder", "↑ Up", "View", "Stored locally on this PC" and "Wi-Fi:".
    • "Copy"/"Copied!", "This PC", "Phone", "Folder", "N items", "Waiting for mobile…" and "{0} Connected" in the view model.
    • The file picker titles.

    Please move them into the i18n files (all 22). connect.title isn't used yet.

  • Please add QRCoder (MIT) to THIRD-PARTY-NOTICES.md.

  • Kestrel stays inside the app. It adds some size, but its security fixes then ship with each Filee release. Please keep the server host (Kestrel setup, binding, auth and headers) in its own class, separate from the Connect features (text, files), so other local-web features can reuse it later.

  • The new default gesture only reaches new installs. Existing settings keep their own list, so the docs shouldn't call it the default for everyone. On macOS, Ctrl+click is a secondary click, so the default there should avoid Ctrl.

  • Shared files stay in %LOCALAPPDATA%\Filee\SharedFolder after the session ends, and the next device that connects sees all of them. I'd clear the folder at the end of a session, or at least ask. Please also follow Filee's data directory, so portable mode works.

  • There's no hint when Windows Firewall blocks the port. It's worth a line in the QR window.

  • Please update docs/FILEE-CONNECT.md to match the code once the fixes are in: the storage path, the binding, the token rules, and the ZIP streaming.

Build and tests

  • On Windows here, the PR builds without errors, and dotnet format --verify-no-changes is clean.
  • Filee.App.Tests: 2195 passed and 1 failed. The failure is an engines-page test, and it comes from a Tesseract install on my machine, so it isn't related to this PR. Your new tests pass.
  • I added a quick local test with many moves for the Ctrl+Alt drag case. It fails (1 click instead of 0), which matches the bug above.
  • CI hasn't run yet because of the merge conflicts.

Thanks again. This is a feature I'd really like to have, and I'm happy to help with any of the points above if something is unclear.

@rohitLovesKrishna

Copy link
Copy Markdown
Contributor Author
efe9510b94e443e8b26928c66f0f4e42_compressed.mp4

Hi @KnifeLemon,

Thank you for the thorough review and valuable security recommendations! We have completely overhauled the networking, pairing, and file-handling layers to treat the local network as untrusted.

Here is a breakdown of what has been implemented and addressed in this update:

1. Authentication & Pairing Hardening

  • Dropped /api/session & Token Injection: Removed /api/session and completely removed window.ACTIVE_TOKEN injection from mobile.html. The client page is served statically without any credentials.
  • Dual Pairing Model:
    • QR Code Pairing: The QR code encodes a single-session cryptographically random secret (/mobile/{secret}). Accessing this verifies the secret in constant time using CryptographicOperations.FixedTimeEquals and hands the mobile client its own scoped device token.
    • 6-Digit OTP PIN: For typing the address by hand, users can open http://<lan-ip>:<port> directly in any mobile browser, which prompts for the 6-digit PIN shown on the desktop screen. The PIN functions as an OTP: it auto-refreshes every 30 seconds and is consumed and renewed immediately once a device pairs. Wrong entries trigger a 5-attempt rate-limiting lockout (2 minutes pause) to prevent Wi-Fi brute-force attempts.
  • Strict Device Token Enforcement: All /api/*, /preview, /download, /folder.zip, and /ws endpoints now reject requests lacking a valid, active device token with 401 Unauthorized or WebSocket closure code 4001.
  • Disconnect & Reconnection Flow: Desktop users can click Disconnect at any time to instantly revoke the device's token and close its connection. Once disconnected, the button seamlessly switches to Connect Again, which opens an in-window QR/PIN overlay without resetting or clearing the active session's message history or shared files.

2. Network & CORS Hardening

  • Removed Wildcard CORS: Removed Access-Control-Allow-*: *.
  • Host & Origin Verification: Enforced strict Host and Origin validation on WebSockets (/ws) and state-changing HTTP requests.
  • Private LAN IP Binding: Bound Kestrel strictly to the private LAN IP (options.Listen(lanIp, port)), refusing non-private IPs (RFC 1918 / ULA / link-local).
  • Slim Host: Built using WebApplication.CreateSlimBuilder(new WebApplicationOptions { ContentRootPath = AppContext.BaseDirectory, EnvironmentName = "Production" }) with developer exception pages removed.

3. File Execution Safety & Content Security

  • No Blind Shell Execution: Received files are never opened directly with UseShellExecute. Safe types (images, PDF, plain text, audio, and video) open directly based on a sanitized extension verified on the PC.
  • File Manager Reveal for Untrusted Types: Programs, scripts (.exe, .bat, .cmd, .ps1, .sh, etc.), shortcuts, and unknown formats are revealed and selected in the system file manager (Explorer / Finder / Linux file manager) instead of being executed.
  • Mark of the Web & Quarantine: Received files are automatically tagged with Zone.Identifier (Internet zone 3) on Windows and com.apple.quarantine on macOS.
  • Preview Headers: Previews enforce X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox, with Content-Disposition: attachment applied to non-inline formats.
  • Streaming ZIP: The /api/folder.zip endpoint streams the archive directly into the HTTP response stream without buffering gigabytes into RAM.

4. Integration, i18n & Tests

  • Uses Avalonia's built-in clipboard service exclusively.
  • Added all keys to all 22 localized language files.
  • Added comprehensive unit tests covering token validation, constant-time secret comparison, PIN consumption and lockout, streaming ZIP generation, path traversal guards, and file execution safety filters.

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, @rohitLovesKrishna, this is a big step forward! I went through ec1e6a81 file by file, and most of the list is done properly:

  • /api/session and the token injection are gone, every route checks a device token with FixedTimeEquals, and the PIN rotates and is used up.
  • The CORS middleware is gone, Kestrel binds the LAN address only, and the host is slim with no exception page.
  • The allowlist is decided on the PC, other files are revealed instead of opened, and Mark of the Web and quarantine are written.
  • The preview headers are right.
  • Quota under the lock, the 64 KB WebSocket cap, and the server-assigned role.
  • All the bugs are fixed: shortcut, Ctrl+Alt drag (thanks for the multi-move test), the per-client send lock, _clients under the lock, one upload per file, Avalonia's clipboard.
  • The i18n files only add keys now, and CurrentModifiers() and the App.axaml.cs fallback are out.
  • QRCoder is in the notices, and the shared folder follows Filee's data directory.

What's left is mostly about the network edge. Please fix these before merging.

Must fix

  1. Check Host, not only Origin (DNS rebinding). LocalWebServer.cs only refuses an empty Host, and it compares Origin with Host. A web page on http://evil.example:3000 can rebind its name to 192.168.x.x. Then Origin and Host are both evil.example:3000, they match, and the page can call /api/pair/pin and read the token from the JSON. Please refuse any request whose Host isn't exactly {lanIp}:{port}, then compare Origin with that same value.

  2. The PIN lockout can be raced. /api/pair/pin checks the lockout before it reads the body, and _failedPinAttempts++ isn't locked. Someone can open many requests, hold their bodies, and release them all within one PIN window, so every guess gets tried. Please:

    • read the body first, with a small limit (see 4);
    • do the lockout check, the compare, the counting and RefreshPin() together under one lock;
    • allow only a few pairing requests at a time.
  3. The QR secret works for the whole session. Every GET of /mobile/{secret} pairs another device, with no limit. Link previews, a browser's prefetch, clipboard history (Copy Link), or a photo of the screen all give lasting access. Please use the secret up when a device pairs, and make a new one for "Connect Again". A cap on paired devices would help too.

  4. Body size per endpoint. MaxRequestBodySize is 510 MB for every route, including /api/pair/pin, which needs no token, and /api/send. Both read the whole body into memory. Please keep the big limit only on the upload route (IHttpMaxRequestBodySizeFeature, or [RequestSizeLimit]/.WithMetadata(new RequestSizeLimitAttribute(...))) and set a few KB as the default. JsonDocument.Parse in /api/send also sits outside a try.

  5. No tokens in query strings. mobile.js and shared-files.js add ?token= to /ws, /download, /preview and /folder.zip, and the server accepts it. Query strings end up in history, logs and Referer. The HttpOnly cookie already covers all of these, so please drop the query fallback on both sides.

  6. Don't keep risky extensions on disk. Files that aren't on the allowlist are stored under their real name, so a received x.scf, x.url or x.library-ms sits in the folder as a real shell file. Explorer reads an .scf's IconFile=\\server\share just by showing the folder ("Open Folder" or the reveal), and that sends the user's Windows sign-in hash to that server. Please store anything off the allowlist under a neutral name (e.g. {id}.bin) and give it its real name only when the user saves it somewhere. Please also take .svg and .xml off the "open directly" list. An SVG opens in the browser from file:// and can run script.

  7. /api/folder.zip will most likely fail. It writes a synchronous ZipArchive straight into Response.Body. The local headers, entry.Open(), the entry and archive Dispose all write synchronously, and Kestrel refuses synchronous I/O by default, so the download should break off with "Synchronous operations are disallowed". The tests only cover WriteFolderZip on a MemoryStream, and the endpoint doesn't use it. On .NET 10 you can use await ZipArchive.CreateAsync(...), await entry.OpenAsync() and await using. A test through TestServer, or a real request, would catch this.

  8. The PC upload handlers can crash the app. The async void handlers in FileeConnectWindow.axaml.cs have no try/catch. A full quota, more than 5000 files, or a name that CleanRelativePath refuses throws straight out of async void. Please catch and show a message.

  9. Two i18n keys are missing. connect.picker_files_title and connect.picker_folder_title (FileeConnectWindow.axaml.cs:42, :67) aren't in any of the 22 files.

Should fix

  • The lockout is global. Five wrong PINs every 2 minutes from any device keep PIN pairing off for the real user. A per-client counter, or a back-off that grows per address, would avoid that.
  • A revoked token can come back. TryValidateDeviceToken writes _pairedDevices[key] = device. A request that runs at the same time as DisconnectDeviceAsync puts the token back. TryUpdate(key, updated, current) fixes it.
  • The adapter choice. GetLocalLanIp takes the first private IPv4 that's up, which is often a Hyper-V / WSL / Docker vEthernet 172.x adapter. The QR then points somewhere the phone can't reach. Preferring the adapter with the default gateway (and Wi-Fi/Ethernet types) would fix most cases. If only loopback is found, please refuse to start, as the docs say.
  • A double press can start two sessions. _isRunning is set only after the await, so two quick presses start two servers. The first one keeps listening after End Session.
  • Leftover files after a crash. SharedFolderManager.Initialize() loads the files of an earlier session and shows them to the next device. Clearing the folder on start too would match the "cleared at the end" promise.
  • Still in English: "PIN:" (both windows), "Local Network", the device names, and the strings in mobile.js / shared-files.js. The mobile page could take its strings from a small /api/strings (after pairing) in the PC's language.
  • Smaller:
    • The "×" delete button does nothing (DeleteItem is empty).
    • LocalhostUrl points at localhost, but the server no longer listens there.
    • The PIN countdown only changes when the PIN refreshes.
    • The firewall hint names Windows Defender Firewall on macOS and Linux too.
  • Docs: FILEE-CONNECT.md mentions an X-Device-Token header (the code reads Bearer/cookie/query), says /mobile/{secret} redirects, and says deletions sync, which doesn't happen. It still presents Ctrl+Alt as everyone's shortcut. ARCHITECTURE.md has the old storage path.
  • Token checking and the preview headers could move into LocalWebServer too, so a later local-web feature gets them for free. This one isn't urgent.

The branch merges cleanly with main, and CI is running now. Thanks again for turning this around so quickly. It's close, and these points are mostly a few lines each.

@rohitLovesKrishna

Copy link
Copy Markdown
Contributor Author

Hi @KnifeLemon, thank you for the thorough follow-up review! I have addressed all the items from your list:

1. Host Validation & DNS Rebinding Protection

  • Enforced strict Host validation in LocalWebServer.cs: any request whose Host header does not match {lanIp}:{port} is rejected immediately with 400 Bad Request.
  • Verified that Origin matches {lanIp}:{port} on all state-changing endpoints and WebSockets (/ws).

2. PIN Lockout & Rate Limiting Hardening

  • Replaced the global failed attempt counter with per-client IP rate limiting (_pinAttemptsByIp), ensuring a noisy device on the Wi-Fi cannot lock out the legitimate user.
  • Enforced a 2 KB body limit upfront, constrained concurrency with a semaphore (_pinPairSemaphore = new(3, 3)), and grouped the lockout check, constant-time compare (CryptographicOperations.FixedTimeEquals), attempt tracking, and RefreshPin() under lock (_pinLock).

3. Single-Use QR Pairing Secret

  • The QR pairing secret is now single-use: /mobile/{secret} sets _pairingSecret = null upon pairing, causing subsequent requests to return 404 Not Found.
  • Clicking Connect Again generates a fresh pairing secret via GenerateNewPairingSecret().
  • Added a cap of 5 paired devices per session.

4. Per-Endpoint Request Body Caps

  • Set Kestrel's default MaxRequestBodySize to 64 KB.
  • Only /api/upload requests the full 510 MB limit dynamically via IHttpMaxRequestBodySizeFeature.
  • Wrapped body reading and JsonDocument.Parse in /api/send inside a try/catch block.

5. Removed Query String Tokens

  • Dropped all ?token= query string lookups from /ws, /download, /preview, and /folder.zip on both client (mobile.js, shared-files.js) and server.
  • Session tokens are handled strictly via SameSite=Lax cookies or Authorization: Bearer headers.

6. Neutral Storage for Off-Allowlist Files

  • Files outside the safe allowlist are now saved on disk as {id}.bin while preserving their actual names and icons in session metadata. This prevents Explorer from evaluating .scf, .url, or .library-ms files and leaking NTLM hashes.
  • Removed .svg and .xml from the direct execution allowlist.

7. Asynchronous Direct ZIP Streaming

  • Updated /api/folder.zip to use .NET 10 async compression APIs (await ZipArchive.CreateAsync(...) and await entry.OpenAsync(...)), avoiding synchronous writes to Response.Body.

8. UI & Concurrency Robustness

  • Wrapped async void picker handlers (OnUploadFilesClick and OnUploadFolderClick) in try/catch to guard against unhandled UI crashes.
  • Guarded StartSessionAsync and StopSessionAsync with SemaphoreSlim _startLock to eliminate double-start races.
  • Token updates now use _pairedDevices.TryUpdate to avoid resurrecting revoked tokens.
  • Scoring in GetLocalLanIp() prioritizes physical Wi-Fi/Ethernet adapters with a default gateway over virtual adapters (WSL, Docker, Hyper-V).
  • Initialized SharedFolderManager to wipe leftover storage and temp directories on launch.
  • Removed LocalhostUrl.
  • Updated the PIN countdown timer every second on the UI thread via PinTicked.

9. Localization & Documentation

  • Appended connect.picker_files_title, connect.picker_folder_title, connect.pin_label, and connect.firewall_hint_generic across all 22 language JSON files preserving existing keys and formatting.
  • Added /api/strings so the mobile interface adapts to the host's selected language.
  • Updated docs/FILEE-CONNECT.md and docs/ARCHITECTURE.md.
  • Expanded unit tests in FileeConnectTests.cs (all 16 tests passing, format verified).

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @rohitLovesKrishna, this round closes almost all of it. I went through 233d3271 file by file.

Done properly:

  • Host is compared exactly with {lanIp}:{port} on every request.
  • The PIN body is read first with a small cap, and the lockout, the compare, the counting and RefreshPin() share one lock. Pairing is limited to 3 at a time, and the lockout is per address.
  • 64 KB is the default body size, and only the upload route raises it. /api/send parses inside a try.
  • No token in query strings, on either side.
  • Off-allowlist files are stored as {id}.bin, and .svg / .xml are off the open list.
  • /api/folder.zip is fully async.
  • All the new keys are in the 22 files.
  • From the should-fix list: TryUpdate, the adapter scoring with the loopback refusal, the start/stop lock, the wipe at start, ×, the countdown and the generic firewall hint.

Five small things are left before merging.

Must fix

  1. Use the QR secret up atomically. /mobile/{secret} reads _pairingSecret, compares it, then sets it to null (FileeConnectService.cs:283-296). Two GETs at the same moment, say a link-preview bot and the phone, can both pair. Please consume it with Interlocked.CompareExchange(ref _pairingSecret, null, activeSecret), and pair only when that returned activeSecret.

  2. The WebSocket Origin check never runs. context.WebSockets.IsWebSocketRequest is read in the middleware at LocalWebServer.cs:114, but app.UseWebSockets(...) comes later (:151). Before that middleware the WebSocket feature isn't there, so isWs is always false and /ws skips the Origin check. Please move UseWebSockets above the check, or test the Upgrade: websocket header there.

  3. A reload after QR pairing shows "Not Found". The phone stays on /mobile/{secret}, and that secret is now used up, so a reload, or the browser restoring the tab, gets a 404. Please set the cookie and then Results.Redirect("/mobile"), as FILEE-CONNECT.md already describes.

  4. Show the errors to the user.

    • The upload handlers in FileeConnectWindow.axaml.cs (around :62 and :96) now catch, which is good, but they only Debug.WriteLine. The user sees nothing, and one failure silently skips the rest of the files. Please show a message, and carry on with the next file where that makes sense.
    • "No Wi-Fi or Ethernet" at session start only goes to the log (WindowService.cs:120), so the click does nothing. A message there too.
  5. Docs, three places:

    • FILEE-CONNECT.md:35 says the QR link redirects (true once 3 is done).
    • FILEE-CONNECT.md:31 says the cookie is HttpOnly, but the code sets HttpOnly = false.
    • FILEE-CONNECT.md:63, :81 and ARCHITECTURE.md:672 still give Ctrl+Alt+click as everyone's shortcut. The macOS default is Alt+Shift, and it only applies to new installs.

Fine as follow-ups (a separate PR is OK)

  • Mobile strings: mobile.js:349-364 asks for keys that /api/strings doesn't send, so only the "Text" tab changes language.
  • Cookie: HttpOnly again, if mobile.js can work without reading it. It also rewrites the cookie with a 24 h max-age and copies the token to sessionStorage. The cookie alone would be enough.
  • Pairing edges:
    • the QR secret stays valid when the phone pairs by PIN instead;
    • a device pushed out by the 5-device cap keeps its WebSocket open;
    • slow PIN bodies can hold all 3 slots (a short read timeout helps).
  • Upload: the upload route raises the body limit before it checks the token. Checking first is cheaper.
  • Off-allowlist files: a Save as… for them, so the PC user doesn't only ever see {guid}.bin.
  • Smaller:
    • the old QR bitmap isn't disposed on Connect Again;
    • leftover FileeConnect_{guid} temp folders from a crash aren't cleaned;
    • a double press can open two QR windows;
    • an HTTP test for the zip download and for an upload over 64 KB would cover the Kestrel path.

CI is green and the branch merges cleanly, so once these five are in, I'll merge it.

@rohitLovesKrishna

Copy link
Copy Markdown
Contributor Author

Hi @KnifeLemon, thanks again for the thorough review!

I have addressed all items in commit 84bcfe94:

Must Fix

  1. Atomic QR secret consumption: /mobile/{secret} now consumes the active pairing secret via Interlocked.CompareExchange(ref _pairingSecret, null, activeSecret). Pairing only occurs if the secret matched atomically, preventing race conditions from concurrent GET requests.
  2. WebSocket Origin check order: Moved app.UseWebSockets(...) ahead of the Host & Origin verification middleware in LocalWebServer.cs and added an explicit Upgrade: websocket check so WebSocket connections cannot bypass Origin enforcement.
  3. Redirect after QR pairing: /mobile/{secret} sets the session cookie and returns Results.Redirect("/mobile") rather than rendering inline HTML. Reloading or restoring tabs now cleanly loads /mobile.
  4. User-facing error dialogs:
    • FileeConnectWindow.axaml.cs now isolates errors per file when uploading files or folders, continues processing remaining files, and displays a summary dialog of any failures to the user.
    • WindowService.cs shows a dialog if session startup fails due to no active Wi-Fi/Ethernet adapter.
  5. Documentation updates: Updated docs/FILEE-CONNECT.md and docs/ARCHITECTURE.md to document the /mobile redirect, HttpOnly cookies, and clarified shortcuts (Ctrl+Alt+Click on Windows/Linux, ⌥ Option + ⇧ Shift + Click on macOS on new installs, configurable in Settings).

Follow-ups

  1. Mobile strings: Aligned /api/strings in FileeConnectService.cs with the keys expected by mobile.js.
  2. HttpOnly auth cookie: The filee_token cookie is marked HttpOnly = true and SameSite = SameSiteMode.Lax. Removed client-side cookie reading and token manipulation in mobile.js and shared-files.js.
  3. Pairing edge cases:
    • Successful PIN pairing resets _pairingSecret = null.
    • Evicting devices when exceeding the 5-device cap closes the evicted device's WebSocket with status code 4001.
    • PIN request body read has a 5-second linked cancellation token timeout.
  4. Upload token check: Validates the device token before raising MaxRequestBodySize to 510 MB on /api/upload.
  5. "Save as…" button: Added a "Save as…" action on file rows in FileeConnectWindow.axaml allowing users to save off-allowlist files via StorageProvider.SaveFilePickerAsync.
  6. Cleanups: Old _qrBitmap instances are disposed on regenerate/Connect Again, leftover FileeConnect_* temporary directories in Path.GetTempPath() are purged on initialization, and WindowService.ShowFileeConnect() is guarded with an atomic flag against double-activation.
  7. Tests: Added end-to-end tests for folder zip streaming, upload > 64 KB, unauthorized upload rejection, and WebSocket origin mismatch rejection. All 20 tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants