1use std::collections::{HashMap, VecDeque};
15use std::sync::{Mutex, OnceLock};
16use std::time::{Duration, Instant};
17
18use serde_json::{json, Value};
19
20use super::widget_packages::{WidgetKind, WidgetPackageInfo, WidgetState};
21
22pub const ERROR_PREFIX: &str = "widget-error:";
25
26pub const STORAGE_MAX_BYTES: usize = 5 * 1024 * 1024;
27pub const STORAGE_VALUE_MAX_BYTES: usize = 1024 * 1024;
28pub const STORAGE_KEY_MAX_BYTES: usize = 256;
29pub const SEND_MAX_BYTES: usize = 8 * 1024;
30pub const SENDS_PER_MINUTE: usize = 10;
31const MAX_SESSIONS: usize = 1024;
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct AccessError {
35 pub code: i32,
36 pub message: String,
37 pub data: Option<Value>,
38}
39
40impl AccessError {
41 pub fn new(code: i32, message: impl Into<String>) -> Self {
42 Self { code, message: message.into(), data: None }
43 }
44 pub fn invalid(message: impl Into<String>) -> Self {
45 Self::new(-32602, message)
46 }
47 pub fn denied(pkg_name: &str, permission: &str) -> Self {
48 Self {
49 code: 1001,
50 message: format!("{pkg_name} wasn't granted \"{permission}\""),
51 data: Some(json!({ "permission": permission })),
52 }
53 }
54 pub fn limit(limit: &str, message: impl Into<String>) -> Self {
55 Self { code: 1002, message: message.into(), data: Some(json!({ "limit": limit })) }
56 }
57 pub fn not_found(message: impl Into<String>) -> Self {
58 Self::new(1003, message)
59 }
60 pub fn network(message: impl Into<String>) -> Self {
61 Self::new(1004, message)
62 }
63 pub fn unavailable(message: impl Into<String>) -> Self {
64 Self::new(1005, message)
65 }
66 pub fn internal(message: impl Into<String>) -> Self {
67 Self::new(1099, message)
68 }
69 pub fn to_rpc(&self) -> String {
71 let mut v = json!({ "code": self.code, "message": self.message });
72 if let Some(d) = &self.data {
73 v["data"] = d.clone();
74 }
75 format!("{ERROR_PREFIX}{v}")
76 }
77}
78
79#[derive(Debug, Clone)]
80pub struct Session {
81 pub widget_id: String,
82 pub hash: String,
83 opened: Instant,
84}
85
86#[derive(Default)]
87pub struct Sessions {
88 state: Mutex<State>,
91 sends: Mutex<HashMap<String, VecDeque<Instant>>>,
92}
93
94#[derive(Default)]
95struct State {
96 open: HashMap<String, Session>,
97 removing: std::collections::HashSet<String>,
99}
100
101pub fn sessions() -> &'static Sessions {
102 static SESSIONS: OnceLock<Sessions> = OnceLock::new();
103 SESSIONS.get_or_init(Sessions::default)
104}
105
106fn runnable<'a>(packages: &'a [WidgetPackageInfo], id: &str, hash: &str) -> Option<&'a WidgetPackageInfo> {
109 packages
110 .iter()
111 .find(|p| p.id == id && p.hash == hash && p.state == WidgetState::Approved && p.kind == WidgetKind::Sandboxed)
112}
113
114impl Sessions {
115 fn state(&self) -> std::sync::MutexGuard<'_, State> {
116 self.state.lock().unwrap_or_else(|p| p.into_inner())
117 }
118
119 pub fn begin_removal(&self, id: &str) {
123 let mut st = self.state();
124 st.removing.insert(id.to_string());
125 st.open.retain(|_, s| s.widget_id != id);
126 }
127
128 pub fn end_removal(&self, id: &str) {
131 self.state().removing.remove(id);
132 }
133
134 pub fn open(&self, packages: &[WidgetPackageInfo], id: &str, hash: &str) -> Result<String, AccessError> {
136 let mut st = self.state();
137 if runnable(packages, id, hash).is_none() || st.removing.contains(id) {
138 return Err(AccessError::unavailable(format!("{id} isn't approved at this version")));
139 }
140 let token = hex::encode(uuid::Uuid::new_v4().as_bytes()) + &hex::encode(uuid::Uuid::new_v4().as_bytes());
141 if st.open.len() >= MAX_SESSIONS {
142 if let Some(oldest) = st.open.iter().min_by_key(|(_, s)| s.opened).map(|(t, _)| t.clone()) {
144 st.open.remove(&oldest);
145 }
146 }
147 st.open.insert(
148 token.clone(),
149 Session { widget_id: id.to_string(), hash: hash.to_string(), opened: Instant::now() },
150 );
151 Ok(token)
152 }
153
154 pub fn close(&self, token: &str) {
155 self.state().open.remove(token);
156 }
157
158 pub fn authorize(&self, packages: &[WidgetPackageInfo], token: &str) -> Result<(Session, WidgetPackageInfo), AccessError> {
162 let session = {
163 let st = self.state();
164 let session = st.open.get(token).cloned().ok_or_else(|| AccessError::unavailable("this widget's session ended; reload it"))?;
165 if st.removing.contains(&session.widget_id) {
166 return Err(AccessError::unavailable(format!("{} is being uninstalled", session.widget_id)));
167 }
168 session
169 };
170 let pkg = runnable(packages, &session.widget_id, &session.hash)
171 .cloned()
172 .ok_or_else(|| AccessError::unavailable(format!("{} changed or was turned off; reload it", session.widget_id)))?;
173 Ok((session, pkg))
174 }
175
176 pub fn take_send(&self, widget_id: &str) -> Result<(), AccessError> {
178 self.take_send_at(widget_id, Instant::now())
179 }
180
181 fn take_send_at(&self, widget_id: &str, now: Instant) -> Result<(), AccessError> {
182 let mut sends = self.sends.lock().unwrap_or_else(|p| p.into_inner());
183 let recent = sends.entry(widget_id.to_string()).or_default();
184 while recent.front().is_some_and(|t| now.duration_since(*t) >= Duration::from_secs(60)) {
185 recent.pop_front();
186 }
187 if recent.len() >= SENDS_PER_MINUTE {
188 return Err(AccessError::limit("rate", format!("a widget can send at most {SENDS_PER_MINUTE} messages a minute")));
189 }
190 recent.push_back(now);
191 Ok(())
192 }
193}
194
195pub fn require(pkg: &WidgetPackageInfo, permission: &str) -> Result<(), AccessError> {
197 if pkg.granted.iter().any(|g| g == permission) {
198 Ok(())
199 } else {
200 Err(AccessError::denied(&pkg.name, permission))
201 }
202}
203
204pub fn check_key(key: &str) -> Result<(), AccessError> {
206 if key.is_empty() || key.len() > STORAGE_KEY_MAX_BYTES {
207 return Err(AccessError::invalid(format!("a storage key is 1 to {STORAGE_KEY_MAX_BYTES} bytes")));
208 }
209 Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::backend::widget_packages::WidgetPaneInfo;
216
217 fn pkg(state: WidgetState, kind: WidgetKind, granted: &[&str]) -> WidgetPackageInfo {
218 WidgetPackageInfo {
219 id: "acme.notes".into(),
220 name: "Notes".into(),
221 version: "1.0.0".into(),
222 description: None,
223 author: None,
224 homepage: None,
225 icon: "note-sticky".into(),
226 default_hue: None,
227 kind,
228 permissions: granted.iter().map(|s| s.to_string()).collect(),
229 granted: granted.iter().map(|s| s.to_string()).collect(),
230 state,
231 error: None,
232 hash: "h1".into(),
233 panes: Vec::<WidgetPaneInfo>::new(),
234 commands: vec![],
235 status_items: vec![],
236 signature: crate::backend::widget_signature::describe("acme.x", None, &Default::default()),
237 files_url: None,
238 implied: false,
239 folder: String::new(),
240 }
241 }
242
243 #[test]
244 fn a_session_is_for_one_approved_sandboxed_version_only() {
245 let s = Sessions::default();
246 let approved = vec![pkg(WidgetState::Approved, WidgetKind::Sandboxed, &["storage"])];
247 assert!(s.open(&approved, "acme.notes", "h0").is_err(), "another hash");
248 assert!(s.open(&[pkg(WidgetState::NeedsApproval, WidgetKind::Sandboxed, &[])], "acme.notes", "h1").is_err());
249 assert!(s.open(&[pkg(WidgetState::Approved, WidgetKind::Trusted, &[])], "acme.notes", "h1").is_err());
250 let token = s.open(&approved, "acme.notes", "h1").unwrap();
251 let (session, p) = s.authorize(&approved, &token).unwrap();
252 assert_eq!(session.widget_id, "acme.notes");
253 assert!(require(&p, "storage").is_ok());
254 assert_eq!(require(&p, "agents:send").unwrap_err().data, Some(json!({ "permission": "agents:send" })));
255 let changed = vec![pkg(WidgetState::Changed, WidgetKind::Sandboxed, &["storage"])];
257 assert_eq!(s.authorize(&changed, &token).unwrap_err().code, 1005);
258 s.close(&token);
259 assert!(s.authorize(&approved, &token).is_err());
260 assert!(s.authorize(&approved, "made-up").is_err());
261 }
262
263 #[test]
264 fn an_uninstall_ends_the_widgets_sessions_until_it_is_done() {
265 let s = Sessions::default();
266 let approved = vec![pkg(WidgetState::Approved, WidgetKind::Sandboxed, &["storage"])];
267 let token = s.open(&approved, "acme.notes", "h1").unwrap();
268 s.begin_removal("acme.notes");
269 assert_eq!(s.authorize(&approved, &token).unwrap_err().code, 1005, "an open pane can't write while it's removed");
270 assert!(s.open(&approved, "acme.notes", "h1").is_err(), "nor open a new session");
271 s.end_removal("acme.notes");
272 assert!(s.authorize(&approved, &token).is_err(), "the old session stays ended");
273 assert!(s.open(&approved, "acme.notes", "h1").is_ok(), "a reinstall's panes can start again");
274 }
275
276 #[test]
277 fn sends_are_limited_per_package_per_minute() {
278 let s = Sessions::default();
279 let t0 = Instant::now();
280 for _ in 0..SENDS_PER_MINUTE {
281 s.take_send_at("acme.a", t0).unwrap();
282 }
283 assert_eq!(s.take_send_at("acme.a", t0).unwrap_err().code, 1002);
284 assert!(s.take_send_at("acme.b", t0).is_ok(), "another package has its own budget");
285 assert!(s.take_send_at("acme.a", t0 + Duration::from_secs(61)).is_ok());
286 }
287
288 #[test]
289 fn errors_carry_their_code_and_data_on_the_wire() {
290 let e = AccessError::denied("Notes", "storage").to_rpc();
291 let v: Value = serde_json::from_str(e.strip_prefix(ERROR_PREFIX).unwrap()).unwrap();
292 assert_eq!(v, json!({ "code": 1001, "message": "Notes wasn't granted \"storage\"", "data": { "permission": "storage" } }));
293 }
294}