use std::collections::BTreeMap;
use std::panic::AssertUnwindSafe;
use async_trait::async_trait;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use cratefield_core::{
ActorError, ActorHandlers, ActorStore, ActorWrites, Actors, HarnessConfig, MAX_ACTOR_KEY_BYTES,
MAX_ACTOR_KIND_BYTES, MAX_ACTOR_MESSAGE_BYTES, check_actor_call, run_actor_alarm,
run_actor_message, validate_actor_key, validate_actor_kind,
};
use futures_util::lock::Mutex;
use hmac::{Hmac, KeyInit, Mac};
use serde::de::DeserializeOwned;
use sha2::Sha256;
use subtle::ConstantTimeEq;
use time::OffsetDateTime;
use worker::d1::serde_wasm_bindgen;
use worker::js_sys::futures::{JsFuture, future_to_promise};
use worker::js_sys::{Date, Function, Promise, Reflect, Uint8Array};
use worker::send::{IntoSendFuture, SendWrapper};
use worker::wasm_bindgen::JsCast;
use worker::wasm_bindgen::JsValue;
use worker::wasm_bindgen::closure::Closure;
use worker::worker_sys::{DurableObjectState, DurableObjectStorage, DurableObjectTransaction};
use worker::{
Env, Headers, ListOptions, Method, ObjectNamespace, Request, RequestInit, Response,
Result as WorkerResult, State,
};
use super::WorkersClock;
type HmacSha256 = Hmac<Sha256>;
const SIGNATURE_HEADER: &str = "x-cratefield-actor-signature";
const KEY_LABEL: &[u8] = b"cratefield actor protocol v1";
const META_KIND_KEY: &str = "\u{0}kind";
const META_KEY_KEY: &str = "\u{0}key";
const USER_PREFIX: &str = "u:";
fn storage_key(key: &str) -> String {
format!("{USER_PREFIX}{key}")
}
const MAX_FRAME_BYTES: usize =
1 + MAX_ACTOR_KIND_BYTES + 2 + MAX_ACTOR_KEY_BYTES + MAX_ACTOR_MESSAGE_BYTES;
fn frame_matches_identity(
name: Option<&str>,
stored: Option<(&str, &str)>,
kind: &str,
key: &str,
) -> bool {
let named = name.is_none_or(|name| name == format!("{kind}:{key}"));
let persisted =
stored.is_none_or(|(stored_kind, stored_key)| stored_kind == kind && stored_key == key);
named && persisted
}
fn protocol_key(secret: &[u8]) -> [u8; 32] {
hmac_sha256(secret, KEY_LABEL)
}
fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
let mut mac =
<HmacSha256 as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(data);
let mut out = [0u8; 32];
out.copy_from_slice(&mac.finalize().into_bytes());
out
}
fn encode_frame(kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
let kind_len = u8::try_from(kind.len()).map_err(|_| {
ActorError::TooLarge(format!(
"actor kind of {} bytes overflows the frame",
kind.len()
))
})?;
let key_len = u16::try_from(key.len()).map_err(|_| {
ActorError::TooLarge(format!(
"actor key of {} bytes overflows the frame",
key.len()
))
})?;
let mut frame = Vec::with_capacity(1 + kind.len() + 2 + key.len() + message.len());
frame.push(kind_len);
frame.extend_from_slice(kind.as_bytes());
frame.extend_from_slice(&key_len.to_be_bytes());
frame.extend_from_slice(key.as_bytes());
frame.extend_from_slice(message);
Ok(frame)
}
fn decode_frame(frame: &[u8]) -> Result<(String, String, Vec<u8>), ActorError> {
let mut cursor = frame;
let (&kind_len, rest) = cursor.split_first().ok_or_else(malformed)?;
cursor = rest;
let kind_len = usize::from(kind_len);
let (kind, rest) = split_at(cursor, kind_len)?;
let (key_len, rest) = split_at(rest, 2)?;
let key_len = usize::from(u16::from_be_bytes([key_len[0], key_len[1]]));
let (key, message) = split_at(rest, key_len)?;
let kind = std::str::from_utf8(kind)
.map_err(|_| ActorError::Operation("actor frame kind is not utf-8".to_owned()))?;
let key = std::str::from_utf8(key)
.map_err(|_| ActorError::Operation("actor frame key is not utf-8".to_owned()))?;
Ok((kind.to_owned(), key.to_owned(), message.to_vec()))
}
fn split_at(bytes: &[u8], at: usize) -> Result<(&[u8], &[u8]), ActorError> {
if bytes.len() < at {
return Err(malformed());
}
Ok(bytes.split_at(at))
}
fn malformed() -> ActorError {
ActorError::Operation("actor frame is malformed".to_owned())
}
fn sign_frame(key: &[u8], frame: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(hmac_sha256(key, frame))
}
fn verify_frame(signature: &str, key: &[u8], frame: &[u8]) -> bool {
let Ok(bytes) = URL_SAFE_NO_PAD.decode(signature) else {
return false;
};
let Ok(expected) = <[u8; 32]>::try_from(bytes) else {
return false;
};
bool::from(hmac_sha256(key, frame).ct_eq(&expected))
}
fn verification_keys(config: &HarnessConfig) -> Vec<[u8; 32]> {
let mut keys = vec![protocol_key(config.harness_secret.as_bytes())];
if let Some(previous) = &config.harness_secret_previous {
keys.push(protocol_key(previous.as_bytes()));
}
keys
}
pub struct ActorDriver {
state: DurableObjectState,
handlers: ActorHandlers,
keys: Vec<[u8; 32]>,
lock: Mutex<()>,
}
impl ActorDriver {
#[must_use]
pub fn new(state: State, env: &Env, handlers: ActorHandlers) -> Self {
let keys = match HarnessConfig::from_config(&crate::config::EnvConfig(env.clone())) {
Ok(config) => verification_keys(&config),
Err(_) => Vec::new(),
};
Self {
state: state._inner(),
handlers,
keys,
lock: Mutex::new(()),
}
}
pub async fn fetch(&self, req: Request) -> WorkerResult<Response> {
let mut req = req;
let Ok(Some(signature)) = req.headers().get(SIGNATURE_HEADER) else {
return Response::error("actor request is not signed", 403);
};
if declared_content_length(&req).is_some_and(|length| length > MAX_FRAME_BYTES) {
return Response::error("actor frame is too large", 413);
}
let frame = req.bytes().await?;
if frame.len() > MAX_FRAME_BYTES {
return Response::error("actor frame is too large", 413);
}
if !self
.keys
.iter()
.any(|key| verify_frame(&signature, key, &frame))
{
return Response::error("actor request signature is invalid", 403);
}
let Ok((kind, key, message)) = decode_frame(&frame) else {
return Response::error("actor frame is malformed", 400);
};
if validate_actor_kind(&kind).is_err() || validate_actor_key(&key).is_err() {
return Response::error("actor kind or key is invalid", 400);
}
let Some(handler) = self.handlers.get(&kind) else {
return Response::error("no handler for this actor kind", 404);
};
let _serialized = self.lock.lock().await;
let stored = read_meta(&self.state).await?;
let named_identity = self.state.id()?.name();
if !frame_matches_identity(
named_identity.as_deref(),
stored
.as_ref()
.map(|(kind, key)| (kind.as_str(), key.as_str())),
&kind,
&key,
) {
return Response::error("actor frame names a different actor", 400);
}
let store = DurableActorStore::new(&self.state, &kind, &key);
match run_actor_message(
handler.as_ref(),
&store,
&WorkersClock,
&kind,
&key,
&message,
)
.await
{
Ok(reply) => Response::from_bytes(reply),
Err(error) => actor_error_response(&error),
}
}
pub async fn alarm(&self) -> WorkerResult<Response> {
let _serialized = self.lock.lock().await;
let Some((kind, key)) = read_meta(&self.state).await? else {
return Response::empty();
};
let Some(handler) = self.handlers.get(&kind) else {
return Response::empty();
};
let store = DurableActorStore::new(&self.state, &kind, &key);
match run_actor_alarm(handler.as_ref(), &store, &WorkersClock, &kind, &key).await {
Ok(()) => Response::empty(),
Err(error) => Err(worker::Error::RustError(error.to_string())),
}
}
}
fn declared_content_length(req: &Request) -> Option<usize> {
req.headers()
.get("content-length")
.ok()
.flatten()
.and_then(|value| value.parse().ok())
}
async fn read_meta(state: &DurableObjectState) -> WorkerResult<Option<(String, String)>> {
let storage = state.storage()?;
let kind = storage_get::<String>(&storage, META_KIND_KEY).await?;
let key = storage_get::<String>(&storage, META_KEY_KEY).await?;
Ok(match (kind, key) {
(Some(kind), Some(key)) => Some((kind, key)),
_ => None,
})
}
async fn storage_get<T: DeserializeOwned>(
storage: &DurableObjectStorage,
key: &str,
) -> WorkerResult<Option<T>> {
let value = JsFuture::from(storage.get(key)?).into_send().await?;
if value.is_undefined() {
return Ok(None);
}
Ok(Some(serde_wasm_bindgen::from_value(value)?))
}
async fn storage_list(
storage: &DurableObjectStorage,
prefix: &str,
) -> WorkerResult<BTreeMap<String, Vec<u8>>> {
let options = ListOptions::new().prefix(prefix);
let promise = storage.list_with_options(serde_wasm_bindgen::to_value(&options)?.into())?;
let listed = JsFuture::from(promise).into_send().await?;
serde_wasm_bindgen::from_value(listed).map_err(Into::into)
}
async fn storage_alarm(storage: &DurableObjectStorage) -> WorkerResult<Option<i64>> {
let value = JsFuture::from(storage.get_alarm(JsValue::NULL.into())?)
.into_send()
.await?;
#[allow(clippy::cast_possible_truncation)]
Ok(value.as_f64().map(|ms| ms as i64))
}
fn actor_error_response(error: &ActorError) -> WorkerResult<Response> {
let (status, detail) = match error {
ActorError::TooLarge(detail) => (413, detail.clone()),
ActorError::Handler(detail) => (422, detail.clone()),
ActorError::NotConfigured => (404, "no handler for this actor kind".to_owned()),
ActorError::Timeout | ActorError::Operation(_) => (500, error.to_string()),
};
Response::error(detail, status)
}
fn store_error(error: &worker::Error) -> ActorError {
ActorError::Operation(error.to_string())
}
fn js_error(error: JsValue) -> ActorError {
store_error(&worker::Error::from(error))
}
struct DurableActorStore<'a> {
state: &'a DurableObjectState,
kind: String,
key: String,
}
impl<'a> DurableActorStore<'a> {
fn new(state: &'a DurableObjectState, kind: &str, key: &str) -> Self {
Self {
state,
kind: kind.to_owned(),
key: key.to_owned(),
}
}
fn storage(&self) -> Result<DurableObjectStorage, ActorError> {
self.state.storage().map_err(js_error)
}
}
#[async_trait]
impl ActorStore for DurableActorStore<'_> {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, ActorError> {
let storage = self.storage()?;
storage_get::<Vec<u8>>(&storage, &storage_key(key))
.await
.map_err(|error| store_error(&error))
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError> {
let storage = self.storage()?;
let entries = storage_list(&storage, &storage_key(prefix))
.await
.map_err(|error| store_error(&error))?;
Ok(entries
.into_iter()
.filter_map(|(key, value)| {
key.strip_prefix(USER_PREFIX)
.map(|key| (key.to_owned(), value))
})
.collect())
}
async fn alarm(&self) -> Result<Option<OffsetDateTime>, ActorError> {
let storage = self.storage()?;
match storage_alarm(&storage)
.await
.map_err(|error| store_error(&error))?
{
Some(ms) => Ok(Some(ms_to_alarm(ms)?)),
None => Ok(None),
}
}
async fn commit(&self, writes: ActorWrites) -> Result<(), ActorError> {
let ActorWrites {
clear_all,
entries,
alarm,
} = writes;
let kind = self.kind.clone();
let key = self.key.clone();
let entries: Vec<(String, Option<Vec<u8>>)> = entries.into_iter().collect();
let clearing: Vec<String> = if clear_all {
let storage = self.storage()?;
storage_list(&storage, USER_PREFIX)
.await
.map_err(|error| store_error(&error))?
.into_keys()
.collect()
} else {
Vec::new()
};
let storage = self.storage()?;
let closure: Box<dyn FnOnce(DurableObjectTransaction) -> Promise> =
Box::new(move |tx: DurableObjectTransaction| -> Promise {
future_to_promise(AssertUnwindSafe(async move {
apply_writes(tx, clearing, entries, kind, key, alarm).await
}))
});
let closure = SendWrapper::new(Closure::once_assert_unwind_safe(closure));
JsFuture::from(storage.transaction(&closure).map_err(js_error)?)
.into_send()
.await
.map_err(js_error)?;
Ok(())
}
}
#[allow(clippy::option_option)]
async fn apply_writes(
tx: DurableObjectTransaction,
clearing: Vec<String>,
entries: Vec<(String, Option<Vec<u8>>)>,
kind: String,
key: String,
alarm: Option<Option<OffsetDateTime>>,
) -> Result<JsValue, JsValue> {
for stored in clearing {
JsFuture::from(tx.delete(&stored)?).await?;
}
for (entry_key, value) in entries {
let stored = storage_key(&entry_key);
match value {
Some(bytes) => {
let value = serde_wasm_bindgen::to_value(&bytes)?;
JsFuture::from(tx.put(&stored, value)?).await?;
}
None => {
JsFuture::from(tx.delete(&stored)?).await?;
}
}
}
JsFuture::from(tx.put(META_KIND_KEY, serde_wasm_bindgen::to_value(&kind)?)?).await?;
JsFuture::from(tx.put(META_KEY_KEY, serde_wasm_bindgen::to_value(&key)?)?).await?;
if let Some(alarm) = alarm {
let this: &JsValue = tx.as_ref();
if let Some(at) = alarm {
#[allow(clippy::cast_precision_loss)]
let when = Date::new(&JsValue::from_f64(alarm_epoch_ms(at) as f64));
let set = transaction_method(&tx, "setAlarm")?;
let promise: Promise = set.call2(this, &when, &JsValue::NULL)?.unchecked_into();
JsFuture::from(promise).await?;
} else {
let delete = transaction_method(&tx, "deleteAlarm")?;
let promise: Promise = delete.call1(this, &JsValue::NULL)?.unchecked_into();
JsFuture::from(promise).await?;
}
}
Ok(JsValue::UNDEFINED)
}
fn transaction_method(tx: &DurableObjectTransaction, name: &str) -> Result<Function, JsValue> {
let object: &JsValue = tx.as_ref();
Reflect::get(object, &JsValue::from_str(name))?.dyn_into()
}
fn ms_to_alarm(ms: i64) -> Result<OffsetDateTime, ActorError> {
OffsetDateTime::from_unix_timestamp_nanos(i128::from(ms) * 1_000_000)
.map_err(|error| ActorError::Operation(format!("stored alarm is not a time: {error}")))
}
fn alarm_epoch_ms(at: OffsetDateTime) -> i64 {
let nanos = at.unix_timestamp_nanos();
let ms = nanos / 1_000_000;
i64::try_from(ms).unwrap_or(if ms.is_negative() { i64::MIN } else { i64::MAX })
}
pub(crate) struct DurableActors {
namespace: ObjectNamespace,
signing_key: Vec<u8>,
}
impl DurableActors {
pub(crate) fn new(namespace: ObjectNamespace, config: &HarnessConfig) -> Self {
Self {
namespace,
signing_key: protocol_key(config.harness_secret.as_bytes()).to_vec(),
}
}
}
#[async_trait]
impl Actors for DurableActors {
async fn call(&self, kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
check_actor_call(kind, key, message)?;
if self.signing_key.is_empty() {
return Err(ActorError::Operation(
"actor host has no signing key: HARNESS_SECRET is not configured".to_owned(),
));
}
let frame = encode_frame(kind, key, message)?;
let signature = sign_frame(&self.signing_key, &frame);
let mut init = RequestInit::new();
init.method = Method::Post;
let headers = Headers::new();
headers
.set(SIGNATURE_HEADER, &signature)
.map_err(|error| store_error(&error))?;
init.headers = headers;
init.with_body(Some(Uint8Array::from(frame.as_slice()).into()));
let request = Request::new_with_init("https://actors.invalid/", &init)
.map_err(|error| store_error(&error))?;
let stub = self
.namespace
.id_from_name(&format!("{kind}:{key}"))
.map_err(|error| store_error(&error))?
.get_stub()
.map_err(|error| store_error(&error))?;
let mut response = stub
.fetch_with_request(request)
.into_send()
.await
.map_err(|error| store_error(&error))?;
match response.status_code() {
200 => response
.bytes()
.into_send()
.await
.map_err(|error| store_error(&error)),
404 => Err(ActorError::NotConfigured),
413 => Err(ActorError::TooLarge(response_text(&mut response).await)),
422 => Err(ActorError::Handler(response_text(&mut response).await)),
status => Err(ActorError::Operation(format!(
"actor host answered {status}: {}",
response_text(&mut response).await
))),
}
}
}
async fn response_text(response: &mut Response) -> String {
response.text().into_send().await.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn keys() -> (HarnessConfig, [u8; 32], Option<[u8; 32]>) {
let config = HarnessConfig {
harness_secret: "test-secret-0123456789abcdef-0123".to_owned(),
harness_secret_previous: Some("previous-secret-0123456789abcdef".to_owned()),
harness_secret_revoked: Vec::new(),
harness_venture: None,
admin_token: None,
env: cratefield_core::VentureEnv::Development,
};
let current = protocol_key(config.harness_secret.as_bytes());
let previous = config
.harness_secret_previous
.as_ref()
.map(|secret| protocol_key(secret.as_bytes()));
(config, current, previous)
}
#[test]
fn a_frame_round_trips() {
let frame = encode_frame("counter", "user-7", b"inc").expect("fits");
assert_eq!(
decode_frame(&frame).expect("decodes"),
("counter".to_owned(), "user-7".to_owned(), b"inc".to_vec())
);
let frame = encode_frame("counter", "k", b"").expect("fits");
assert_eq!(decode_frame(&frame).expect("decodes").2, Vec::<u8>::new());
}
#[test]
fn a_malformed_frame_is_refused() {
assert!(decode_frame(&[4, b'a']).is_err());
assert!(decode_frame(&[1, b'a', 0, 9, b'k']).is_err());
assert!(decode_frame(&[]).is_err());
assert!(decode_frame(&[1, 0xff, 0, 1, b'k']).is_err());
}
#[test]
fn a_signature_verifies_and_a_tampered_frame_does_not() {
let (_, current, _) = keys();
let frame = encode_frame("counter", "k", b"inc").expect("fits");
let signature = sign_frame(¤t, &frame);
assert!(verify_frame(&signature, ¤t, &frame));
let mut tampered = frame.clone();
tampered[0] = tampered[0].wrapping_add(1);
assert!(!verify_frame(&signature, ¤t, &tampered));
}
#[test]
fn a_signature_over_a_different_kind_or_key_is_refused() {
let (_, current, _) = keys();
let signature = sign_frame(
¤t,
&encode_frame("counter", "one", b"m").expect("fits"),
);
assert!(!verify_frame(
&signature,
¤t,
&encode_frame("counter", "two", b"m").expect("fits")
));
assert!(!verify_frame(
&signature,
¤t,
&encode_frame("other", "one", b"m").expect("fits")
));
}
#[test]
fn a_wrong_secret_is_refused_and_the_previous_one_is_accepted() {
let (config, current, previous) = keys();
let frame = encode_frame("counter", "k", b"inc").expect("fits");
let previous = previous.expect("the fixture sets one");
let previous_signature = sign_frame(&previous, &frame);
assert!(!verify_frame(&previous_signature, ¤t, &frame));
let accepted = verification_keys(&config)
.iter()
.any(|key| verify_frame(&previous_signature, key, &frame));
assert!(accepted, "the previous secret was not accepted");
let other = protocol_key(b"a secret nobody configured");
let signature = sign_frame(&other, &frame);
assert!(
!verification_keys(&config)
.iter()
.any(|key| verify_frame(&signature, key, &frame))
);
}
#[test]
fn a_malformed_signature_is_refused() {
let (_, current, _) = keys();
let frame = encode_frame("counter", "k", b"inc").expect("fits");
assert!(!verify_frame("not base64!", ¤t, &frame));
assert!(!verify_frame("", ¤t, &frame));
assert!(!verify_frame(
&URL_SAFE_NO_PAD.encode(b"short"),
¤t,
&frame
));
}
#[test]
fn the_protocol_key_is_domain_separated_from_the_secret() {
let (_, current, _) = keys();
assert_ne!(¤t[..], b"test-secret-0123456789abcdef-0123");
}
#[test]
fn an_alarm_round_trips_through_epoch_milliseconds() {
let now = OffsetDateTime::UNIX_EPOCH;
assert_eq!(
alarm_epoch_ms(now + time::Duration::milliseconds(1500)),
1500
);
assert_eq!(
ms_to_alarm(1500).expect("a time"),
now + time::Duration::milliseconds(1500)
);
assert_eq!(alarm_epoch_ms(now), 0);
}
#[test]
fn a_frame_matches_its_identity() {
assert!(frame_matches_identity(
Some("counter:k"),
None,
"counter",
"k"
));
assert!(!frame_matches_identity(
Some("counter:other"),
None,
"counter",
"k"
));
assert!(!frame_matches_identity(
Some("other:k"),
None,
"counter",
"k"
));
assert!(frame_matches_identity(None, None, "counter", "k"));
assert!(frame_matches_identity(
None,
Some(("counter", "k")),
"counter",
"k"
));
assert!(!frame_matches_identity(
None,
Some(("counter", "other")),
"counter",
"k"
));
assert!(!frame_matches_identity(
None,
Some(("other", "k")),
"counter",
"k"
));
assert!(!frame_matches_identity(
Some("counter:k"),
Some(("counter", "other")),
"counter",
"k"
));
}
}