use std::any::Any;
use std::sync::{Arc, RwLock};
use async_trait::async_trait;
use pubky_common::{auth::AuthToken, crypto::PublicKey, session::CookieSessionRecord};
use reqwest::{Method, RequestBuilder, Response};
use crate::actors::session::core::PubkySession;
use crate::actors::session::credential::{SessionCredential, credential_session_missing};
use crate::{
PubkyHttpClient, actors::session::SessionInfo, actors::storage::resource::resolve_pubky,
cross_log, errors::Result, util::check_http_status,
};
#[cfg(not(target_arch = "wasm32"))]
use crate::errors::AuthError;
#[derive(Clone, Debug)]
pub struct CookieCredential {
user: PublicKey,
record: Arc<RwLock<CookieSessionRecord>>,
cookie: Option<String>,
}
impl CookieCredential {
pub(crate) fn new(
user: PublicKey,
cookie: Option<String>,
record: CookieSessionRecord,
) -> Self {
Self {
user,
record: Arc::new(RwLock::new(record)),
cookie,
}
}
pub(crate) async fn from_response(response: Response) -> Result<Self> {
let raw_set_cookies = collect_set_cookies(&response);
let bytes = response.bytes().await?;
let record = CookieSessionRecord::deserialize(&bytes)?;
let user = record.public_key().clone();
let cookie_name = user.z32();
let cookie = raw_set_cookies
.iter()
.filter_map(|raw| cookie::Cookie::parse(raw.clone()).ok())
.find(|c| c.name() == cookie_name)
.map(|c| c.value().to_string());
#[cfg(not(target_arch = "wasm32"))]
{
if cookie.is_none() {
return Err(AuthError::Validation("missing session cookie".into()).into());
}
}
#[cfg(target_arch = "wasm32")]
if cookie.is_none() {
cross_log!(
info,
"Hydrating WASM cookie credential without captured secret \
(browser jar will handle attachment) for {}",
user
);
}
cross_log!(info, "Hydrated cookie credential for {}", user);
Ok(Self::new(user, cookie, record))
}
pub(crate) async fn from_auth_token(
token: &AuthToken,
client: &PubkyHttpClient,
) -> Result<Self> {
let url = format!("pubky{}/session", token.public_key().z32());
cross_log!(
info,
"Establishing new session exchange for {}",
token.public_key()
);
let resolved = resolve_pubky(&url)?;
let response = client
.cross_request(Method::POST, resolved)
.await?
.body(token.serialize())
.send()
.await?;
let response = check_http_status(response).await?;
cross_log!(
info,
"Session exchange for {} succeeded; constructing credential",
token.public_key()
);
Self::from_response(response).await
}
pub(crate) fn cookie_secret(&self) -> Option<&str> {
self.cookie.as_deref()
}
pub(crate) fn cookie_record(&self) -> CookieSessionRecord {
self.record
.read()
.expect("CookieCredential record RwLock poisoned")
.clone()
}
pub(crate) fn replace_record(&self, record: CookieSessionRecord) {
if let Ok(mut r) = self.record.write() {
*r = record;
}
}
}
fn collect_set_cookies(response: &Response) -> Vec<String> {
let mut out = Vec::new();
for val in response.headers().get_all(reqwest::header::SET_COOKIE) {
if let Ok(raw) = std::str::from_utf8(val.as_bytes()) {
out.push(raw.to_owned());
}
}
out
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl SessionCredential for CookieCredential {
fn info(&self) -> SessionInfo {
let record = self
.record
.read()
.expect("CookieCredential record RwLock poisoned");
SessionInfo::new(record.public_key().clone(), record.capabilities().to_vec())
}
async fn signout(&self, client: &PubkyHttpClient) -> Result<()> {
let url = format!("pubky{}/session", self.user.z32());
let resolved = resolve_pubky(&url)?;
let rb = client.cross_request(Method::DELETE, resolved).await?;
let rb = self.attach(rb, client).await?;
let response = rb.send().await.map_err(crate::Error::from)?;
check_http_status(response).await?;
Ok(())
}
async fn attach(
&self,
rb: RequestBuilder,
_client: &PubkyHttpClient,
) -> Result<RequestBuilder> {
match &self.cookie {
Some(cookie) => {
let cookie_name = self.user.z32();
Ok(rb.header(reqwest::header::COOKIE, format!("{cookie_name}={cookie}")))
}
None => Ok(rb),
}
}
async fn revalidate(
&self,
client: &PubkyHttpClient,
user: &PublicKey,
) -> Result<Option<SessionInfo>> {
let url = format!("pubky{}/session", user.z32());
let resolved = resolve_pubky(&url)?;
let rb = client.cross_request(Method::GET, resolved).await?;
let rb = self.attach(rb, client).await?;
let response = rb.send().await.map_err(crate::Error::from)?;
if credential_session_missing(&response) {
cross_log!(info, "Cookie session missing on revalidate");
return Ok(None);
}
let response = check_http_status(response).await?;
let bytes = response.bytes().await?;
let record = CookieSessionRecord::deserialize(&bytes)?;
let info = SessionInfo::new(record.public_key().clone(), record.capabilities().to_vec());
self.replace_record(record);
Ok(Some(info))
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl PubkySession {
#[must_use]
pub fn from_cookie_credential(client: PubkyHttpClient, credential: CookieCredential) -> Self {
Self::from_credential(client, Arc::new(credential))
}
}