ubus: honour "X-Ubus-No-Touch: 1" HTTP header on /ubus/ - #39
Conversation
|
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. |
7cf2cf4 to
64d1c48
Compare
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>
|
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 A quick note on why the original revision used a query parameter and a wildcard
End-to-end tested on a t1023 (25.12) target with |
64d1c48 to
65b4c81
Compare
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>
65b4c81 to
b869113
Compare
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>
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>
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>
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>
Add optional client-side signalling that a
/ubus/request is background activity that must not refresh the rpcd session idle timer, so that the documentedsessiontimeoption in/etc/config/rpcdfinally expires an idle LuCI login.How it works
X-Ubus-No-Touch: 1from the HTTP request headers alongside the existingAuthorizationheader.notouchboolean argument to rpcd'ssession/accessRPC, in two paths:uh_ubus_allowed), which runs before every downstream RPC dispatched over/ubus/.session/accesscall — LuCI probes access-group ACLs from view code, and those calls must be marked too.session/accessis intercepted; every other object/method pair passes through unchanged. Header absent → zero behavioural change.Companion patches
notouchpolicy field;rpc_session_getbecomes a pure lookup).X-Ubus-No-Touch: 1header on any XHR they issue).Design note (header vs. query parameter)
An earlier revision of this PR used a
?_luci_bg=1query parameter and injectednotouch=1atuh_ubus_send_requestunconditionally. The header form is cleaner and per @jow-'s review has been adopted. The switch also let us drop the wildcardnotouchinjection and scope it tosession/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 reachesexpires:0inside the timeout and shows the "Session expired" modal on the next Poll tick.