Skip to content

session: make rpc_session_get() a pure lookup and add "notouch" to session/access - #39

Open
micpf wants to merge 1 commit into
openwrt:masterfrom
micpf:idle-timeout-notouch
Open

session: make rpc_session_get() a pure lookup and add "notouch" to session/access#39
micpf wants to merge 1 commit into
openwrt:masterfrom
micpf:idle-timeout-notouch

Conversation

@micpf

@micpf micpf commented Aug 7, 2026

Copy link
Copy Markdown

Problem

The option sessiontime idle timeout in /etc/config/rpcd never fires when LuCI is open. Every LuCI page runs periodic Poll callbacks that issue ubus RPCs; every RPC goes through session/access, and every plugin (uci, luci-rpc, …) that calls the exported rpc_session_access() for its own ACL checks also touches the session via rpc_session_get(). The idle timer is refreshed continuously, so it behaves as an absolute session lifetime instead.

Reproduce (unpatched): uci set rpcd.@rpcd[0].sessiontime='30'; /etc/init.d/rpcd restart, log in to LuCI, leave the Overview page open. The session never expires.

Fix

Split lookup from touch:

  • rpc_session_get() becomes a pure AVL lookup with no side effects. Internal callers (uci/luci-rpc permission checks, session data accessors) no longer keep the session alive on their own.
  • The top-level session/access ubus method remains the single natural "keep alive" signal – it is invoked by uhttpd on every request. It refreshes the idle timer unless the caller passes the new optional notouch=true argument.

The companion uhttpd change forwards this hint when LuCI marks the request as a background poll (see openwrt/uhttpd#39), and LuCI marks its own polls (see openwrt/luci#8916).

notouch defaults to false, so existing callers see no behaviour change.

Threat model note

The notouch hint does not weaken the idle timer's security properties. Any code running with a valid session cookie can already keep the session alive by omitting the hint, or by synthesising fake user activity to trigger real XHRs. The idle timer defends against the "walked-away-from-the-terminal" case (unattended authenticated browser), not against code executing inside the authenticated origin. Today that defence is silently disabled by LuCI's polling; this PR restores it.

Related PRs

Land in order rpcd → uhttpd → luci. Each is independently ABI-compatible with older peers (notouch defaults to false; unknown query params ignored; unknown blob keys ignored).

…ssion/access

The rpcd session idle timer (sessiontime) is meant to expire a session
after N seconds of inactivity, but it is currently refreshed by every
"session get" and "session access" ubus call, and by every RPC in every
loadable plugin (uci, luci-rpc, ...) that calls rpc_session_access() to
verify permissions. Because LuCI polls its status pages every few
seconds, the timer is refreshed continuously and never expires: the
"idle timeout" behaves as an absolute session lifetime instead.

Split the two concerns:

  * rpc_session_get() becomes a pure AVL lookup with no side effects.
    All internal lookups (uci/luci-rpc permission checks, session data
    accessors, etc.) no longer keep the session alive on their own.

  * The top-level session/access ubus method is the only path that
    still touches the session, and it does so only when the new
    optional "notouch" boolean argument is absent or false. Callers
    that want to distinguish real user activity from background
    polling (uhttpd's /ubus/ handler when it sees LuCI's _luci_bg=1
    marker) pass notouch=1 and the timer keeps counting down.

The rpcd HTTP entry point (uhttpd) already invokes session/access on
every request to authorise the call, so it stays the single natural
"keep alive" signal for the session -- exactly what an idle timeout
needs -- without duplicating touches from every downstream plugin.

The "notouch" argument is optional and defaults to false, so existing
callers see no behaviour change.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/uhttpd that referenced this pull request Aug 10, 2026
Add optional client-side signalling to tell rpcd that the current
/ubus/ request is background activity (LuCI's periodic Poll refreshes
and status page reloads) that must not refresh the session idle
timer.  Together with the matching rpcd change (openwrt/rpcd#39),
this lets sessiontime work as documented for LuCI.

Parse "X-Ubus-No-Touch: 1" from the HTTP request headers alongside
the existing "Authorization" header, then forward the hint as the
new "notouch" boolean argument to session/access in two places:

  * uhttpd's internal ACL check (uh_ubus_allowed), which runs before
    every downstream RPC dispatched over /ubus/;
  * a direct session/access RPC invoked by the client itself (LuCI
    uses this to probe access-group ACLs from its own view code).

Only session/access is intercepted; other object/method pairs pass
through unmodified.  Header absent -> zero behavioural change.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/luci that referenced this pull request Aug 10, 2026
LuCI's Poll infrastructure keeps issuing /ubus/ RPCs from every open
tab so long as it is loaded, even when the user has walked away.
Those RPCs currently refresh the rpcd session idle timer, so the
"sessiontime" option in /etc/config/rpcd never expires the session
in practice.

Track when a Poll callback is on the JS stack via a Poll.tickDepth
counter, and set an "X-Ubus-No-Touch: 1" HTTP request header on any
XHR issued from within one.  The matching uhttpd (openwrt/uhttpd#39)
and rpcd (openwrt/rpcd#39) patches consume the hint and skip
refreshing the idle timer for those requests, so background polling
no longer masquerades as user activity.

Header-only signalling: URLs and RPC payloads are unchanged, and
requests from real user actions (form submits, direct clicks) are
outside any Poll.tick and therefore continue to touch the session
as before.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/uhttpd that referenced this pull request Aug 10, 2026
Add optional client-side signalling to tell rpcd that the current
/ubus/ request is background activity (LuCI's periodic Poll refreshes
and status page reloads) that must not refresh the session idle
timer.  Together with the matching rpcd change (openwrt/rpcd#39),
this lets sessiontime work as documented for LuCI.

Parse "X-Ubus-No-Touch: 1" from the HTTP request headers alongside
the existing "Authorization" header, then forward the hint as the
new "notouch" boolean argument to session/access in two places:

  * uhttpd's internal ACL check (uh_ubus_allowed), which runs before
    every downstream RPC dispatched over /ubus/;
  * a direct session/access RPC invoked by the client itself (LuCI
    uses this to probe access-group ACLs from its own view code).

Only session/access is intercepted; other object/method pairs pass
through unmodified.  Header absent -> zero behavioural change.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/uhttpd that referenced this pull request Aug 10, 2026
Add optional client-side signalling to tell rpcd that the current
/ubus/ request is background activity (LuCI's periodic Poll refreshes
and status page reloads) that must not refresh the session idle
timer.  Together with the matching rpcd change (openwrt/rpcd#39),
this lets sessiontime work as documented for LuCI.

Parse "X-Ubus-No-Touch: 1" from the HTTP request headers alongside
the existing "Authorization" header, then forward the hint as the
new "notouch" boolean argument to session/access in two places:

  * uhttpd's internal ACL check (uh_ubus_allowed), which runs before
    every downstream RPC dispatched over /ubus/;
  * a direct session/access RPC invoked by the client itself (LuCI
    uses this to probe access-group ACLs from its own view code).

Only session/access is intercepted; other object/method pairs pass
through unmodified.  Header absent -> zero behavioural change.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/luci that referenced this pull request Aug 11, 2026
LuCI's Poll infrastructure keeps issuing /ubus/ RPCs from every open
tab so long as it is loaded, even when the user has walked away.
Those RPCs currently refresh the rpcd session idle timer, so the
"sessiontime" option in /etc/config/rpcd never expires the session
in practice.

Track when a Poll.step() callback is synchronously on the JS stack
via a Poll.inCallback counter, propagate that as a per-request
opt.background flag in Request.request(), and set an
"X-Ubus-No-Touch: 1" HTTP request header on the outgoing XHR when
the flag is set.  For the batched RPC path in flushRequestQueue(),
the header is only set on the merged XHR when *every* queued entry
carries the flag, so a user-initiated ubus call that lands in the
same requestAnimationFrame batch behind a poll-initiated one still
touches the session.  The matching uhttpd (openwrt/uhttpd#39) and
rpcd (openwrt/rpcd#39) patches consume the hint and skip refreshing
the idle timer for those requests, so background polling no longer
masquerades as user activity.

Header-only signalling: URLs and RPC payloads are unchanged, and
requests from real user actions (form submits, direct clicks) are
outside any Poll.step() callback and therefore continue to touch
the session as before.

The Poll.inCallback counter is guarded with a try/catch around the
`Promise.resolve(e.fn())` construction so that a poll callback which
throws synchronously still decrements the counter and clears its
queue entry's in-flight flag, instead of permanently leaking the
counter (which would then tag every subsequent XHR, including real
user clicks, as background) and wedging that queue entry.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/luci that referenced this pull request Aug 12, 2026
LuCI's Poll infrastructure keeps issuing /ubus/ RPCs from every open
tab so long as it is loaded, even when the user has walked away.
Those RPCs currently refresh the rpcd session idle timer, so the
"sessiontime" option in /etc/config/rpcd never expires the session
in practice.

Track when a Poll.step() callback is in-flight (pending its async
completion) via a Poll.inCallback counter, propagate that as a
per-request opt.background flag in Request.request(), and set an
"X-Ubus-No-Touch: 1" HTTP request header on the outgoing XHR when
the flag is set.  For the batched RPC path in flushRequestQueue(),
the header is only set on the merged XHR when *every* queued entry
carries the flag, so a user-initiated ubus call that lands in the
same requestAnimationFrame batch behind a poll-initiated one still
touches the session; Request.request() does not override an explicit
background value supplied by flushRequestQueue().  The matching
uhttpd (openwrt/uhttpd#39) and rpcd (openwrt/rpcd#39) patches consume
the hint and skip refreshing the idle timer for those requests, so
background polling no longer masquerades as user activity.

Header-only signalling: URLs and RPC payloads are unchanged.
Requests that originate outside a pending Poll.step() callback
continue to touch the session as before; user-initiated requests
that happen to overlap an in-flight poll callback are therefore
also tagged as background.  This is an acceptable trade-off: the
window is bounded by the poll interval, and it is far preferable to
treating all poll XHRs as foreground.

The Poll.inCallback counter is guarded with a try/catch around the
`Promise.resolve(e.fn())` construction so that a poll callback which
throws synchronously still decrements the counter and clears its
queue entry's in-flight flag, instead of permanently leaking the
counter (which would then tag every subsequent XHR, including real
user clicks, as background) and wedging that queue entry.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/luci that referenced this pull request Aug 12, 2026
LuCI's Poll infrastructure keeps issuing /ubus/ RPCs from every open
tab so long as it is loaded, even when the user has walked away.
Those RPCs currently refresh the rpcd session idle timer, so the
"sessiontime" option in /etc/config/rpcd never expires the session
in practice.

Track when a Poll.step() callback is in-flight (pending its async
completion) via a Poll.inCallback counter, propagate that as a
per-request opt.background flag in Request.request(), and set an
"X-Ubus-No-Touch: 1" HTTP request header on the outgoing XHR when
the flag is set.  For the batched RPC path in flushRequestQueue(),
the header is only set on the merged XHR when *every* queued entry
carries the flag, so a user-initiated ubus call that lands in the
same requestAnimationFrame batch behind a poll-initiated one still
touches the session; Request.request() does not override an explicit
background value supplied by flushRequestQueue().  The matching
uhttpd (openwrt/uhttpd#39) and rpcd (openwrt/rpcd#39) patches consume
the hint and skip refreshing the idle timer for those requests, so
background polling no longer masquerades as user activity.

Header-only signalling: URLs and RPC payloads are unchanged.
Requests that originate outside a pending Poll.step() callback
continue to touch the session as before; user-initiated requests
that happen to overlap an in-flight poll callback are therefore
also tagged as background.  This is an acceptable trade-off: the
window is bounded by the poll interval, and it is far preferable to
treating all poll XHRs as foreground.

The Poll.inCallback counter is guarded with a try/catch around the
`Promise.resolve(e.fn())` construction so that a poll callback which
throws synchronously still decrements the counter and clears its
queue entry's in-flight flag, instead of permanently leaking the
counter (which would then tag every subsequent XHR, including real
user clicks, as background) and wedging that queue entry.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
micpf added a commit to micpf/luci that referenced this pull request Aug 12, 2026
LuCI's Poll infrastructure keeps issuing /ubus/ RPCs from every open
tab so long as it is loaded, even when the user has walked away.
Those RPCs currently refresh the rpcd session idle timer, so the
"sessiontime" option in /etc/config/rpcd never expires the session
in practice.

Add capture-phase document listeners for user-initiated event types
(click, submit, change, input, keydown, mousedown, touchstart) that
clear a Request.background flag for the synchronous duration of the
handler; a microtask resets it to true afterwards.  Request.request()
defaults opt.background from that flag when the caller does not set
it explicitly, so user-initiated RPCs leave background false (session
is touched) while all other requests — poll callbacks, page-load
RPCs — default to background true.

When background is true, an "X-Ubus-No-Touch: 1" header is set on
the outgoing XHR.  For the batched RPC path in flushRequestQueue()
the header is only set on the merged XHR when every queued entry
carries the flag, so a user-initiated RPC that lands in the same
requestAnimationFrame batch as a background one still touches the
session.

The matching uhttpd (openwrt/uhttpd#39) and rpcd (openwrt/rpcd#39)
patches consume the hint and skip refreshing the idle timer for
background requests, so autonomous polling no longer masquerades as
user activity.

Signed-off-by: Michael Pfeifroth <micpf@westermo.com>
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.

1 participant