Skip to content

ubus: honour "X-Ubus-No-Touch: 1" HTTP header on /ubus/ - #39

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

ubus: honour "X-Ubus-No-Touch: 1" HTTP header on /ubus/#39
micpf wants to merge 1 commit into
openwrt:masterfrom
micpf:idle-timeout-luci-bg

Conversation

@micpf

@micpf micpf commented Aug 7, 2026

Copy link
Copy Markdown

Add optional client-side signalling that a /ubus/ request is background activity that must not refresh the rpcd session idle timer, so that the documented sessiontime option in /etc/config/rpcd finally expires an idle LuCI login.

How it works

  • Parse X-Ubus-No-Touch: 1 from the HTTP request headers alongside the existing Authorization header.
  • Forward the hint as the new optional notouch boolean argument to rpcd's session/access RPC, in two paths:
    • uhttpd's internal ACL check (uh_ubus_allowed), which runs before every downstream RPC dispatched over /ubus/.
    • A direct client-initiated session/access call — LuCI probes access-group ACLs from view code, and those calls must be marked too.
  • Only session/access is intercepted; every other object/method pair passes through unchanged. Header absent → zero behavioural change.

Companion patches

Design note (header vs. query parameter)

An earlier revision of this PR used a ?_luci_bg=1 query parameter and injected notouch=1 at uh_ubus_send_request unconditionally. The header form is cleaner and per @jow-'s review has been adopted. The switch also let us drop the wildcard notouch injection and scope it to session/access — everything else is now a straight pass-through.

Verified end-to-end on a t1023 (25.12) target with option sessiontime '30': a LuCI tab left focused and idle now correctly reaches expires:0 inside the timeout and shows the "Session expired" modal on the next Poll tick.

@jow-

jow- commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

I don't like the design, it's essentially hacking a LuCI specifc special case into the ubus HTTP gateway. My gut feeling is that this either should be an HTTP header (no query string, no ubus method call arg) or maybe even a property of the ubus ACL, shifting control entirely to the rpcd side and avoiding the need to expose any of that to uhttpd or LuCI while also paving the way for other potential flags in the future.

@micpf
micpf force-pushed the idle-timeout-luci-bg branch from 7cf2cf4 to 64d1c48 Compare August 10, 2026 14:03
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 micpf changed the title ubus: propagate _luci_bg=1 query flag as session/access notouch ubus: honour "X-Ubus-No-Touch: 1" HTTP header on /ubus/ Aug 10, 2026
@micpf

micpf commented Aug 10, 2026

Copy link
Copy Markdown
Author

Thanks for the review, @jow- — you're right that the HTTP header is the cleaner transport and I've reworked the PR accordingly. Force-pushed as 64d1c48.

A quick note on why the original revision used a query parameter and a wildcard notouch injection, in case it's useful for future context:

  • Query parameter over header: rationale was batch-safety and log-visibility. Some LuCI code paths batch multiple RPCs into a single /ubus/ POST; a per-batch header can only carry one hint for the whole batch, whereas a per-URL parameter makes the "background" nature of the batch explicit at every layer that later inspects request URLs (access logs, debug proxies, session dumps). That trade-off didn't outweigh the cost of grepping URLs in ubus.c though — the header route reuses the existing Authorization header parsing infrastructure, which is a much smaller diff.
  • Injecting notouch at uh_ubus_send_request unconditionally: originally the rpcd change (rpcd#39) still had rpc_session_get() touching, so we had to cover downstream RPCs too. Once rpcd was cleaned up so only the session/access method touches, injection could be scoped tightly. The revised patch only injects notouch=1 when uhttpd itself invokes session/access (either the internal uh_ubus_allowed probe or a client-initiated session/access RPC). All other methods pass through untouched.

End-to-end tested on a t1023 (25.12) target with option sessiontime '30': idle LuCI tab now correctly hits expires:0 inside the timeout and the "Session expired" modal fires on the next Poll tick.

@micpf
micpf force-pushed the idle-timeout-luci-bg branch from 64d1c48 to 65b4c81 Compare August 10, 2026 14:14
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
micpf force-pushed the idle-timeout-luci-bg branch from 65b4c81 to b869113 Compare August 10, 2026 14:17
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.

2 participants