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