chore: rename quicproquo → quicprochat in Rust workspace

Rename all crate directories, package names, binary names, proto
package/module paths, ALPN strings, env var prefixes, config filenames,
mDNS service names, and plugin ABI symbols from quicproquo/qpq to
quicprochat/qpc.
This commit is contained in:
2026-03-07 18:24:52 +01:00
parent d8c1392587
commit a710037dde
212 changed files with 609 additions and 609 deletions

View File

@@ -0,0 +1,51 @@
//! Account handler — account deletion.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::account::AccountService;
use super::{domain_err, require_auth, ServerState};
pub async fn handle_delete_account(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
// DeleteAccountRequest is empty but decode for protocol correctness.
let _req = match v1::DeleteAccountRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = AccountService {
store: Arc::clone(&state.store),
kt_log: Arc::clone(&state.kt_log),
};
match svc.delete_account(&identity_key) {
Ok(()) => {
// Remove session for the deleted account.
if let Some(token) = ctx.session_token.as_deref() {
state.sessions.remove(token);
}
let proto = v1::DeleteAccountResponse { success: true };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}

View File

@@ -0,0 +1,255 @@
//! OPAQUE auth handlers — registration and login.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::error::RpcStatus;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::auth::{PendingLogin, SessionInfo, SESSION_TTL_SECS};
use crate::domain::auth::AuthService;
use crate::domain::types::{RegisterFinishReq, RegisterStartReq};
use super::ServerState;
pub async fn handle_opaque_register_start(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::OpaqueRegisterStartRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.username.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "username must not be empty");
}
let svc = AuthService {
store: Arc::clone(&state.store),
opaque_setup: Arc::clone(&state.opaque_setup),
pending_logins: Arc::clone(&state.pending_logins),
sessions: Arc::clone(&state.sessions),
auth_cfg: Arc::clone(&state.auth_cfg),
};
let domain_req = RegisterStartReq {
username: req.username,
request_bytes: req.request,
};
match svc.register_start(domain_req) {
Ok(resp) => {
let proto = v1::OpaqueRegisterStartResponse {
response: resp.response_bytes,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("register_start: {e}")),
}
}
pub async fn handle_opaque_register_finish(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::OpaqueRegisterFinishRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.username.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "username must not be empty");
}
let svc = AuthService {
store: Arc::clone(&state.store),
opaque_setup: Arc::clone(&state.opaque_setup),
pending_logins: Arc::clone(&state.pending_logins),
sessions: Arc::clone(&state.sessions),
auth_cfg: Arc::clone(&state.auth_cfg),
};
let domain_req = RegisterFinishReq {
username: req.username.clone(),
upload_bytes: req.upload,
identity_key: req.identity_key.clone(),
};
match svc.register_finish(domain_req) {
Ok(resp) => {
state
.hooks
.on_user_registered(&req.username, &req.identity_key);
let proto = v1::OpaqueRegisterFinishResponse {
success: resp.success,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("register_finish: {e}")),
}
}
pub async fn handle_opaque_login_start(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::OpaqueLoginStartRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.username.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "username must not be empty");
}
// Look up user record.
let user_record = match state.store.get_user_record(&req.username) {
Ok(Some(r)) => r,
Ok(None) => {
return HandlerResult::err(RpcStatus::NotFound, "user not found");
}
Err(e) => return HandlerResult::err(RpcStatus::Internal, &format!("store: {e}")),
};
// Deserialise stored registration.
let registration =
match opaque_ke::ServerRegistration::<quicprochat_core::opaque_auth::OpaqueSuite>::deserialize(&user_record) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
RpcStatus::Internal,
&format!("corrupt user record: {e}"),
)
}
};
// Start login.
let credential_request =
match opaque_ke::CredentialRequest::<quicprochat_core::opaque_auth::OpaqueSuite>::deserialize(&req.request)
{
Ok(r) => r,
Err(e) => {
return HandlerResult::err(RpcStatus::BadRequest, &format!("bad login request: {e}"))
}
};
let login_start = match opaque_ke::ServerLogin::<
quicprochat_core::opaque_auth::OpaqueSuite,
>::start(
&mut rand::rngs::OsRng,
&state.opaque_setup,
Some(registration),
credential_request,
req.username.as_bytes(),
Default::default(),
) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(RpcStatus::Internal, &format!("login start: {e}"));
}
};
let response_bytes = login_start.message.serialize().to_vec();
// Store pending login state.
let now = crate::auth::current_timestamp();
state.pending_logins.insert(
req.username.clone(),
PendingLogin {
state_bytes: login_start.state.serialize().to_vec(),
created_at: now,
},
);
let proto = v1::OpaqueLoginStartResponse {
response: response_bytes,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
pub async fn handle_opaque_login_finish(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::OpaqueLoginFinishRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.username.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "username must not be empty");
}
// Retrieve pending login state.
let pending = match state.pending_logins.remove(&req.username) {
Some((_, p)) => p,
None => {
return HandlerResult::err(
RpcStatus::BadRequest,
"no pending login for this username",
);
}
};
let login_state = match opaque_ke::ServerLogin::<
quicprochat_core::opaque_auth::OpaqueSuite,
>::deserialize(&pending.state_bytes)
{
Ok(s) => s,
Err(e) => {
return HandlerResult::err(
RpcStatus::Internal,
&format!("corrupt pending login: {e}"),
)
}
};
let finalization = match opaque_ke::CredentialFinalization::<
quicprochat_core::opaque_auth::OpaqueSuite,
>::deserialize(&req.finalization)
{
Ok(f) => f,
Err(e) => {
return HandlerResult::err(RpcStatus::BadRequest, &format!("bad finalization: {e}"));
}
};
if let Err(e) = login_state.finish(finalization, Default::default()) {
state.hooks.on_auth(&crate::hooks::AuthEvent {
username: req.username.clone(),
success: false,
failure_reason: format!("{e}"),
});
return HandlerResult::err(RpcStatus::Unauthorized, &format!("login failed: {e}"));
}
// Generate session token.
let mut token = vec![0u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token);
let now = crate::auth::current_timestamp();
state.sessions.insert(
token.clone(),
SessionInfo {
username: req.username.clone(),
identity_key: req.identity_key.clone(),
created_at: now,
expires_at: now + SESSION_TTL_SECS,
},
);
state.hooks.on_auth(&crate::hooks::AuthEvent {
username: req.username,
success: true,
failure_reason: String::new(),
});
let proto = v1::OpaqueLoginFinishResponse {
session_token: token,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}

View File

@@ -0,0 +1,101 @@
//! Blob handlers — chunked file upload/download.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::blobs::BlobService;
use crate::domain::types::{CallerAuth, DownloadBlobReq, UploadBlobReq};
use super::{domain_err, require_auth, ServerState};
fn caller_auth(identity_key: Vec<u8>) -> CallerAuth {
CallerAuth {
identity_key,
token: Vec::new(),
device_id: None,
}
}
pub async fn handle_upload_blob(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::UploadBlobRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = BlobService {
data_dir: state.data_dir.clone(),
};
let auth = caller_auth(identity_key);
let domain_req = UploadBlobReq {
blob_hash: req.blob_hash,
chunk: req.chunk,
offset: req.offset,
total_size: req.total_size,
mime_type: req.mime_type,
};
match svc.upload_blob(domain_req, &auth) {
Ok(resp) => {
let proto = v1::UploadBlobResponse {
blob_id: resp.blob_id,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_download_blob(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::DownloadBlobRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = BlobService {
data_dir: state.data_dir.clone(),
};
let auth = caller_auth(identity_key);
let domain_req = DownloadBlobReq {
blob_id: req.blob_id,
offset: req.offset,
length: req.length,
};
match svc.download_blob(domain_req, &auth) {
Ok(resp) => {
let proto = v1::DownloadBlobResponse {
chunk: resp.chunk,
total_size: resp.total_size,
mime_type: resp.mime_type,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}

View File

@@ -0,0 +1,60 @@
//! Channel handler — 1:1 DM channel creation.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::channels::ChannelService;
use crate::domain::types::CreateChannelReq;
use crate::hooks::ChannelEvent;
use super::{domain_err, require_auth, ServerState};
pub async fn handle_create_channel(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::CreateChannelRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = ChannelService {
store: Arc::clone(&state.store),
};
let domain_req = CreateChannelReq {
peer_key: req.peer_key.clone(),
};
match svc.create_channel(domain_req, &identity_key) {
Ok(resp) => {
state.hooks.on_channel_created(&ChannelEvent {
channel_id: resp.channel_id.clone(),
initiator_key: identity_key,
peer_key: req.peer_key,
was_new: resp.was_new,
});
let proto = v1::CreateChannelResponse {
channel_id: resp.channel_id,
was_new: resp.was_new,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}

View File

@@ -0,0 +1,408 @@
//! Delivery handlers — enqueue, fetch, fetch_wait, peek, ack, batch_enqueue.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::error::RpcStatus;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use sha2::{Digest, Sha256};
use tokio::sync::Notify;
use crate::domain::delivery::DeliveryService;
use crate::domain::types::{AckReq, BatchEnqueueReq, EnqueueReq, FetchReq, PeekReq};
use crate::hooks::{HookAction, MessageEvent};
use super::{require_auth, ServerState};
/// Build a 96-byte delivery proof: `SHA-256(seq || recipient_key || timestamp_ms) || Ed25519(hash)`.
///
/// The sender stores this as cryptographic evidence that the server enqueued the message.
fn build_delivery_proof(
signing_key: &quicprochat_core::IdentityKeypair,
seq: u64,
recipient_key: &[u8],
timestamp_ms: u64,
) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(seq.to_le_bytes());
hasher.update(recipient_key);
hasher.update(timestamp_ms.to_le_bytes());
let hash: [u8; 32] = hasher.finalize().into();
let sig = signing_key.sign_raw(&hash);
let mut proof = vec![0u8; 96];
proof[..32].copy_from_slice(&hash);
proof[32..].copy_from_slice(&sig);
proof
}
pub async fn handle_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::EnqueueRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.recipient_key.is_empty() || req.payload.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "recipient_key and payload required");
}
// Rate limiting.
if let Err(_e) = crate::auth::check_rate_limit(&state.rate_limits, &identity_key) {
return HandlerResult::err(RpcStatus::RateLimited, "rate limit exceeded");
}
// Idempotency dedup: if message_id is provided and already seen, return the cached seq.
if !req.message_id.is_empty() {
if let Some(entry) = state.seen_message_ids.get(&req.message_id) {
let (cached_seq, _ts) = *entry;
let proto = v1::EnqueueResponse {
seq: cached_seq,
delivery_proof: Vec::new(),
duplicate: true,
};
return HandlerResult::ok(Bytes::from(proto.encode_to_vec()));
}
}
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),
};
let domain_req = EnqueueReq {
recipient_key: req.recipient_key.clone(),
payload: req.payload.clone(),
channel_id: req.channel_id.clone(),
ttl_secs: req.ttl_secs,
};
match svc.enqueue(domain_req) {
Ok(resp) => {
// Record message_id for dedup.
if !req.message_id.is_empty() {
let now = crate::auth::current_timestamp();
state.seen_message_ids.insert(req.message_id, (resp.seq, now));
}
// Fire hook.
let action = state.hooks.on_message_enqueue(&MessageEvent {
sender_identity: Some(identity_key),
recipient_key: req.recipient_key.clone(),
channel_id: req.channel_id,
payload_len: req.payload.len(),
seq: resp.seq,
});
if let HookAction::Reject(reason) = action {
return HandlerResult::err(RpcStatus::Forbidden, &reason);
}
// Build server-signed delivery proof.
let timestamp_ms = crate::auth::current_timestamp();
let delivery_proof = build_delivery_proof(
&state.signing_key,
resp.seq,
&req.recipient_key,
timestamp_ms,
);
let proto = v1::EnqueueResponse {
seq: resp.seq,
delivery_proof,
duplicate: false,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("enqueue: {e}")),
}
}
pub async fn handle_fetch(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::FetchRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),
};
let base_key = if req.recipient_key.is_empty() {
identity_key
} else {
req.recipient_key
};
let recipient_key = if req.device_id.is_empty() {
base_key
} else {
DeliveryService::device_recipient_key(&base_key, &req.device_id)
};
let domain_req = FetchReq {
recipient_key,
channel_id: req.channel_id,
limit: req.limit,
};
match svc.fetch(domain_req) {
Ok(resp) => {
let proto = v1::FetchResponse {
payloads: resp
.payloads
.into_iter()
.map(|e| v1::Envelope {
seq: e.seq,
data: e.data,
})
.collect(),
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("fetch: {e}")),
}
}
pub async fn handle_fetch_wait(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::FetchWaitRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let base_key = if req.recipient_key.is_empty() {
identity_key
} else {
req.recipient_key
};
let recipient_key = if req.device_id.is_empty() {
base_key
} else {
DeliveryService::device_recipient_key(&base_key, &req.device_id)
};
let timeout_ms = if req.timeout_ms == 0 {
30_000
} else {
req.timeout_ms.min(60_000)
};
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),
};
// Try immediate fetch first.
let fetch_req = FetchReq {
recipient_key: recipient_key.clone(),
channel_id: req.channel_id.clone(),
limit: req.limit,
};
match svc.fetch(fetch_req) {
Ok(resp) if !resp.payloads.is_empty() => {
let proto = v1::FetchWaitResponse {
payloads: resp
.payloads
.into_iter()
.map(|e| v1::Envelope {
seq: e.seq,
data: e.data,
})
.collect(),
};
return HandlerResult::ok(Bytes::from(proto.encode_to_vec()));
}
Err(e) => {
return HandlerResult::err(RpcStatus::Internal, &format!("fetch: {e}"));
}
_ => {}
}
// Long-poll: wait for notification or timeout.
let notify = state
.waiters
.entry(recipient_key.clone())
.or_insert_with(|| Arc::new(Notify::new()))
.clone();
let timeout = tokio::time::Duration::from_millis(timeout_ms);
let _ = tokio::time::timeout(timeout, notify.notified()).await;
// Re-fetch after wake or timeout.
let fetch_req = FetchReq {
recipient_key,
channel_id: req.channel_id,
limit: req.limit,
};
match svc.fetch(fetch_req) {
Ok(resp) => {
let proto = v1::FetchWaitResponse {
payloads: resp
.payloads
.into_iter()
.map(|e| v1::Envelope {
seq: e.seq,
data: e.data,
})
.collect(),
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("fetch: {e}")),
}
}
pub async fn handle_peek(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::PeekRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),
};
let base_key = if req.recipient_key.is_empty() {
identity_key
} else {
req.recipient_key
};
let recipient_key = if req.device_id.is_empty() {
base_key
} else {
DeliveryService::device_recipient_key(&base_key, &req.device_id)
};
let domain_req = PeekReq {
recipient_key,
channel_id: req.channel_id,
limit: req.limit,
};
match svc.peek(domain_req) {
Ok(resp) => {
let proto = v1::PeekResponse {
payloads: resp
.payloads
.into_iter()
.map(|e| v1::Envelope {
seq: e.seq,
data: e.data,
})
.collect(),
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("peek: {e}")),
}
}
pub async fn handle_ack(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::AckRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),
};
let base_key = if req.recipient_key.is_empty() {
identity_key
} else {
req.recipient_key
};
let recipient_key = if req.device_id.is_empty() {
base_key
} else {
DeliveryService::device_recipient_key(&base_key, &req.device_id)
};
let domain_req = AckReq {
recipient_key,
channel_id: req.channel_id,
seq_up_to: req.seq_up_to,
};
match svc.ack(domain_req) {
Ok(()) => {
let proto = v1::AckResponse {};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("ack: {e}")),
}
}
pub async fn handle_batch_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::BatchEnqueueRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.recipient_keys.is_empty() || req.payload.is_empty() {
return HandlerResult::err(
RpcStatus::BadRequest,
"recipient_keys and payload required",
);
}
// Rate limiting.
if let Err(_e) = crate::auth::check_rate_limit(&state.rate_limits, &identity_key) {
return HandlerResult::err(RpcStatus::RateLimited, "rate limit exceeded");
}
let svc = DeliveryService {
store: Arc::clone(&state.store),
waiters: Arc::clone(&state.waiters),
};
let domain_req = BatchEnqueueReq {
recipient_keys: req.recipient_keys,
payload: req.payload,
channel_id: req.channel_id,
ttl_secs: req.ttl_secs,
};
match svc.batch_enqueue(domain_req) {
Ok(resp) => {
let proto = v1::BatchEnqueueResponse { seqs: resp.seqs };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("batch_enqueue: {e}")),
}
}

View File

@@ -0,0 +1,127 @@
//! Device handlers — register, list, revoke devices.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::devices::DeviceService;
use crate::domain::types::{RegisterDeviceReq, RevokeDeviceReq};
use super::{domain_err, require_auth, ServerState};
pub async fn handle_register_device(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::RegisterDeviceRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = DeviceService {
store: Arc::clone(&state.store),
};
let domain_req = RegisterDeviceReq {
device_id: req.device_id,
device_name: req.device_name,
};
match svc.register_device(domain_req, &identity_key) {
Ok(resp) => {
let proto = v1::RegisterDeviceResponse {
success: resp.success,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_list_devices(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
// ListDevicesRequest is empty but we still decode for protocol correctness.
let _req = match v1::ListDevicesRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = DeviceService {
store: Arc::clone(&state.store),
};
match svc.list_devices(&identity_key) {
Ok(resp) => {
let proto = v1::ListDevicesResponse {
devices: resp
.devices
.into_iter()
.map(|d| v1::Device {
device_id: d.device_id,
device_name: d.device_name,
registered_at: d.registered_at,
})
.collect(),
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_revoke_device(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::RevokeDeviceRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = DeviceService {
store: Arc::clone(&state.store),
};
let domain_req = RevokeDeviceReq {
device_id: req.device_id,
};
match svc.revoke_device(domain_req, &identity_key) {
Ok(resp) => {
let proto = v1::RevokeDeviceResponse {
success: resp.success,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}

View File

@@ -0,0 +1,227 @@
//! Federation v2 RPC handlers — relay, proxy, and health.
//!
//! Implements the inbound side of server-to-server federation: accepts relay
//! and proxy requests from peer servers and delegates to local storage.
//! Outbound relay to remote peers is handled by the capnp-based
//! `FederationClient` on the main connection path.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::error::RpcStatus;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::federation::address::FederatedAddress;
use super::ServerState;
/// Validate that the request carries a valid federation auth origin.
fn validate_federation_auth(auth: &Option<v1::FederationAuth>) -> Result<String, HandlerResult> {
let a = auth.as_ref().ok_or_else(|| {
HandlerResult::err(RpcStatus::Unauthorized, "missing federation auth")
})?;
if a.origin.is_empty() {
return Err(HandlerResult::err(
RpcStatus::Unauthorized,
"federation auth origin must not be empty",
));
}
Ok(a.origin.clone())
}
/// Relay a single message to a local recipient.
///
/// This handler is called by peer servers to deliver messages to users
/// homed on this server. If the recipient is not local, returns NotFound
/// (the originating server should route directly to the correct home server).
pub async fn handle_relay_enqueue(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let req = match v1::RelayEnqueueRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let origin = match validate_federation_auth(&req.auth) {
Ok(o) => o,
Err(e) => return e,
};
if req.recipient_key.len() != 32 {
return HandlerResult::err(RpcStatus::BadRequest, "recipient_key must be 32 bytes");
}
if req.payload.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "payload must not be empty");
}
match state
.store
.enqueue(&req.recipient_key, &req.channel_id, req.payload, None)
{
Ok(seq) => {
if let Some(waiter) = state.waiters.get(&req.recipient_key) {
waiter.notify_waiters();
}
tracing::info!(
origin = %origin,
recipient_prefix = %hex::encode(&req.recipient_key[..4]),
seq = seq,
"federation: relayed enqueue"
);
let resp = v1::RelayEnqueueResponse { seq };
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}
Err(e) => HandlerResult::err(RpcStatus::Internal, &format!("store error: {e}")),
}
}
/// Relay a batch of messages to local recipients.
pub async fn handle_relay_batch_enqueue(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::RelayBatchEnqueueRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let _origin = match validate_federation_auth(&req.auth) {
Ok(o) => o,
Err(e) => return e,
};
if req.payload.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "payload must not be empty");
}
let mut seqs = Vec::with_capacity(req.recipient_keys.len());
for rk in &req.recipient_keys {
if rk.len() != 32 {
return HandlerResult::err(
RpcStatus::BadRequest,
"each recipient_key must be 32 bytes",
);
}
match state
.store
.enqueue(rk, &req.channel_id, req.payload.clone(), None)
{
Ok(seq) => {
if let Some(waiter) = state.waiters.get(rk.as_slice()) {
waiter.notify_waiters();
}
seqs.push(seq);
}
Err(e) => {
return HandlerResult::err(RpcStatus::Internal, &format!("store error: {e}"))
}
}
}
tracing::info!(
recipient_count = req.recipient_keys.len(),
"federation: relayed batch_enqueue"
);
let resp = v1::RelayBatchEnqueueResponse { seqs };
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}
/// Proxy a key package fetch from local storage.
pub async fn handle_proxy_fetch_key_package(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::ProxyFetchKeyPackageRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let _origin = match validate_federation_auth(&req.auth) {
Ok(o) => o,
Err(e) => return e,
};
let package = match state.store.fetch_key_package(&req.identity_key) {
Ok(pkg) => pkg.unwrap_or_default(),
Err(e) => return HandlerResult::err(RpcStatus::Internal, &format!("store error: {e}")),
};
let resp = v1::ProxyFetchKeyPackageResponse { package };
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}
/// Proxy a hybrid key fetch from local storage.
pub async fn handle_proxy_fetch_hybrid_key(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::ProxyFetchHybridKeyRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let _origin = match validate_federation_auth(&req.auth) {
Ok(o) => o,
Err(e) => return e,
};
let hybrid_public_key = match state.store.fetch_hybrid_key(&req.identity_key) {
Ok(pk) => pk.unwrap_or_default(),
Err(e) => return HandlerResult::err(RpcStatus::Internal, &format!("store error: {e}")),
};
let resp = v1::ProxyFetchHybridKeyResponse { hybrid_public_key };
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}
/// Proxy a user resolution from local storage.
///
/// Supports federated `user@domain` addresses: if the domain matches the
/// local server, the local user is resolved; otherwise returns empty.
pub async fn handle_proxy_resolve_user(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::ProxyResolveUserRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let _origin = match validate_federation_auth(&req.auth) {
Ok(o) => o,
Err(e) => return e,
};
let addr = FederatedAddress::parse(&req.username);
let is_local = addr.is_local(&state.local_domain);
let identity_key = if is_local {
match state.store.get_user_identity_key(&addr.username) {
Ok(key) => key.unwrap_or_default(),
Err(e) => {
return HandlerResult::err(RpcStatus::Internal, &format!("store error: {e}"))
}
}
} else {
// Remote user: not on this server. Return empty.
Vec::new()
};
let resp = v1::ProxyResolveUserResponse { identity_key };
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}
/// Federation health check — returns ok status and this server's domain.
pub async fn handle_federation_health(
state: Arc<ServerState>,
_ctx: RequestContext,
) -> HandlerResult {
let resp = v1::FederationHealthResponse {
status: "ok".into(),
server_domain: state.local_domain.clone(),
};
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}

View File

@@ -0,0 +1,162 @@
//! Group management handlers — remove member, update metadata, list members, rotate keys.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::error::RpcStatus;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::groups::GroupService;
use crate::domain::types::{ListGroupMembersReq, UpdateGroupMetadataReq};
use super::{domain_err, require_auth, ServerState};
/// Handle RemoveMember (410): track member removal server-side.
///
/// Note: actual MLS removal (Remove proposal + Commit) is done client-side
/// via the SDK. This handler records the membership change on the server.
pub async fn handle_remove_member(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::RemoveMemberRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.group_id.is_empty() || req.member_identity_key.is_empty() {
return HandlerResult::err(
RpcStatus::BadRequest,
"group_id and member_identity_key required",
);
}
let svc = GroupService {
store: Arc::clone(&state.store),
};
match svc.remove_member(&req.group_id, &req.member_identity_key) {
Ok(_) => {
let _ = identity_key; // caller is authorized; removal tracked
let proto = v1::RemoveMemberResponse {
commit: Vec::new(), // commit is generated client-side
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
/// Handle UpdateGroupMetadata (411): store group name, description, avatar.
pub async fn handle_update_group_metadata(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::UpdateGroupMetadataRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let svc = GroupService {
store: Arc::clone(&state.store),
};
let domain_req = UpdateGroupMetadataReq {
group_id: req.group_id,
name: req.name,
description: req.description,
avatar_hash: req.avatar_hash,
};
match svc.update_metadata(domain_req, &identity_key) {
Ok(()) => {
let proto = v1::UpdateGroupMetadataResponse { success: true };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
/// Handle ListGroupMembers (412): return member list with resolved usernames.
pub async fn handle_list_group_members(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::ListGroupMembersRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let svc = GroupService {
store: Arc::clone(&state.store),
};
let domain_req = ListGroupMembersReq {
group_id: req.group_id,
};
match svc.list_members(domain_req) {
Ok(resp) => {
let proto = v1::ListGroupMembersResponse {
members: resp
.members
.into_iter()
.map(|m| v1::GroupMemberInfo {
identity_key: m.identity_key,
username: m.username,
joined_at: m.joined_at,
})
.collect(),
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
/// Handle RotateKeys (413): acknowledge key rotation.
///
/// Actual MLS key rotation (Update proposal + Commit) is done client-side.
/// This handler exists for server-side tracking and future rate limiting.
pub async fn handle_rotate_keys(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::RotateKeysRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.group_id.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "group_id required");
}
// Key rotation is handled entirely client-side in MLS.
// This endpoint is for server-side auditing and future rate limiting.
let proto = v1::RotateKeysResponse {
commit: Vec::new(), // commit is generated client-side
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}

View File

@@ -0,0 +1,217 @@
//! Key management handlers — KeyPackage and hybrid key operations.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::keys::KeyService;
use crate::domain::types::{
CallerAuth, FetchHybridKeyReq, FetchHybridKeysReq, FetchKeyPackageReq, UploadHybridKeyReq,
UploadKeyPackageReq,
};
use super::{domain_err, require_auth, ServerState};
fn caller_auth(identity_key: Vec<u8>) -> CallerAuth {
CallerAuth {
identity_key,
token: Vec::new(),
device_id: None,
}
}
pub async fn handle_upload_key_package(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::UploadKeyPackageRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = KeyService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = UploadKeyPackageReq {
identity_key: req.identity_key,
package: req.package,
};
match svc.upload_key_package(domain_req, &auth) {
Ok(resp) => {
let proto = v1::UploadKeyPackageResponse {
fingerprint: resp.fingerprint,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_fetch_key_package(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::FetchKeyPackageRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = KeyService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = FetchKeyPackageReq {
identity_key: req.identity_key,
};
match svc.fetch_key_package(domain_req, &auth) {
Ok(resp) => {
let proto = v1::FetchKeyPackageResponse {
package: resp.package,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_upload_hybrid_key(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::UploadHybridKeyRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = KeyService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = UploadHybridKeyReq {
identity_key: req.identity_key,
hybrid_public_key: req.hybrid_public_key,
};
match svc.upload_hybrid_key(domain_req, &auth) {
Ok(()) => {
let proto = v1::UploadHybridKeyResponse {};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_fetch_hybrid_key(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::FetchHybridKeyRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = KeyService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = FetchHybridKeyReq {
identity_key: req.identity_key,
};
match svc.fetch_hybrid_key(domain_req, &auth) {
Ok(resp) => {
let proto = v1::FetchHybridKeyResponse {
hybrid_public_key: resp.hybrid_public_key,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_fetch_hybrid_keys(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::FetchHybridKeysRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = KeyService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = FetchHybridKeysReq {
identity_keys: req.identity_keys,
};
match svc.fetch_hybrid_keys(domain_req, &auth) {
Ok(resp) => {
let proto = v1::FetchHybridKeysResponse { keys: resp.keys };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}

View File

@@ -0,0 +1,439 @@
//! v2 RPC handler dispatch — protobuf in, domain logic, protobuf out.
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use dashmap::DashMap;
use opaque_ke::ServerSetup;
use quicprochat_core::opaque_auth::OpaqueSuite;
use quicprochat_proto::method_ids;
use quicprochat_rpc::error::RpcStatus;
use quicprochat_rpc::method::{HandlerResult, MethodRegistry, RequestContext};
use tokio::sync::Notify;
use crate::audit::AuditLogger;
use crate::auth::{AuthConfig, PendingLogin, RateEntry, SessionInfo};
use crate::hooks::ServerHooks;
use crate::storage::Store;
pub mod account;
pub mod auth;
pub mod blob;
pub mod channel;
pub mod delivery;
pub mod device;
pub mod federation;
pub mod group;
pub mod keys;
pub mod moderation;
pub mod p2p;
pub mod recovery;
pub mod user;
/// Shared server state accessible by all v2 RPC handlers.
pub struct ServerState {
pub store: Arc<dyn Store>,
pub waiters: Arc<DashMap<Vec<u8>, Arc<Notify>>>,
pub auth_cfg: Arc<AuthConfig>,
pub opaque_setup: Arc<ServerSetup<OpaqueSuite>>,
pub pending_logins: Arc<DashMap<String, PendingLogin>>,
pub sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
pub rate_limits: Arc<DashMap<Vec<u8>, RateEntry>>,
pub sealed_sender: bool,
pub hooks: Arc<dyn ServerHooks>,
pub signing_key: Arc<quicprochat_core::IdentityKeypair>,
pub kt_log: Arc<std::sync::Mutex<quicprochat_kt::MerkleLog>>,
pub revocation_log: Arc<std::sync::Mutex<quicprochat_kt::RevocationLog>>,
pub data_dir: PathBuf,
pub redact_logs: bool,
/// Structured audit logger for security-relevant events.
pub audit_logger: Arc<dyn AuditLogger>,
/// When true, the server is draining and will reject new work.
/// Health endpoint returns "draining" status so load balancers stop routing.
pub draining: Arc<AtomicBool>,
/// Idempotency dedup: message_id -> (seq, timestamp). TTL-cleaned by cleanup task.
pub seen_message_ids: Arc<DashMap<Vec<u8>, (u64, u64)>>,
/// Banned users: identity_key -> BanRecord.
pub banned_users: Arc<DashMap<Vec<u8>, BanRecord>>,
/// Moderation reports (append-only).
pub moderation_reports: Arc<std::sync::Mutex<Vec<ModerationReport>>>,
/// Unique node identifier for multi-node health reporting.
pub node_id: String,
/// Process start time for uptime calculation.
pub start_time: std::time::Instant,
/// Storage backend name (e.g. "sql", "file").
pub storage_backend: String,
/// Federation client for outbound server-to-server relay. None when federation is disabled.
pub federation_client: Option<Arc<crate::federation::FederationClient>>,
/// This server's domain for federation addressing. Empty when federation is disabled.
pub local_domain: String,
}
/// A ban record for a user.
#[derive(Debug, Clone)]
pub struct BanRecord {
pub reason: String,
pub banned_at: u64,
/// 0 = permanent.
pub expires_at: u64,
}
/// A stored moderation report.
#[derive(Debug, Clone)]
pub struct ModerationReport {
pub id: u64,
pub encrypted_report: Vec<u8>,
pub conversation_id: Vec<u8>,
pub reporter_identity: Vec<u8>,
pub timestamp: u64,
}
/// Validate the session token from the request context and return the
/// authenticated caller's identity key. Returns an Unauthorized HandlerResult
/// on failure.
pub fn require_auth(state: &ServerState, ctx: &RequestContext) -> Result<Vec<u8>, HandlerResult> {
let token = ctx
.session_token
.as_deref()
.or(ctx.identity_key.as_deref())
.unwrap_or(&[]);
if token.is_empty() {
return Err(HandlerResult::err(
RpcStatus::Unauthorized,
"missing session token",
));
}
// Check session store.
if let Some(session) = state.sessions.get(token) {
let now = crate::auth::current_timestamp();
if session.expires_at > now && !session.identity_key.is_empty() {
// Check ban status.
if let Some(ban) = state.banned_users.get(&session.identity_key) {
if ban.expires_at == 0 || ban.expires_at > now {
return Err(HandlerResult::err(
RpcStatus::Forbidden,
"account banned",
));
}
// Ban expired — remove it.
drop(ban);
state.banned_users.remove(&session.identity_key);
}
return Ok(session.identity_key.clone());
}
}
// Fall back to static bearer token (dev mode).
if state.auth_cfg.allow_insecure_identity_from_request {
if let Some(ik) = ctx.identity_key.as_deref() {
if !ik.is_empty() {
return Ok(ik.to_vec());
}
}
}
Err(HandlerResult::err(
RpcStatus::Unauthorized,
"invalid or expired session token",
))
}
/// Map a domain error to an RPC HandlerResult error.
pub fn domain_err(e: crate::domain::types::DomainError) -> HandlerResult {
use crate::domain::types::DomainError;
match &e {
DomainError::InvalidIdentityKey(_)
| DomainError::EmptyPackage
| DomainError::EmptyHybridKey
| DomainError::EmptyUsername
| DomainError::BlobHashLength(_)
| DomainError::BadParams(_) => HandlerResult::err(RpcStatus::BadRequest, &e.to_string()),
DomainError::BlobNotFound | DomainError::DeviceNotFound | DomainError::GroupNotFound => {
HandlerResult::err(RpcStatus::NotFound, &e.to_string())
}
DomainError::PackageTooLarge(_) | DomainError::BlobTooLarge(_) => {
HandlerResult::err(RpcStatus::BadRequest, &e.to_string())
}
DomainError::BlobHashMismatch => {
HandlerResult::err(RpcStatus::BadRequest, &e.to_string())
}
DomainError::DeviceLimit(_) => HandlerResult::err(RpcStatus::Forbidden, &e.to_string()),
DomainError::Io(_) | DomainError::Storage(_) => {
HandlerResult::err(RpcStatus::Internal, &e.to_string())
}
}
}
/// Build the v2 method registry with all handlers registered.
///
/// `default_rpc_timeout` sets the server-wide per-RPC timeout. Individual methods
/// (e.g. blob upload, health) may override this with shorter or longer values.
pub fn build_registry(default_rpc_timeout: std::time::Duration) -> MethodRegistry<ServerState> {
let mut reg = MethodRegistry::new();
reg.set_default_timeout(default_rpc_timeout);
// Auth (100-103)
reg.register(
method_ids::OPAQUE_REGISTER_START,
"OpaqueRegisterStart",
auth::handle_opaque_register_start,
);
reg.register(
method_ids::OPAQUE_REGISTER_FINISH,
"OpaqueRegisterFinish",
auth::handle_opaque_register_finish,
);
reg.register(
method_ids::OPAQUE_LOGIN_START,
"OpaqueLoginStart",
auth::handle_opaque_login_start,
);
reg.register(
method_ids::OPAQUE_LOGIN_FINISH,
"OpaqueLoginFinish",
auth::handle_opaque_login_finish,
);
// Delivery (200-205)
reg.register(method_ids::ENQUEUE, "Enqueue", delivery::handle_enqueue);
reg.register(method_ids::FETCH, "Fetch", delivery::handle_fetch);
reg.register(
method_ids::FETCH_WAIT,
"FetchWait",
delivery::handle_fetch_wait,
);
reg.register(method_ids::PEEK, "Peek", delivery::handle_peek);
reg.register(method_ids::ACK, "Ack", delivery::handle_ack);
reg.register(
method_ids::BATCH_ENQUEUE,
"BatchEnqueue",
delivery::handle_batch_enqueue,
);
// Keys (300-304)
reg.register(
method_ids::UPLOAD_KEY_PACKAGE,
"UploadKeyPackage",
keys::handle_upload_key_package,
);
reg.register(
method_ids::FETCH_KEY_PACKAGE,
"FetchKeyPackage",
keys::handle_fetch_key_package,
);
reg.register(
method_ids::UPLOAD_HYBRID_KEY,
"UploadHybridKey",
keys::handle_upload_hybrid_key,
);
reg.register(
method_ids::FETCH_HYBRID_KEY,
"FetchHybridKey",
keys::handle_fetch_hybrid_key,
);
reg.register(
method_ids::FETCH_HYBRID_KEYS,
"FetchHybridKeys",
keys::handle_fetch_hybrid_keys,
);
// Channel (400)
reg.register(
method_ids::CREATE_CHANNEL,
"CreateChannel",
channel::handle_create_channel,
);
// Group management (410-413)
reg.register(
method_ids::REMOVE_MEMBER,
"RemoveMember",
group::handle_remove_member,
);
reg.register(
method_ids::UPDATE_GROUP_METADATA,
"UpdateGroupMetadata",
group::handle_update_group_metadata,
);
reg.register(
method_ids::LIST_GROUP_MEMBERS,
"ListGroupMembers",
group::handle_list_group_members,
);
reg.register(
method_ids::ROTATE_KEYS,
"RotateKeys",
group::handle_rotate_keys,
);
// User (500-501)
reg.register(
method_ids::RESOLVE_USER,
"ResolveUser",
user::handle_resolve_user,
);
reg.register(
method_ids::RESOLVE_IDENTITY,
"ResolveIdentity",
user::handle_resolve_identity,
);
// Key Transparency (510-520)
reg.register(
method_ids::REVOKE_KEY,
"RevokeKey",
user::handle_revoke_key,
);
reg.register(
method_ids::CHECK_REVOCATION,
"CheckRevocation",
user::handle_check_revocation,
);
reg.register(
method_ids::AUDIT_KEY_TRANSPARENCY,
"AuditKeyTransparency",
user::handle_audit_key_transparency,
);
// Blob (600-601) — longer timeout for file transfers.
reg.register_with_timeout(
method_ids::UPLOAD_BLOB,
"UploadBlob",
std::time::Duration::from_secs(120),
blob::handle_upload_blob,
);
reg.register_with_timeout(
method_ids::DOWNLOAD_BLOB,
"DownloadBlob",
std::time::Duration::from_secs(120),
blob::handle_download_blob,
);
// Device (700-702)
reg.register(
method_ids::REGISTER_DEVICE,
"RegisterDevice",
device::handle_register_device,
);
reg.register(
method_ids::LIST_DEVICES,
"ListDevices",
device::handle_list_devices,
);
reg.register(
method_ids::REVOKE_DEVICE,
"RevokeDevice",
device::handle_revoke_device,
);
// P2P (800-802)
reg.register(
method_ids::PUBLISH_ENDPOINT,
"PublishEndpoint",
p2p::handle_publish_endpoint,
);
reg.register(
method_ids::RESOLVE_ENDPOINT,
"ResolveEndpoint",
p2p::handle_resolve_endpoint,
);
reg.register_with_timeout(
method_ids::HEALTH,
"Health",
std::time::Duration::from_secs(5),
p2p::handle_health,
);
// Federation (900-905)
reg.register(
method_ids::RELAY_ENQUEUE,
"RelayEnqueue",
federation::handle_relay_enqueue,
);
reg.register(
method_ids::RELAY_BATCH_ENQUEUE,
"RelayBatchEnqueue",
federation::handle_relay_batch_enqueue,
);
reg.register(
method_ids::PROXY_FETCH_KEY_PACKAGE,
"ProxyFetchKeyPackage",
federation::handle_proxy_fetch_key_package,
);
reg.register(
method_ids::PROXY_FETCH_HYBRID_KEY,
"ProxyFetchHybridKey",
federation::handle_proxy_fetch_hybrid_key,
);
reg.register(
method_ids::PROXY_RESOLVE_USER,
"ProxyResolveUser",
federation::handle_proxy_resolve_user,
);
reg.register(
method_ids::FEDERATION_HEALTH,
"FederationHealth",
federation::handle_federation_health,
);
// Moderation (420-424)
reg.register(
method_ids::REPORT_MESSAGE,
"ReportMessage",
moderation::handle_report_message,
);
reg.register(
method_ids::BAN_USER,
"BanUser",
moderation::handle_ban_user,
);
reg.register(
method_ids::UNBAN_USER,
"UnbanUser",
moderation::handle_unban_user,
);
reg.register(
method_ids::LIST_REPORTS,
"ListReports",
moderation::handle_list_reports,
);
reg.register(
method_ids::LIST_BANNED,
"ListBanned",
moderation::handle_list_banned,
);
// Recovery (750-752)
reg.register(
method_ids::STORE_RECOVERY_BUNDLE,
"StoreRecoveryBundle",
recovery::handle_store_recovery_bundle,
);
reg.register(
method_ids::FETCH_RECOVERY_BUNDLE,
"FetchRecoveryBundle",
recovery::handle_fetch_recovery_bundle,
);
reg.register(
method_ids::DELETE_RECOVERY_BUNDLE,
"DeleteRecoveryBundle",
recovery::handle_delete_recovery_bundle,
);
// Account (950)
reg.register(
method_ids::DELETE_ACCOUNT,
"DeleteAccount",
account::handle_delete_account,
);
reg
}

View File

@@ -0,0 +1,199 @@
//! Moderation handlers — report, ban, unban, list reports, list banned.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::error::RpcStatus;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use tracing::{info, warn};
use super::{require_auth, BanRecord, ModerationReport, ServerState};
/// Submit an encrypted report. Any authenticated user can report.
pub async fn handle_report_message(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::ReportMessageRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.encrypted_report.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "encrypted_report required");
}
let now = crate::auth::current_timestamp();
let report = {
let mut reports = match state.moderation_reports.lock() {
Ok(r) => r,
Err(e) => {
warn!("moderation_reports lock poisoned: {e}");
return HandlerResult::err(RpcStatus::Internal, "internal error");
}
};
let id = reports.len() as u64;
let report = ModerationReport {
id,
encrypted_report: req.encrypted_report,
conversation_id: req.conversation_id,
reporter_identity: identity_key.clone(),
timestamp: now,
};
reports.push(report.clone());
report
};
info!(
report_id = report.id,
reporter = hex::encode(&identity_key[..4.min(identity_key.len())]),
"moderation report submitted"
);
let proto = v1::ReportMessageResponse { accepted: true };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
/// Ban a user. Requires admin role (currently: any authenticated user for MVP).
pub async fn handle_ban_user(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let admin_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::BanUserRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.identity_key.is_empty() || req.identity_key.len() != 32 {
return HandlerResult::err(RpcStatus::BadRequest, "identity_key must be 32 bytes");
}
let now = crate::auth::current_timestamp();
let expires_at = if req.duration_secs == 0 {
0 // permanent
} else {
now + req.duration_secs
};
let record = BanRecord {
reason: req.reason.clone(),
banned_at: now,
expires_at,
};
state.banned_users.insert(req.identity_key.clone(), record);
info!(
target_key = hex::encode(&req.identity_key[..4]),
admin_key = hex::encode(&admin_key[..4.min(admin_key.len())]),
reason = %req.reason,
duration_secs = req.duration_secs,
"user banned"
);
let proto = v1::BanUserResponse { success: true };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
/// Unban a user. Requires admin role.
pub async fn handle_unban_user(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let admin_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::UnbanUserRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
if req.identity_key.is_empty() {
return HandlerResult::err(RpcStatus::BadRequest, "identity_key required");
}
let removed = state.banned_users.remove(&req.identity_key).is_some();
info!(
target_key = hex::encode(&req.identity_key[..4.min(req.identity_key.len())]),
admin_key = hex::encode(&admin_key[..4.min(admin_key.len())]),
removed,
"user unbanned"
);
let proto = v1::UnbanUserResponse { success: removed };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
/// List moderation reports. Requires admin role.
pub async fn handle_list_reports(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let _admin_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::ListReportsRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let reports = match state.moderation_reports.lock() {
Ok(r) => r,
Err(e) => {
warn!("moderation_reports lock poisoned: {e}");
return HandlerResult::err(RpcStatus::Internal, "internal error");
}
};
let offset = req.offset as usize;
let limit = if req.limit == 0 { 50 } else { req.limit as usize };
let entries: Vec<v1::ReportEntry> = reports
.iter()
.skip(offset)
.take(limit)
.map(|r| v1::ReportEntry {
id: r.id,
encrypted_report: r.encrypted_report.clone(),
conversation_id: r.conversation_id.clone(),
reporter_identity: r.reporter_identity.clone(),
timestamp: r.timestamp,
})
.collect();
let proto = v1::ListReportsResponse { reports: entries };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
/// List banned users.
pub async fn handle_list_banned(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let _admin_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let _req = match v1::ListBannedRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
};
let now = crate::auth::current_timestamp();
let entries: Vec<v1::BannedUserEntry> = state
.banned_users
.iter()
.filter(|entry| entry.expires_at == 0 || entry.expires_at > now)
.map(|entry| v1::BannedUserEntry {
identity_key: entry.key().clone(),
reason: entry.reason.clone(),
banned_at: entry.banned_at,
expires_at: entry.expires_at,
})
.collect();
let proto = v1::ListBannedResponse { users: entries };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}

View File

@@ -0,0 +1,118 @@
//! P2P handlers — publish/resolve endpoints and health check.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::p2p::P2pService;
use crate::domain::types::{CallerAuth, PublishEndpointReq, ResolveEndpointReq};
use super::{domain_err, require_auth, ServerState};
fn caller_auth(identity_key: Vec<u8>) -> CallerAuth {
CallerAuth {
identity_key,
token: Vec::new(),
device_id: None,
}
}
pub async fn handle_publish_endpoint(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::PublishEndpointRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = P2pService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = PublishEndpointReq {
identity_key: req.identity_key,
node_addr: req.node_addr,
};
match svc.publish_endpoint(domain_req, &auth) {
Ok(()) => {
let proto = v1::PublishEndpointResponse {};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_resolve_endpoint(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::ResolveEndpointRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = P2pService {
store: Arc::clone(&state.store),
};
let auth = caller_auth(identity_key);
let domain_req = ResolveEndpointReq {
identity_key: req.identity_key,
};
match svc.resolve_endpoint(domain_req, &auth) {
Ok(resp) => {
let proto = v1::ResolveEndpointResponse {
node_addr: resp.node_addr,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_health(
state: Arc<ServerState>,
_ctx: RequestContext,
) -> HandlerResult {
let status = if state.draining.load(std::sync::atomic::Ordering::Relaxed) {
"draining"
} else {
"ok"
};
let uptime = state.start_time.elapsed().as_secs();
let resp = v1::HealthResponse {
status: status.into(),
node_id: state.node_id.clone(),
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_secs: uptime,
storage_backend: state.storage_backend.clone(),
};
HandlerResult::ok(Bytes::from(resp.encode_to_vec()))
}

View File

@@ -0,0 +1,99 @@
//! Recovery handlers — store/fetch/delete encrypted recovery bundles.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::recovery::RecoveryService;
use super::{domain_err, ServerState};
/// Store an encrypted recovery bundle (no auth required — recovery is pre-login).
pub async fn handle_store_recovery_bundle(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::StoreRecoveryBundleRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = RecoveryService {
store: Arc::clone(&state.store),
};
match svc.store_bundle(&req.token_hash, req.bundle, req.ttl_secs) {
Ok(()) => {
let proto = v1::StoreRecoveryBundleResponse { success: true };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
/// Fetch an encrypted recovery bundle (no auth required — recovery is pre-login).
pub async fn handle_fetch_recovery_bundle(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::FetchRecoveryBundleRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = RecoveryService {
store: Arc::clone(&state.store),
};
match svc.fetch_bundle(&req.token_hash) {
Ok(bundle_opt) => {
let proto = v1::FetchRecoveryBundleResponse {
bundle: bundle_opt.unwrap_or_default(),
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
/// Delete an encrypted recovery bundle (no auth required — caller proves
/// knowledge of the token_hash).
pub async fn handle_delete_recovery_bundle(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let req = match v1::DeleteRecoveryBundleRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = RecoveryService {
store: Arc::clone(&state.store),
};
match svc.delete_bundle(&req.token_hash) {
Ok(deleted) => {
let proto = v1::DeleteRecoveryBundleResponse { success: deleted };
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}

View File

@@ -0,0 +1,213 @@
//! User resolution handlers — username <-> identity key lookups,
//! key revocation, and KT audit.
use std::sync::Arc;
use bytes::Bytes;
use prost::Message;
use quicprochat_proto::qpc::v1;
use quicprochat_rpc::method::{HandlerResult, RequestContext};
use crate::domain::types::{
AuditKeyTransparencyReq, CheckRevocationReq, ResolveIdentityReq, ResolveUserReq, RevokeKeyReq,
};
use crate::domain::users::UserService;
use super::{domain_err, require_auth, ServerState};
fn user_svc(state: &Arc<ServerState>) -> UserService {
UserService {
store: Arc::clone(&state.store),
kt_log: Arc::clone(&state.kt_log),
revocation_log: Arc::clone(&state.revocation_log),
}
}
pub async fn handle_resolve_user(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::ResolveUserRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = user_svc(&state);
let domain_req = ResolveUserReq {
username: req.username,
};
match svc.resolve_user(domain_req) {
Ok(resp) => {
let proto = v1::ResolveUserResponse {
identity_key: resp.identity_key,
inclusion_proof: resp.inclusion_proof,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_resolve_identity(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::ResolveIdentityRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = user_svc(&state);
let domain_req = ResolveIdentityReq {
identity_key: req.identity_key,
};
match svc.resolve_identity(domain_req) {
Ok(resp) => {
let proto = v1::ResolveIdentityResponse {
username: resp.username,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_revoke_key(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::RevokeKeyRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = user_svc(&state);
let domain_req = RevokeKeyReq {
identity_key: req.identity_key,
reason: req.reason,
};
match svc.revoke_key(domain_req) {
Ok(resp) => {
let proto = v1::RevokeKeyResponse {
success: resp.success,
leaf_index: resp.leaf_index,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_check_revocation(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::CheckRevocationRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = user_svc(&state);
let domain_req = CheckRevocationReq {
identity_key: req.identity_key,
};
match svc.check_revocation(domain_req) {
Ok(resp) => {
let proto = v1::CheckRevocationResponse {
revoked: resp.revoked,
reason: resp.reason,
timestamp_ms: resp.timestamp_ms,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}
pub async fn handle_audit_key_transparency(
state: Arc<ServerState>,
ctx: RequestContext,
) -> HandlerResult {
let _identity_key = match require_auth(&state, &ctx) {
Ok(ik) => ik,
Err(e) => return e,
};
let req = match v1::AuditKeyTransparencyRequest::decode(ctx.payload) {
Ok(r) => r,
Err(e) => {
return HandlerResult::err(
quicprochat_rpc::error::RpcStatus::BadRequest,
&format!("decode: {e}"),
)
}
};
let svc = user_svc(&state);
let domain_req = AuditKeyTransparencyReq {
start: req.start,
end: req.end,
};
match svc.audit_key_transparency(domain_req) {
Ok(resp) => {
let proto = v1::AuditKeyTransparencyResponse {
entries: resp
.entries
.into_iter()
.map(|e| v1::LogEntry {
index: e.index,
leaf_hash: e.leaf_hash,
})
.collect(),
tree_size: resp.tree_size,
root: resp.root,
};
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
}
Err(e) => domain_err(e),
}
}