add request signatures and GET/POST requests

This commit is contained in:
sam 2024-01-17 15:51:16 +01:00
parent 0e71e9dc5f
commit 7a694623e5
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
24 changed files with 690 additions and 30 deletions

35
Cargo.lock generated
View file

@ -2,6 +2,16 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "addr"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93b8a41dbe230ad5087cc721f8d41611de654542180586b315d9f4cf6b72bef"
dependencies = [
"psl",
"psl-types",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.21.0" version = "0.21.0"
@ -399,7 +409,9 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"wasm-bindgen",
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
@ -729,8 +741,15 @@ dependencies = [
name = "foxchat" name = "foxchat"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"addr",
"axum", "axum",
"base64",
"chrono",
"eyre", "eyre",
"once_cell",
"rand",
"reqwest",
"rsa",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
@ -1115,6 +1134,7 @@ dependencies = [
"axum", "axum",
"base64", "base64",
"bcrypt", "bcrypt",
"chrono",
"clap", "clap",
"color-eyre", "color-eyre",
"eyre", "eyre",
@ -1614,6 +1634,21 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "psl"
version = "2.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "383703acfc34f7a00724846c14dc5ea4407c59e5aedcbbb18a1c0c1a23fe5013"
dependencies = [
"psl-types",
]
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.35"

View file

@ -1,4 +1,4 @@
database_url = "postgresql://foxchat:password@localhost/foxchat_ident_dev" database_url = "postgresql://foxchat:password@localhost/foxchat_ident_dev"
port = 3000 port = 3000
log_level = "DEBUG" log_level = "DEBUG"
insecure_requests = true domain = "chat.foxchat.localhost"

View file

@ -6,8 +6,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
addr = "0.15.6"
axum = "0.7.4" axum = "0.7.4"
base64 = "0.21.7"
chrono = "0.4.31"
eyre = "0.6.11" eyre = "0.6.11"
once_cell = "1.19.0"
rand = "0.8.5"
reqwest = "0.11.23"
rsa = { version = "0.9.6", features = ["sha2"] }
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111" serde_json = "1.0.111"
sqlx = "0.7.3" sqlx = "0.7.3"

View file

@ -1,7 +1,13 @@
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug, Copy, Clone)] #[derive(Error, Debug, Copy, Clone)]
pub enum QueryError { pub enum FoxError {
#[error("object not found")] #[error("object not found")]
NotFound, NotFound,
#[error("date for signature out of range")]
SignatureDateOutOfRange(&'static str),
#[error("non-200 response to federation request")]
ResponseNotOk,
#[error("server is invalid")]
InvalidServer,
} }

4
foxchat/src/fed/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod request;
pub mod signature;
pub use request::{get, post};

100
foxchat/src/fed/request.rs Normal file
View file

@ -0,0 +1,100 @@
use eyre::Result;
use once_cell::sync::Lazy;
use reqwest::{Response, StatusCode};
use rsa::RsaPrivateKey;
use serde::{de::DeserializeOwned, Serialize};
use crate::{
signature::{build_signature, format_date},
FoxError,
};
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
reqwest::Client::builder()
.user_agent(APP_USER_AGENT)
.build()
.unwrap()
});
pub fn is_valid_domain(domain: &str) -> bool {
addr::parse_domain_name(domain).is_ok()
}
pub async fn get<R: DeserializeOwned>(
private_key: &RsaPrivateKey,
self_domain: String,
host: String,
path: String,
user_id: Option<String>,
) -> Result<R> {
let (signature, date) = build_signature(
private_key,
host.clone(),
path.clone(),
None,
user_id.clone(),
);
let mut req = CLIENT
.get(format!("https://{host}{path}"))
.header("Date", format_date(date))
.header("X-Foxchat-Signature", signature)
.header("X-Foxchat-Server", self_domain);
req = if let Some(id) = user_id {
req.header("X-Foxchat-User", id)
} else {
req
};
let resp = req.send().await?;
handle_response(resp).await
}
pub async fn post<T: Serialize, R: DeserializeOwned>(
private_key: &RsaPrivateKey,
self_domain: String,
host: String,
path: String,
user_id: Option<String>,
body: &T,
) -> Result<R> {
let body = serde_json::to_string(body)?;
let (signature, date) = build_signature(
private_key,
host.clone(),
path.clone(),
Some(body.len()),
user_id.clone(),
);
let mut req = CLIENT
.post(format!("https://{host}{path}"))
.header("Date", format_date(date))
.header("X-Foxchat-Signature", signature)
.header("X-Foxchat-Server", self_domain)
.header("Content-Type", "application/json; charset=utf-8")
.header("Content-Length", body.len().to_string())
.body(body);
req = if let Some(id) = user_id {
req.header("X-Foxchat-User", id)
} else {
req
};
let resp = req.send().await?;
handle_response(resp).await
}
async fn handle_response<R: DeserializeOwned>(resp: Response) -> Result<R> {
if resp.status() != StatusCode::OK {
return Err(FoxError::ResponseNotOk.into());
}
let parsed = resp.json::<R>().await?;
Ok(parsed)
}

View file

@ -0,0 +1,78 @@
use base64::prelude::{Engine, BASE64_URL_SAFE};
use chrono::{DateTime, Utc, Duration};
use eyre::Result;
use rsa::pkcs1v15::{SigningKey, VerifyingKey, Signature};
use rsa::sha2::Sha256;
use rsa::signature::{RandomizedSigner, SignatureEncoding, Verifier};
use rsa::{RsaPrivateKey, RsaPublicKey};
use crate::FoxError;
pub fn build_signature(
private_key: &RsaPrivateKey,
host: String,
request_path: String,
content_length: Option<usize>,
user_id: Option<String>,
) -> (String, DateTime<Utc>) {
let mut rng = rand::thread_rng();
let signing_key = SigningKey::<Sha256>::new(private_key.clone());
let time = Utc::now();
let plaintext = plaintext_string(time, host, request_path, content_length, user_id);
let signature = signing_key.sign_with_rng(&mut rng, plaintext.as_bytes());
let str = BASE64_URL_SAFE.encode(signature.to_bytes());
(str, time)
}
fn plaintext_string(
time: DateTime<Utc>,
host: String,
request_path: String,
content_length: Option<usize>,
user_id: Option<String>,
) -> String {
let raw_time = format_date(time);
let raw_content_length = content_length
.map(|i| i.to_string())
.unwrap_or("".to_owned());
let raw_user_id = user_id.unwrap_or("".to_owned());
format!(
"{}:{}:{}:{}:{}",
raw_time, host, request_path, raw_content_length, raw_user_id
)
}
pub fn format_date(time: DateTime<Utc>) -> String {
time.format("%a, %d %b %Y %T GMT").to_string()
}
pub fn verify_signature(
public_key: &RsaPublicKey,
encoded_signature: String,
time: DateTime<Utc>, // from Date header
host: String, // from Host header, verify that it's actually your host
request_path: String, // from router
content_length: Option<usize>, // from Content-Length header
user_id: Option<String>, // from X-Foxchat-User header
) -> Result<bool> {
let verifying_key = VerifyingKey::<Sha256>::new(public_key.clone());
let now = Utc::now();
if (now - Duration::minutes(1)) < time {
return Err(FoxError::SignatureDateOutOfRange("request was made too long ago").into());
} else if (now + Duration::minutes(1)) > time {
return Err(FoxError::SignatureDateOutOfRange("request was made in the future").into());
}
let plaintext = plaintext_string(time, host, request_path, content_length, user_id);
let hash = &BASE64_URL_SAFE.decode(encoded_signature)?;
let signature = Signature::try_from(hash.as_ref())?;
verifying_key.verify(plaintext.as_bytes(), &signature)?;
Ok(true)
}

View file

@ -8,7 +8,7 @@ use serde::Serialize;
use serde_json::json; use serde_json::json;
use tracing::error; use tracing::error;
use crate::QueryError; use crate::FoxError;
pub struct ApiError { pub struct ApiError {
status: StatusCode, status: StatusCode,
@ -35,6 +35,7 @@ impl IntoResponse for ApiError {
pub enum ErrorCode { pub enum ErrorCode {
InternalServerError, InternalServerError,
ObjectNotFound, ObjectNotFound,
InvalidServer,
} }
impl From<sqlx::Error> for ApiError { impl From<sqlx::Error> for ApiError {
@ -53,7 +54,7 @@ impl From<sqlx::Error> for ApiError {
impl From<Report> for ApiError { impl From<Report> for ApiError {
fn from(err: Report) -> Self { fn from(err: Report) -> Self {
match err.downcast_ref::<QueryError>() { match err.downcast_ref::<FoxError>() {
Some(e) => return (*e).into(), Some(e) => return (*e).into(),
None => {} None => {}
}; };
@ -71,14 +72,24 @@ impl From<Report> for ApiError {
} }
} }
impl From<QueryError> for ApiError { impl From<FoxError> for ApiError {
fn from(err: QueryError) -> Self { fn from(err: FoxError) -> Self {
match err { match err {
QueryError::NotFound => ApiError { FoxError::NotFound => ApiError {
status: StatusCode::NOT_FOUND, status: StatusCode::NOT_FOUND,
code: ErrorCode::ObjectNotFound, code: ErrorCode::ObjectNotFound,
message: "Object not found".into(), message: "Object not found".into(),
}, },
FoxError::InvalidServer => ApiError {
status: StatusCode::BAD_REQUEST,
code: ErrorCode::InvalidServer,
message: "Invalid domain or server".into(),
},
_ => ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: ErrorCode::InternalServerError,
message: "Internal server error".into(),
},
} }
} }
} }

View file

@ -1,5 +1,7 @@
pub mod error; pub mod error;
pub mod http; pub mod http;
pub mod s2s; pub mod s2s;
pub mod fed;
pub use error::QueryError; pub use error::FoxError;
pub use fed::signature;

View file

@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM instance WHERE id = 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "public_key",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "private_key",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false
]
},
"hash": "29b5c01c2511f63be1ae213d8b1999becab8ab1c5f6da712db7f759f30f2bca8"
}

View file

@ -0,0 +1,62 @@
{
"db_name": "PostgreSQL",
"query": "select\n a.id, a.username, a.email, a.password, a.role as \"role: Role\", a.avatar\n from accounts a\n join tokens t on t.account_id = a.id\n where t.account_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "password",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "role: Role",
"type_info": {
"Custom": {
"name": "account_role",
"kind": {
"Enum": [
"user",
"admin"
]
}
}
}
},
{
"ordinal": 5,
"name": "avatar",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "3402be9906900efd5c0e9f7ec10e9303a3068b728142dc90bb66389069608e28"
}

View file

@ -0,0 +1,31 @@
{
"db_name": "PostgreSQL",
"query": "insert into accounts (id, username, email, password) values ($1, $2, $3, $4) returning id, username",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "665e75d8304aab441acb50e78793092e5c603089672bceff3a4fa17dd624d933"
}

View file

@ -0,0 +1,62 @@
{
"db_name": "PostgreSQL",
"query": "select\n id, username, email, password, role as \"role: Role\", avatar\n from accounts where username = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "password",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "role: Role",
"type_info": {
"Custom": {
"name": "account_role",
"kind": {
"Enum": [
"user",
"admin"
]
}
}
}
},
{
"ordinal": 5,
"name": "avatar",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "acb98db00095d510a139a8a4fd61edcd04e1570c96869993b6b20d16ad71da3c"
}

View file

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "select exists(select * from instance)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "ad1116f3443ba2c3093c6c7605508d7c7ce717e42f970ea30fd7e828c8c9ef06"
}

View file

@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "insert into chat_instances\n (id, domain, base_url, public_key) values ($1, $2, $3, $4)\n returning id, domain, base_url, public_key,\n status as \"status: InstanceStatus\", reason",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "domain",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "base_url",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "public_key",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "status: InstanceStatus",
"type_info": {
"Custom": {
"name": "instance_status",
"kind": {
"Enum": [
"active",
"suspended"
]
}
}
}
},
{
"ordinal": 5,
"name": "reason",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "bdf6cfaad2176b76f5092898e796580047c45469fc9a867a4cc93a49343a5333"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "insert into tokens (token, account_id) values ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "d125d3c97a59ac5c4ba04eb02d9a8a35d5dc84b0860ba22a8c105d457cb8b4e6"
}

View file

@ -0,0 +1,62 @@
{
"db_name": "PostgreSQL",
"query": "select id, domain, base_url, public_key,\n status as \"status: InstanceStatus\", reason\n from chat_instances where domain = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "domain",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "base_url",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "public_key",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "status: InstanceStatus",
"type_info": {
"Custom": {
"name": "instance_status",
"kind": {
"Enum": [
"active",
"suspended"
]
}
}
}
},
{
"ordinal": 5,
"name": "reason",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
true
]
},
"hash": "dab570575b63e52a4bee944f139121074794c172310c88bdcdd8bef083df9431"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "insert into instance (public_key, private_key) values ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "e8680ec00d42254b8fc8240636d8e91cbd34ef31391418828f4a0bb4e9e2a4ce"
}

View file

@ -26,3 +26,4 @@ bcrypt = "0.15.0"
base64 = "0.21.7" base64 = "0.21.7"
sha256 = "1.5.0" sha256 = "1.5.0"
reqwest = { version = "0.11.23", features = ["json", "gzip", "brotli", "multipart"] } reqwest = { version = "0.11.23", features = ["json", "gzip", "brotli", "multipart"] }
chrono = "0.4.31"

View file

@ -10,11 +10,9 @@ pub const CONFIG_FILE: &str = "config.identity.toml";
pub struct Config { pub struct Config {
pub database_url: String, pub database_url: String,
pub port: u16, pub port: u16,
pub domain: String,
pub auto_migrate: Option<bool>, pub auto_migrate: Option<bool>,
pub log_level: Option<String>, pub log_level: Option<String>,
/// Whether to try HTTP if a server is not served over HTTPS.
/// This should only be set to `true` in development.
pub insecure_requests: Option<bool>,
} }
impl Config { impl Config {
@ -43,21 +41,16 @@ impl Config {
_ => None _ => None
} }
} }
/// If true, the server will try to identify remote servers over HTTP if they are not available over HTTPS
pub fn should_try_insecure(&self) -> bool {
self.insecure_requests.unwrap_or(false)
}
} }
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { Config {
database_url: env::var("DATABASE_URL").unwrap_or("".into()), database_url: env::var("DATABASE_URL").unwrap_or("".into()),
domain: "".into(),
port: 3000, port: 3000,
auto_migrate: None, auto_migrate: None,
log_level: None, log_level: None,
insecure_requests: None,
} }
} }
} }

View file

@ -1,6 +1,6 @@
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
use eyre::{Context, Report, Result}; use eyre::{Context, Report, Result};
use foxchat::QueryError; use foxchat::FoxError;
use rand::RngCore; use rand::RngCore;
use sqlx::{PgExecutor, Pool, Postgres}; use sqlx::{PgExecutor, Pool, Postgres};
@ -22,16 +22,16 @@ pub async fn get_user_by_username_and_password(
.await .await
.map_err(|e| -> Report { .map_err(|e| -> Report {
match e { match e {
sqlx::Error::RowNotFound => QueryError::NotFound.into(), sqlx::Error::RowNotFound => FoxError::NotFound.into(),
_ => e.into(), _ => e.into(),
} }
})?; })?;
if bcrypt::verify(password, &account.password).map_err(|_| QueryError::NotFound)? { if bcrypt::verify(password, &account.password).map_err(|_| FoxError::NotFound)? {
return Ok(account); return Ok(account);
} }
Err(QueryError::NotFound.into()) Err(FoxError::NotFound.into())
} }
pub async fn check_token(pool: &Pool<Postgres>, token: String) -> Result<Account> { pub async fn check_token(pool: &Pool<Postgres>, token: String) -> Result<Account> {
@ -51,7 +51,7 @@ pub async fn check_token(pool: &Pool<Postgres>, token: String) -> Result<Account
match account { match account {
Some(a) => Ok(a), Some(a) => Ok(a),
None => Err(QueryError::NotFound.into()), None => Err(FoxError::NotFound.into()),
} }
} }

View file

@ -15,6 +15,7 @@ pub fn new(pool: Pool<Postgres>, config: Config) -> Router {
let app = Router::new() let app = Router::new()
.merge(account::router()) .merge(account::router())
.route("/_fox/ident/node", get(node::get_node)) .route("/_fox/ident/node", get(node::get_node))
.route("/_fox/ident/node/:domain", get(node::get_chat_node))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())
.layer(Extension(app_state)); .layer(Extension(app_state));

View file

@ -1,14 +1,16 @@
use std::sync::Arc; use std::sync::Arc;
use axum::{Extension, Json}; use axum::{Extension, Json, extract::Path};
use eyre::{Result, Context}; use eyre::{Context, Result};
use foxchat::http::ApiError; use foxchat::http::ApiError;
use rsa::pkcs1::EncodeRsaPublicKey; use rsa::pkcs1::EncodeRsaPublicKey;
use serde::Serialize; use serde::Serialize;
use crate::{app_state::AppState, model::instance::Instance}; use crate::{app_state::AppState, model::{instance::Instance, chat_instance::ChatInstance}};
pub async fn get_node(Extension(state): Extension<Arc<AppState>>) -> Result<Json<NodeResponse>, ApiError> { pub async fn get_node(
Extension(state): Extension<Arc<AppState>>,
) -> Result<Json<NodeResponse>, ApiError> {
let instance = Instance::get(&state.pool).await?; let instance = Instance::get(&state.pool).await?;
let public_key = instance let public_key = instance
@ -29,3 +31,12 @@ pub struct NodeResponse {
pub software: &'static str, pub software: &'static str,
pub public_key: String, pub public_key: String,
} }
pub async fn get_chat_node(
Extension(state): Extension<Arc<AppState>>,
Path(domain): Path<String>,
) -> Result<Json<ChatInstance>, ApiError> {
let instance = ChatInstance::get(state, domain).await?;
Ok(Json(instance))
}

View file

@ -1,10 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use eyre::Result; use eyre::Result;
use foxchat::{fed::{self, request::is_valid_domain}, FoxError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::app_state::AppState; use crate::{app_state::AppState, model::instance::Instance};
#[derive(Serialize)]
pub struct ChatInstance { pub struct ChatInstance {
pub id: String, pub id: String,
pub domain: String, pub domain: String,
@ -23,6 +26,10 @@ pub enum InstanceStatus {
impl ChatInstance { impl ChatInstance {
pub async fn get(state: Arc<AppState>, domain: String) -> Result<Self> { pub async fn get(state: Arc<AppState>, domain: String) -> Result<Self> {
if !is_valid_domain(&domain) {
return Err(FoxError::InvalidServer.into());
}
if let Some(instance) = sqlx::query_as!( if let Some(instance) = sqlx::query_as!(
Self, Self,
r#"select id, domain, base_url, public_key, r#"select id, domain, base_url, public_key,
@ -36,9 +43,49 @@ impl ChatInstance {
return Ok(instance); return Ok(instance);
} }
// TODO: identify server process let current_instance = Instance::get(&state.pool).await?;
// only try HTTP if `state.config.should_try_insecure()`
todo!() let resp: HelloResponse = fed::post(
&current_instance.private_key,
state.config.domain.clone(),
domain.clone(),
"/_fox/chat/hello".into(),
None,
&HelloRequest {
host: state.config.domain.clone(),
},
)
.await?;
if resp.host != domain.clone() {
return Err(FoxError::InvalidServer.into());
}
let instance = sqlx::query_as!(
Self,
r#"insert into chat_instances
(id, domain, base_url, public_key) values ($1, $2, $3, $4)
returning id, domain, base_url, public_key,
status as "status: InstanceStatus", reason"#,
Ulid::new().to_string(),
domain.clone(),
format!("https://{domain}"),
resp.public_key
)
.fetch_one(&state.pool)
.await?;
Ok(instance)
} }
} }
#[derive(Serialize, Debug)]
struct HelloRequest {
pub host: String,
}
#[derive(Deserialize, Debug)]
struct HelloResponse {
pub public_key: String,
pub host: String,
}