From fa14844d0df24e667d91f2aeb0ac7462363810c5 Mon Sep 17 00:00:00 2001 From: oxalica Date: Wed, 25 Sep 2024 12:34:31 -0400 Subject: [PATCH] feat(webapi): expose server metadata --- blah-types/src/server.rs | 29 +++++++++++++++++++++++++++ blahd/src/lib.rs | 43 +++++++++++++++++++++++++++++++++++++--- blahd/tests/webapi.rs | 19 +++++++++++++++++- flake.nix | 1 + 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/blah-types/src/server.rs b/blah-types/src/server.rs index 13f5ac7..624e92b 100644 --- a/blah-types/src/server.rs +++ b/blah-types/src/server.rs @@ -1,6 +1,7 @@ //! Data types and constants for Chat Server interaction. use serde::{Deserialize, Serialize}; +use url::Url; use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId}; use crate::PubKey; @@ -8,6 +9,34 @@ use crate::PubKey; pub const X_BLAH_NONCE: &str = "x-blah-nonce"; pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty"; +/// Metadata about the version and capabilities of a Chat Server. +/// +/// It should be relatively stable and do not change very often. +/// It may contains extra fields and clients should ignore them for future compatibility. +/// Chat Servers can also include any custom fields here as long they have a `_` prefix. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerMetadata { + /// A server-defined version string indicating its implementation name and the version. + /// + /// It is expected to be in form `/` but not mandatory. + pub server: String, + + /// The URL to the source code of the Chat Server. + /// + /// It is expected to be a public accessible maybe-compressed tarball link without + /// access control. + pub src_url: Option, + + /// The server capabilities set. + pub capabilities: ServerCapabilities, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ServerCapabilities { + /// Whether registration is open to public. + pub allow_public_register: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoomMetadata { /// Room id. diff --git a/blahd/src/lib.rs b/blahd/src/lib.rs index 62174bd..56780ff 100644 --- a/blahd/src/lib.rs +++ b/blahd/src/lib.rs @@ -3,10 +3,11 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; +use axum::body::Bytes; use axum::extract::{ws, OriginalUri}; use axum::extract::{Path, Query, State, WebSocketUpgrade}; -use axum::http::{header, HeaderMap, HeaderName, StatusCode}; -use axum::response::Response; +use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::{Json, Router}; use axum_extra::extract::WithRejection as R; @@ -15,7 +16,9 @@ use blah_types::msg::{ MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission, SignedChatMsgWithId, UserRegisterPayload, }; -use blah_types::server::{RoomMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; +use blah_types::server::{ + RoomMetadata, ServerCapabilities, ServerMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE, +}; use blah_types::{get_timestamp, Id, Signed, UserKey}; use database::{Transaction, TransactionOps}; use feed::FeedData; @@ -41,6 +44,10 @@ mod utils; pub use database::{Config as DatabaseConfig, Database}; pub use middleware::ApiError; +/// The server name and version, for metadata report and user agent. +pub(crate) const SERVER_AND_VERSION: &str = concat!("blahd/", env!("CARGO_PKG_VERSION")); +const SERVER_SRC_URL: Option<&str> = option_env!("CFG_SRC_URL"); + #[serde_inline_default] #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] @@ -85,11 +92,27 @@ pub struct AppState { event: event::State, register: register::State, + server_metadata: Bytes, config: ServerConfig, } impl AppState { pub fn new(db: Database, config: ServerConfig) -> Self { + let meta = ServerMetadata { + server: SERVER_AND_VERSION.into(), + // TODO: Validate this at compile time? + src_url: SERVER_SRC_URL.map(|url| { + url.parse() + .expect("BLAHD_SRC_URL from compile time should be valid") + }), + capabilities: ServerCapabilities { + allow_public_register: config.register.enable_public, + }, + }; + let server_metadata = serde_json::to_string(&meta) + .expect("serialization cannot fail") + .into(); + Self { db, used_nonces: Mutex::new(ExpiringSet::new(Duration::from_secs( @@ -98,6 +121,7 @@ impl AppState { event: event::State::default(), register: register::State::new(config.register.clone()), + server_metadata, config, } } @@ -121,6 +145,7 @@ type ArcState = State>; pub fn router(st: Arc) -> Router { let router = Router::new() + .route("/server", get(handle_server_metadata)) .route("/ws", get(handle_ws)) .route("/user/me", get(user_get).post(user_register)) .route("/room", get(room_list)) @@ -150,6 +175,18 @@ pub fn router(st: Arc) -> Router { type RE = R; +async fn handle_server_metadata(State(st): ArcState) -> Response { + // TODO: If-None-Match. + ( + [( + header::CONTENT_TYPE, + const { HeaderValue::from_static("application/json") }, + )], + st.server_metadata.clone(), + ) + .into_response() +} + async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response { ws.on_upgrade(move |mut socket| async move { match event::handle_ws(st, &mut socket).await { diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs index 1f40b9b..166d157 100644 --- a/blahd/tests/webapi.rs +++ b/blahd/tests/webapi.rs @@ -15,7 +15,7 @@ use blah_types::msg::{ MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, UserRegisterPayload, WithMsgId, }; -use blah_types::server::{RoomMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; +use blah_types::server::{RoomMetadata, ServerMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; use blah_types::{Id, SignExt, Signed, UserKey}; use blahd::{AppState, Database, RoomList, RoomMsgs}; use ed25519_dalek::SigningKey; @@ -245,6 +245,10 @@ impl Server { msg.sign_msg(&user.pubkeys.id_key, &user.act_priv).unwrap() } + async fn get_metadata(&self) -> Result { + self.get::("/server", None).await + } + fn create_room( &self, user: &User, @@ -450,6 +454,15 @@ async fn smoke(server: Server) { assert_eq!(got, exp); } +#[rstest] +#[tokio::test] +#[expect(clippy::print_stdout, reason = "allowed in tests for debugging")] +async fn server_metadata(server: Server) { + let meta = server.get_metadata().await.unwrap(); + println!("{meta:#?}"); + assert!(meta.server.starts_with("blahd/")); +} + fn auth(user: &User) -> String { let msg = AuthPayload {} .sign_msg(&user.pubkeys.id_key, &user.act_priv) @@ -1262,6 +1275,10 @@ unsafe_allow_id_url_single_label = {allow_single_label} }; let server = server_with(Database::open(&db_config).unwrap(), &config); + // Report in capabilities. + let meta = server.get_metadata().await.unwrap(); + assert_eq!(meta.capabilities.allow_public_register, enabled); + // Returns challenge headers only if registration is enabled. let hdrs = server.get_me(Some(&CAROL)).await.unwrap_err(); if enabled { diff --git a/flake.nix b/flake.nix index 4f93d5a..0058d3e 100644 --- a/flake.nix +++ b/flake.nix @@ -85,6 +85,7 @@ rec { blahd = (pkgs.callPackage mkPkg { }).overrideAttrs { # Only set this for the main derivation, not for deps. CFG_RELEASE = "git-${rev}"; + CFG_SRC_URL = "https://github.com/Blah-IM/blahrs/archive/${rev}.tar.gz"; }; } );