Skip to content

fix(file-transfer): expire authenticated sessions and drop nested runBlocking - #33

Merged
wangzhishou merged 2 commits into
wangzhishou:mainfrom
shiaho777:fix/file-transfer-session-expiry
Oct 7, 2026
Merged

wangzhishou merged 2 commits into
wangzhishou:mainfrom
shiaho777:fix/file-transfer-session-expiry

Conversation

@shiaho777

@shiaho777 shiaho777 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What changed

FileTransferServer kept authenticated sessions in a mutableSetOf<String>() that was only ever written to. Three changes in feature/file-transfer:

  • Sessions are stored as sessionId -> lastSeenAt in a ConcurrentHashMap with a 30 minute sliding expiry: every authenticated request refreshes the timestamp, and expired entries are pruned lazily on the request path, so no background job is needed.
  • MAX_SESSIONS = 64 caps the map and evicts the least recently seen entries, so browser reconnect churn cannot grow it without bound. The entry being validated is refreshed before pruning runs, so the cap can never pick the session that just authenticated and send that browser back to the password prompt.
  • updateConfig() clears the map when the password actually changes, so sessions issued under the previous password stop being accepted.
  • getChatHistoryByChannel() and listChatSessions() no longer wrap their Room queries in runBlocking(Dispatchers.IO). Both are called from NanoHTTPD worker threads, which are already off the main thread, so the second hop only borrowed an IO thread.

How it was verified

  • ./gradlew :feature:file-transfer:compileFossUniversalDebugKotlin — BUILD SUCCESSFUL
  • ./gradlew :app:assembleFossUniversalDebug — BUILD SUCCESSFUL
  • ./gradlew :feature:file-transfer:detekt — 193 findings, identical to the count on main; the change adds none. isAuthenticated(), refreshSession() and pruneExpiredSessions() all stay inside the default ReturnCount and complexity limits.

Not verified on a device: the expiry itself and the browser-side logout after a password change, both of which need a running file transfer service and two clients.

…Blocking

Authenticated sessions were kept in a plain set that was never pruned, and
changing the transfer password left every previously issued session cookie
valid. Track sessions as sessionId -> last seen timestamp with a 30 minute
sliding expiry, a cap of 64 entries, and clear the map when the password
changes.

getChatHistoryByChannel() and listChatSessions() also bridged NanoHTTPD
worker threads through runBlocking(Dispatchers.IO); both callers are already
off the main thread, so the extra hop is dropped.
pruneExpiredSessions() ran before the requesting session's timestamp was
refreshed, so at the session cap the entry chosen for eviction could be the
one being validated and that browser was sent back to the password prompt.
Refresh (or insert, in handleAuth) first and prune after: the freshly touched
entry has the newest timestamp, so the cap cannot pick it, and the map is no
longer one entry over the cap between an auth and the next request.
@wangzhishou
wangzhishou merged commit 4ec8898 into wangzhishou:main Oct 7, 2026
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