use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use buffr_permissions::{Capability, Decision, PermError, Permissions};
use cef::{
ImplMediaAccessCallback, ImplPermissionPromptCallback, MediaAccessCallback,
PermissionPromptCallback, PermissionRequestResult,
};
use tracing::{trace, warn};
pub use buffr_engine::permissions::PromptOutcome;
static CEF_RESOLVE_ID: AtomicU64 = AtomicU64::new(1);
pub fn next_resolve_id() -> String {
format!("cef-{}", CEF_RESOLVE_ID.fetch_add(1, Ordering::Relaxed))
}
pub type CefCallbackRegistry = Arc<Mutex<std::collections::HashMap<String, PendingPermission>>>;
const MEDIA_DEVICE_AUDIO_CAPTURE: u32 = 1;
const MEDIA_DEVICE_VIDEO_CAPTURE: u32 = 2;
const MEDIA_DESKTOP_AUDIO_CAPTURE: u32 = 4;
const MEDIA_DESKTOP_VIDEO_CAPTURE: u32 = 8;
const PERM_CAMERA_PAN_TILT_ZOOM: u32 = 2;
const PERM_CAMERA_STREAM: u32 = 4;
const PERM_CLIPBOARD: u32 = 16;
const PERM_GEOLOCATION: u32 = 256;
const PERM_MIC_STREAM: u32 = 4096;
const PERM_MIDI_SYSEX: u32 = 8192;
const PERM_NOTIFICATIONS: u32 = 32768;
pub enum PendingPermission {
MediaAccess {
origin: String,
capabilities: Vec<Capability>,
callback: MediaAccessCallback,
requested_mask: u32,
},
Prompt {
origin: String,
capabilities: Vec<Capability>,
callback: PermissionPromptCallback,
prompt_id: u64,
},
}
impl std::fmt::Debug for PendingPermission {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PendingPermission::MediaAccess {
origin,
capabilities,
requested_mask,
..
} => f
.debug_struct("PendingPermission::MediaAccess")
.field("origin", origin)
.field("capabilities", capabilities)
.field("requested_mask", requested_mask)
.finish_non_exhaustive(),
PendingPermission::Prompt {
origin,
capabilities,
prompt_id,
..
} => f
.debug_struct("PendingPermission::Prompt")
.field("origin", origin)
.field("capabilities", capabilities)
.field("prompt_id", prompt_id)
.finish_non_exhaustive(),
}
}
}
impl PendingPermission {
pub fn origin(&self) -> &str {
match self {
PendingPermission::MediaAccess { origin, .. }
| PendingPermission::Prompt { origin, .. } => origin,
}
}
pub fn capabilities(&self) -> &[Capability] {
match self {
PendingPermission::MediaAccess { capabilities, .. }
| PendingPermission::Prompt { capabilities, .. } => capabilities,
}
}
pub fn resolve(self, outcome: PromptOutcome, store: &Permissions) -> Result<usize, PermError> {
let (decision_to_persist, remember) = match outcome {
PromptOutcome::Allow { remember } => (Some(Decision::Allow), remember),
PromptOutcome::Deny { remember } => (Some(Decision::Deny), remember),
PromptOutcome::Defer => (None, false),
};
let mut written = 0usize;
match self {
PendingPermission::MediaAccess {
origin,
capabilities,
callback,
requested_mask,
} => {
if remember && let Some(decision) = decision_to_persist {
for cap in &capabilities {
store.set(&origin, *cap, decision)?;
written += 1;
}
}
match outcome {
PromptOutcome::Allow { .. } => callback.cont(requested_mask),
PromptOutcome::Deny { .. } | PromptOutcome::Defer => callback.cancel(),
}
}
PendingPermission::Prompt {
origin,
capabilities,
callback,
prompt_id: _,
} => {
if remember && let Some(decision) = decision_to_persist {
for cap in &capabilities {
store.set(&origin, *cap, decision)?;
written += 1;
}
}
let result = match outcome {
PromptOutcome::Allow { .. } => PermissionRequestResult::ACCEPT,
PromptOutcome::Deny { .. } => PermissionRequestResult::DENY,
PromptOutcome::Defer => PermissionRequestResult::DISMISS,
};
callback.cont(result);
}
}
Ok(written)
}
}
pub fn enqueue_to_both(
pending: PendingPermission,
registry: &CefCallbackRegistry,
engine_queue: &buffr_engine::PermissionsQueue,
resolve_id: String,
) {
let neutral = buffr_engine::permissions::PendingPermission {
origin: pending.origin().to_string(),
capabilities: pending.capabilities().to_vec(),
resolve_id: Some(resolve_id.clone()),
};
match registry.lock() {
Ok(mut reg) => {
reg.insert(resolve_id, pending);
}
Err(_) => {
warn!("permissions: callback registry mutex poisoned");
return;
}
}
match engine_queue.lock() {
Ok(mut q) => q.push_back(neutral),
Err(_) => {
warn!("permissions: engine queue mutex poisoned");
}
}
}
pub type PermissionsQueue = Arc<Mutex<VecDeque<PendingPermission>>>;
pub fn new_queue() -> PermissionsQueue {
Arc::new(Mutex::new(VecDeque::new()))
}
pub fn queue_len(queue: &PermissionsQueue) -> usize {
queue.lock().map(|g| g.len()).unwrap_or(0)
}
pub fn pop_front(queue: &PermissionsQueue) -> Option<PendingPermission> {
queue.lock().ok().and_then(|mut g| g.pop_front())
}
pub fn peek_front(queue: &PermissionsQueue) -> Option<(String, Vec<Capability>)> {
let g = queue.lock().ok()?;
let front = g.front()?;
Some((front.origin().to_string(), front.capabilities().to_vec()))
}
pub fn drain_with_defer(queue: &PermissionsQueue, store: &Permissions) {
let drained: Vec<PendingPermission> = match queue.lock() {
Ok(mut g) => g.drain(..).collect(),
Err(_) => return,
};
for p in drained {
if let Err(err) = p.resolve(PromptOutcome::Defer, store) {
warn!(error = %err, "permissions: defer dispatch on drain failed");
}
}
}
pub fn drain_registry_with_defer(registry: &CefCallbackRegistry, store: &Permissions) {
let drained: Vec<PendingPermission> = match registry.lock() {
Ok(mut reg) => reg.drain().map(|(_, v)| v).collect(),
Err(_) => return,
};
for p in drained {
if let Err(err) = p.resolve(PromptOutcome::Defer, store) {
warn!(error = %err, "permissions: defer dispatch on registry drain failed");
}
}
}
pub fn capabilities_for_media_mask(mask: u32) -> Vec<Capability> {
let mut out = Vec::with_capacity(2);
let video =
(mask & MEDIA_DEVICE_VIDEO_CAPTURE) != 0 || (mask & MEDIA_DESKTOP_VIDEO_CAPTURE) != 0;
let audio =
(mask & MEDIA_DEVICE_AUDIO_CAPTURE) != 0 || (mask & MEDIA_DESKTOP_AUDIO_CAPTURE) != 0;
if video {
out.push(Capability::Camera);
}
if audio {
out.push(Capability::Microphone);
}
out
}
pub fn capabilities_for_request_mask(mask: u32) -> Vec<Capability> {
let mut out = Vec::new();
if mask == 0 {
return out;
}
let mut remaining = mask;
let known: &[(u32, Capability)] = &[
(PERM_CAMERA_STREAM, Capability::Camera),
(PERM_CAMERA_PAN_TILT_ZOOM, Capability::Camera),
(PERM_MIC_STREAM, Capability::Microphone),
(PERM_GEOLOCATION, Capability::Geolocation),
(PERM_NOTIFICATIONS, Capability::Notifications),
(PERM_CLIPBOARD, Capability::Clipboard),
(PERM_MIDI_SYSEX, Capability::Midi),
];
for (bit, cap) in known {
if (remaining & *bit) != 0 {
if !out.contains(cap) {
out.push(*cap);
}
remaining &= !*bit;
}
}
let mut bit = 1u32;
while bit != 0 {
if (remaining & bit) != 0 {
out.push(Capability::Other(bit));
remaining &= !bit;
}
bit = bit.checked_shl(1).unwrap_or(0);
}
out
}
pub fn precheck(
store: &Permissions,
origin: &str,
caps: &[Capability],
) -> Result<Option<Decision>, PermError> {
if caps.is_empty() {
return Ok(Some(Decision::Allow));
}
let mut all_allow = true;
for cap in caps {
match store.get(origin, *cap)? {
Some(Decision::Allow) => {}
Some(Decision::Deny) => {
all_allow = false;
}
None => {
trace!(origin, capability = ?cap, "permissions: precheck miss");
return Ok(None);
}
}
}
if all_allow {
Ok(Some(Decision::Allow))
} else {
Ok(Some(Decision::Deny))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn media_mask_video_only() {
let caps = capabilities_for_media_mask(MEDIA_DEVICE_VIDEO_CAPTURE);
assert_eq!(caps, vec![Capability::Camera]);
}
#[test]
fn media_mask_audio_only() {
let caps = capabilities_for_media_mask(MEDIA_DEVICE_AUDIO_CAPTURE);
assert_eq!(caps, vec![Capability::Microphone]);
}
#[test]
fn media_mask_both() {
let mask = MEDIA_DEVICE_VIDEO_CAPTURE | MEDIA_DEVICE_AUDIO_CAPTURE;
let caps = capabilities_for_media_mask(mask);
assert_eq!(caps, vec![Capability::Camera, Capability::Microphone]);
}
#[test]
fn media_mask_desktop_collapses_to_same_caps() {
let mask = MEDIA_DESKTOP_AUDIO_CAPTURE | MEDIA_DESKTOP_VIDEO_CAPTURE;
let caps = capabilities_for_media_mask(mask);
assert_eq!(caps, vec![Capability::Camera, Capability::Microphone]);
}
#[test]
fn request_mask_geolocation() {
let caps = capabilities_for_request_mask(PERM_GEOLOCATION);
assert_eq!(caps, vec![Capability::Geolocation]);
}
#[test]
fn request_mask_camera_with_pan_tilt_zoom_dedupes() {
let mask = PERM_CAMERA_STREAM | PERM_CAMERA_PAN_TILT_ZOOM;
let caps = capabilities_for_request_mask(mask);
assert_eq!(caps, vec![Capability::Camera]);
}
#[test]
fn request_mask_unknown_bit_falls_back_to_other() {
let caps = capabilities_for_request_mask(1);
assert_eq!(caps, vec![Capability::Other(1)]);
}
#[test]
fn request_mask_combined_known_and_unknown() {
let caps = capabilities_for_request_mask(PERM_GEOLOCATION | 1);
assert!(caps.contains(&Capability::Geolocation));
assert!(caps.contains(&Capability::Other(1)));
assert_eq!(caps.len(), 2);
}
#[test]
fn request_mask_empty_returns_empty() {
let caps = capabilities_for_request_mask(0);
assert!(caps.is_empty());
}
#[test]
fn precheck_empty_caps_allows() {
let store = Permissions::open_in_memory().unwrap();
let r = precheck(&store, "https://x", &[]).unwrap();
assert_eq!(r, Some(Decision::Allow));
}
#[test]
fn precheck_all_allow_returns_allow() {
let store = Permissions::open_in_memory().unwrap();
store
.set("https://x", Capability::Camera, Decision::Allow)
.unwrap();
store
.set("https://x", Capability::Microphone, Decision::Allow)
.unwrap();
let r = precheck(
&store,
"https://x",
&[Capability::Camera, Capability::Microphone],
)
.unwrap();
assert_eq!(r, Some(Decision::Allow));
}
#[test]
fn precheck_one_deny_returns_deny() {
let store = Permissions::open_in_memory().unwrap();
store
.set("https://x", Capability::Camera, Decision::Allow)
.unwrap();
store
.set("https://x", Capability::Microphone, Decision::Deny)
.unwrap();
let r = precheck(
&store,
"https://x",
&[Capability::Camera, Capability::Microphone],
)
.unwrap();
assert_eq!(r, Some(Decision::Deny));
}
#[test]
fn precheck_one_missing_returns_none() {
let store = Permissions::open_in_memory().unwrap();
store
.set("https://x", Capability::Camera, Decision::Allow)
.unwrap();
let r = precheck(
&store,
"https://x",
&[Capability::Camera, Capability::Microphone],
)
.unwrap();
assert_eq!(r, None);
}
#[test]
fn queue_starts_empty() {
let q = new_queue();
assert_eq!(queue_len(&q), 0);
assert!(pop_front(&q).is_none());
assert!(peek_front(&q).is_none());
}
}