1use std::sync::Arc;
8
9use rpi_plugin_sdk::{StbString, StbStringRef};
10use serde_json::Value;
11
12use crate::registry::{RegisteredOAuthProvider, RegistrySnapshot};
13
14#[derive(Debug, thiserror::Error)]
15pub enum OAuthExtensionError {
16 #[error("OAuth provider `{0}` is not registered")]
17 NotFound(String),
18 #[error("OAuth response has unsupported version")]
19 UnsupportedVersion,
20 #[error("OAuth response has invalid kind")]
21 InvalidResponseKind,
22 #[error("OAuth action must be one of begin, exchange, refresh, revoke")]
23 InvalidAction,
24 #[error("OAuth plugin panicked during request")]
25 PluginPanic,
26 #[error("OAuth plugin returned status {0}: {1}")]
27 PluginStatus(i32, String),
28 #[error("OAuth plugin returned invalid JSON: {0}")]
29 InvalidResponse(String),
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum OAuthResponseKind {
36 Interaction,
37 Credential,
38 Success,
39 Error,
40}
41
42#[derive(Debug, Clone)]
43pub struct OAuthActionResponse {
44 pub kind: OAuthResponseKind,
45 pub payload: Value,
46}
47
48impl OAuthActionResponse {
49 pub fn parse(value: Value) -> Result<Self, OAuthExtensionError> {
50 let version = value.get("version").and_then(Value::as_u64).unwrap_or(1);
51 if version != 1 {
52 return Err(OAuthExtensionError::UnsupportedVersion);
53 }
54 let kind = match value.get("kind").and_then(Value::as_str) {
55 Some("interaction") => OAuthResponseKind::Interaction,
56 Some("credential") => OAuthResponseKind::Credential,
57 Some("success") => OAuthResponseKind::Success,
58 Some("error") => OAuthResponseKind::Error,
59 _ => return Err(OAuthExtensionError::InvalidResponseKind),
60 };
61 Ok(Self {
62 kind,
63 payload: value.get("payload").cloned().unwrap_or(Value::Null),
64 })
65 }
66
67 pub fn credential(&self) -> Result<Option<OAuthCredential>, OAuthExtensionError> {
70 if self.kind != OAuthResponseKind::Credential {
71 return Ok(None);
72 }
73 let object = self.payload.as_object().ok_or_else(|| {
74 OAuthExtensionError::InvalidResponse("credential payload must be an object".into())
75 })?;
76 let access = object
77 .get("access")
78 .and_then(Value::as_str)
79 .filter(|value| !value.is_empty())
80 .ok_or_else(|| {
81 OAuthExtensionError::InvalidResponse("missing credential access".into())
82 })?;
83 let refresh = object
84 .get("refresh")
85 .and_then(Value::as_str)
86 .unwrap_or_default();
87 let expires_at = object
88 .get("expires_at")
89 .and_then(Value::as_i64)
90 .unwrap_or(0);
91 Ok(Some(OAuthCredential {
92 access: access.to_string(),
93 refresh: refresh.to_string(),
94 expires_at,
95 }))
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct OAuthCredential {
101 pub access: String,
102 pub refresh: String,
103 pub expires_at: i64,
104}
105
106#[derive(Debug, Clone, serde::Serialize)]
108pub struct OAuthProviderInfo {
109 pub plugin: String,
110 pub id: String,
111 #[serde(flatten)]
112 pub manifest: serde_json::Map<String, Value>,
113}
114
115pub fn list(snapshot: &RegistrySnapshot) -> Vec<OAuthProviderInfo> {
117 snapshot.oauth_providers().iter().filter_map(info).collect()
118}
119
120pub fn request(
125 snapshot: &Arc<RegistrySnapshot>,
126 provider_id: &str,
127 action: &str,
128 params: Value,
129) -> Result<Value, OAuthExtensionError> {
130 if !matches!(action, "begin" | "exchange" | "refresh" | "revoke") {
131 return Err(OAuthExtensionError::InvalidAction);
132 }
133 let provider = snapshot
134 .oauth_provider(provider_id)
135 .ok_or_else(|| OAuthExtensionError::NotFound(provider_id.to_string()))?;
136 invoke(provider, action, params)
137}
138
139fn info(provider: &RegisteredOAuthProvider) -> Option<OAuthProviderInfo> {
140 let mut manifest = serde_json::from_str::<Value>(&provider.manifest_json)
141 .ok()?
142 .as_object()?
143 .clone();
144 let id = manifest.get("id").and_then(Value::as_str)?.to_string();
145 manifest.remove("id");
146 Some(OAuthProviderInfo {
147 plugin: provider.plugin.clone(),
148 id,
149 manifest,
150 })
151}
152
153fn invoke(
154 provider: &RegisteredOAuthProvider,
155 action: &str,
156 params: Value,
157) -> Result<Value, OAuthExtensionError> {
158 let request = serde_json::json!({
159 "action": action,
160 "providerId": provider_id(provider),
161 "input": params,
162 })
163 .to_string();
164 let mut out = StbString::empty();
165 let status = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
166 (provider.request_fn)(
167 StbStringRef::from_str(&request),
168 &mut out,
169 provider.user_data,
170 )
171 }));
172 let out = unsafe {
176 rpi_plugin_sdk::callbacks::OwnedString::from_raw(out, Some(provider.plugin_free_string))
177 };
178 let response = out.to_string_lossy();
179 let status = match status {
180 Ok(status) => status,
181 Err(_) => return Err(OAuthExtensionError::PluginPanic),
182 };
183 if status != 0 {
184 return Err(OAuthExtensionError::PluginStatus(status, response));
185 }
186 serde_json::from_str(&response)
187 .map_err(|error| OAuthExtensionError::InvalidResponse(error.to_string()))
188}
189
190fn provider_id(provider: &RegisteredOAuthProvider) -> String {
191 serde_json::from_str::<Value>(&provider.manifest_json)
195 .ok()
196 .and_then(|value| value.get("id").and_then(Value::as_str).map(str::to_owned))
197 .unwrap_or_default()
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::registry::{ExtensionRegistry, RegisteredOAuthProvider};
204 use std::sync::atomic::{AtomicUsize, Ordering};
205
206 static FREED: AtomicUsize = AtomicUsize::new(0);
207
208 unsafe extern "C" fn free_output(value: StbString) {
209 if !value.is_empty() {
210 FREED.fetch_add(1, Ordering::SeqCst);
211 }
212 unsafe { rpi_plugin_sdk::callbacks::free_string(value) };
214 }
215
216 rpi_plugin_sdk::string_handler! { fn callback(request, out, _user_data) {
217 let value: Value = serde_json::from_str(request).unwrap();
218 let response = if value.get("action").and_then(Value::as_str) == Some("refresh") {
219 r#"{"access":"secret"}"#
220 } else {
221 r#"{"ok":true}"#
222 };
223 i32::from(!out.write(response.to_string()))
224 } }
225
226 fn snapshot() -> Arc<RegistrySnapshot> {
227 let mut registry = ExtensionRegistry::new();
228 registry.register_oauth_provider(RegisteredOAuthProvider {
229 plugin: "oauth-test".to_string(),
230 manifest_json: r#"{"id":"acme","displayName":"Acme"}"#.to_string(),
231 request_fn: callback,
232 plugin_free_string: free_output,
233 user_data: std::ptr::null_mut(),
234 });
235 Arc::new(registry.snapshot())
236 }
237
238 #[test]
239 fn lists_non_secret_manifest_info() {
240 let providers = list(&snapshot());
241 assert_eq!(providers[0].id, "acme");
242 assert_eq!(providers[0].plugin, "oauth-test");
243 assert!(!providers[0].manifest.contains_key("id"));
244 }
245
246 #[test]
247 fn invokes_action_and_reclaims_plugin_output() {
248 FREED.store(0, Ordering::SeqCst);
249 let result = request(&snapshot(), "acme", "refresh", serde_json::json!({"x": 1})).unwrap();
250 assert_eq!(result["access"], "secret");
251 assert_eq!(FREED.load(Ordering::SeqCst), 1);
252 }
253
254 #[test]
255 fn parses_standard_interaction_and_credential_responses() {
256 let interaction = OAuthActionResponse::parse(serde_json::json!({
257 "version": 1,
258 "kind": "interaction",
259 "payload": {"display": {"url": "opaque"}}
260 }))
261 .unwrap();
262 assert_eq!(interaction.kind, OAuthResponseKind::Interaction);
263
264 let credential = OAuthActionResponse::parse(serde_json::json!({
265 "version": 1,
266 "kind": "credential",
267 "payload": {"access": "a", "refresh": "r", "expires_at": 42}
268 }))
269 .unwrap();
270 assert_eq!(credential.credential().unwrap().unwrap().expires_at, 42);
271 }
272
273 #[test]
274 fn rejects_unknown_response_version_and_kind() {
275 assert!(matches!(
276 OAuthActionResponse::parse(serde_json::json!({"version": 2, "kind": "success"})),
277 Err(OAuthExtensionError::UnsupportedVersion)
278 ));
279 assert!(matches!(
280 OAuthActionResponse::parse(
281 serde_json::json!({"version": 1, "kind": "provider-specific"})
282 ),
283 Err(OAuthExtensionError::InvalidResponseKind)
284 ));
285 }
286
287 #[test]
288 fn rejects_unknown_action_and_provider() {
289 assert!(matches!(
290 request(&snapshot(), "acme", "unknown", Value::Null),
291 Err(OAuthExtensionError::InvalidAction)
292 ));
293 assert!(matches!(
294 request(&snapshot(), "missing", "begin", Value::Null),
295 Err(OAuthExtensionError::NotFound(_))
296 ));
297 }
298}