Repository navigation
feat(fileeconnect): introduce Filee Connect for local device pairing and synchronization - #1307
rohitLovesKrishna wants to merge 4 commits into
Conversation
KnifeLemon
left a comment
There was a problem hiding this comment.
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)
- The pairing token isn't enforced yet.
/,/mobileand/connectserve the page with the real token injected into it (window.ACTIVE_TOKEN)./mobile/{token}never checks the token, and/api/sessionreturns it to anyone.ValidateAuthreturnstruewhen no token is sent./preview,/downloadand/folder.zipdon't check at all./wsaccepts an empty token.- Please drop
/api/sessionand the token injection, and reject every/api/*and/wsrequest that doesn't carry a valid device token. Compare tokens withCryptographicOperations.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.
- QR code: the QR carries a random pairing secret for this session.
- A paired device should show up in the Connect window with a way to disconnect it, which also revokes its token.
- Please remove the
Access-Control-Allow-*: *middleware and checkOrigin/Hoston/wsand on every state-changing request. The page and the API share one origin, so CORS isn't needed. Right now any web site can callhttp://localhost:3000/api/session, read the token, and then read the chat history or the shared files. - 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, sophoto.exesent asimage/pngshows 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, andcom.apple.quarantineon macOS, so the OS warns if someone runs one later. - Choose the icon on the server side from the same sanitized extension.
- Please bind to the chosen LAN address only (
options.Listen(lanIp, port)), notListenAnyIP, and refuse to start when that address isn't private (RFC 1918 / ULA / link-local). - Preview responses: set the MIME type on the server, and add
X-Content-Type-Options: nosniff,Content-Security-Policy: sandbox, andContent-Disposition: attachmentfor anything that isn't an image or a PDF. An uploaded.htmlor.svgcurrently runs in the Connect origin. - Please remove
UseDeveloperExceptionPage()and build the host withWebApplication.CreateSlimBuilder(new WebApplicationOptions { ContentRootPath = AppContext.BaseDirectory, EnvironmentName = "Production" }), so it doesn't readappsettings.jsonfrom the working directory orASPNETCORE_*variables. - Some smaller hardening:
- Stream
folder.zipstraight to the response instead of building it in aMemoryStream(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.
- Stream
Bugs
- Pressing the shortcut again ends the session.
ShowFileeConnect()callsStartSessionAsync()before it checks whether the window is already open. That restarts the server, and the queuedSessionEndedthen closes the window, whoseClosedhandler stops the new session too. Please check for the open window first and don't restart whileIsRunning. - A Ctrl+Alt drag can still fire the click. After the pointer leaves the 24 px box,
_armedbecomesnull, and the next move goes throughDetectLateModifier, 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 inMouseDown, and not arming it again during that press, should fix it. A test with several moves would help. - Concurrent
SendAsyncon one WebSocket isn't allowed.Broadcaststarts several at once, and the exception is swallowed, so messages get lost. A per-client send queue or aSemaphoreSlimwould fix it. _clientsis read without the lock inIsMobileConnectedand ininitial-statewhile 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 (
ReadFormAsyncis outside thetry). One request per file andFormOptions.MultipartBodyLengthLimitwould fix both. - The Windows
SetClipboardTextcallsOpenClipboard(NULL)and thenEmptyClipboard, and the docs saySetClipboardDatafails after that. It also leaks theGlobalAllochandle on failure. Because the view model already uses Avalonia'sIClipboard, I'd remove the new clipboard methods fromIPlatformServicesand the P/Invokes.
Keeping the PR to one thing
- The i18n files were reformatted: the blank lines between groups were removed everywhere, and
it.jsonwas re-indented to 4 spaces. That's where the −2060 lines come from, and it's why 20 of the 22 files conflict withmainnow. No existing key or value changed (I checked), but please rebase ontomainand 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.titleisn'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\SharedFolderafter 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.mdto 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-changesis 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.
…nd reconnection flow
ecf6557 to
ec1e6a8
Compare
efe9510b94e443e8b26928c66f0f4e42_compressed.mp4Hi @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
2. Network & CORS Hardening
3. File Execution Safety & Content Security
4. Integration, i18n & Tests
|
KnifeLemon
left a comment
There was a problem hiding this comment.
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/sessionand the token injection are gone, every route checks a device token withFixedTimeEquals, 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,
_clientsunder 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
-
Check
Host, not onlyOrigin(DNS rebinding).LocalWebServer.csonly refuses an emptyHost, and it comparesOriginwithHost. A web page onhttp://evil.example:3000can rebind its name to192.168.x.x. ThenOriginandHostare bothevil.example:3000, they match, and the page can call/api/pair/pinand read the token from the JSON. Please refuse any request whoseHostisn't exactly{lanIp}:{port}, then compareOriginwith that same value. -
The PIN lockout can be raced.
/api/pair/pinchecks 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.
-
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. -
Body size per endpoint.
MaxRequestBodySizeis 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.Parsein/api/sendalso sits outside atry. -
No tokens in query strings.
mobile.jsandshared-files.jsadd?token=to/ws,/download,/previewand/folder.zip, and the server accepts it. Query strings end up in history, logs andReferer. TheHttpOnlycookie already covers all of these, so please drop the query fallback on both sides. -
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.urlorx.library-mssits in the folder as a real shell file. Explorer reads an.scf'sIconFile=\\server\sharejust 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.svgand.xmloff the "open directly" list. An SVG opens in the browser fromfile://and can run script. -
/api/folder.zipwill most likely fail. It writes a synchronousZipArchivestraight intoResponse.Body. The local headers,entry.Open(), the entry and archiveDisposeall write synchronously, and Kestrel refuses synchronous I/O by default, so the download should break off with "Synchronous operations are disallowed". The tests only coverWriteFolderZipon aMemoryStream, and the endpoint doesn't use it. On .NET 10 you can useawait ZipArchive.CreateAsync(...),await entry.OpenAsync()andawait using. A test throughTestServer, or a real request, would catch this. -
The PC upload handlers can crash the app. The
async voidhandlers inFileeConnectWindow.axaml.cshave notry/catch. A full quota, more than 5000 files, or a name thatCleanRelativePathrefuses throws straight out ofasync void. Please catch and show a message. -
Two i18n keys are missing.
connect.picker_files_titleandconnect.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.
TryValidateDeviceTokenwrites_pairedDevices[key] = device. A request that runs at the same time asDisconnectDeviceAsyncputs the token back.TryUpdate(key, updated, current)fixes it. - The adapter choice.
GetLocalLanIptakes the first private IPv4 that's up, which is often a Hyper-V / WSL / DockervEthernet172.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.
_isRunningis set only after theawait, 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 (
DeleteItemis empty). LocalhostUrlpoints 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.
- The "×" delete button does nothing (
- Docs:
FILEE-CONNECT.mdmentions anX-Device-Tokenheader (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.mdhas the old storage path. - Token checking and the preview headers could move into
LocalWebServertoo, 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.
|
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
2. PIN Lockout & Rate Limiting Hardening
3. Single-Use QR Pairing Secret
4. Per-Endpoint Request Body Caps
5. Removed Query String Tokens
6. Neutral Storage for Off-Allowlist Files
7. Asynchronous Direct ZIP Streaming
8. UI & Concurrency Robustness
9. Localization & Documentation
|
KnifeLemon
left a comment
There was a problem hiding this comment.
Thanks, @rohitLovesKrishna, this round closes almost all of it. I went through 233d3271 file by file.
Done properly:
Hostis 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/sendparses inside atry. - No token in query strings, on either side.
- Off-allowlist files are stored as
{id}.bin, and.svg/.xmlare off the open list. /api/folder.zipis 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
-
Use the QR secret up atomically.
/mobile/{secret}reads_pairingSecret, compares it, then sets it tonull(FileeConnectService.cs:283-296). Two GETs at the same moment, say a link-preview bot and the phone, can both pair. Please consume it withInterlocked.CompareExchange(ref _pairingSecret, null, activeSecret), and pair only when that returnedactiveSecret. -
The WebSocket Origin check never runs.
context.WebSockets.IsWebSocketRequestis read in the middleware atLocalWebServer.cs:114, butapp.UseWebSockets(...)comes later (:151). Before that middleware the WebSocket feature isn't there, soisWsis alwaysfalseand/wsskips the Origin check. Please moveUseWebSocketsabove the check, or test theUpgrade: websocketheader there. -
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 thenResults.Redirect("/mobile"), asFILEE-CONNECT.mdalready describes. -
Show the errors to the user.
- The upload handlers in
FileeConnectWindow.axaml.cs(around:62and:96) now catch, which is good, but they onlyDebug.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.
- The upload handlers in
-
Docs, three places:
FILEE-CONNECT.md:35says the QR link redirects (true once 3 is done).FILEE-CONNECT.md:31says the cookie isHttpOnly, but the code setsHttpOnly = false.FILEE-CONNECT.md:63,:81andARCHITECTURE.md:672still 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-364asks for keys that/api/stringsdoesn't send, so only the "Text" tab changes language. - Cookie:
HttpOnlyagain, ifmobile.jscan work without reading it. It also rewrites the cookie with a 24 h max-age and copies the token tosessionStorage. 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.
…origin, httponly cookie, and error handling
|
Hi @KnifeLemon, thanks again for the thorough review! I have addressed all items in commit Must Fix
Follow-ups
|
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
Ctrl + Alt + Left Click(configurable in Settings -> Shortcuts).2. Streamlined Pairing Modal (QR Code)
Please make sure both devices are on the same network).3. Text Synchronization & Clipboard
4. Shared Folder File Transfer
5. Responsive Mobile Web Client
6. Internationalization (i18n)
src/Filee.App/Assets/i18n/*.json.7. Documentation
docs/FILEE-CONNECT.mdand linked it indocs/ARCHITECTURE.md.Verification
dotnet format --verify-no-changes --severity warnwith 0 warnings.tests/Filee.App.Tests/FileeConnectTests.csandtests/Filee.App.Tests/GestureDetectorTests.cs(all passed).build/build-installer.ps1 -Portable.ScreenShot:
