add initial endpoints

This commit is contained in:
sam 2024-01-16 17:16:39 +01:00
parent 97d089c284
commit 3025f1380c
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
19 changed files with 300 additions and 9 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
debug/
target/
.idea
.vscode
.env

67
Cargo.lock generated
View file

@ -239,6 +239,19 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bcrypt"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3"
dependencies = [
"base64",
"blowfish",
"getrandom",
"subtle",
"zeroize",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -263,6 +276,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher",
]
[[package]]
name = "bumpalo"
version = "3.14.0"
@ -312,6 +335,16 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.4.16"
@ -575,7 +608,12 @@ dependencies = [
name = "foxchat"
version = "0.1.0"
dependencies = [
"axum",
"eyre",
"serde",
"serde_json",
"sqlx",
"tracing",
"uuid",
]
@ -875,6 +913,7 @@ name = "identity"
version = "0.1.0"
dependencies = [
"axum",
"bcrypt",
"clap",
"color-eyre",
"dotenvy",
@ -887,6 +926,7 @@ dependencies = [
"sqlx",
"tokio",
"toml",
"tower-http",
"tracing",
"tracing-subscriber",
"ulid",
@ -919,6 +959,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "itertools"
version = "0.12.0"
@ -1363,6 +1412,7 @@ dependencies = [
"pkcs8",
"rand_core",
"serde",
"sha2",
"signature",
"spki",
"subtle",
@ -2042,6 +2092,23 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e"
dependencies = [
"bitflags 2.4.1",
"bytes",
"http",
"http-body",
"http-body-util",
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"

View file

@ -5,6 +5,3 @@ members = [
"chat"
]
resolver = "2"
[profile.dev.package.num-bigint-dig]
opt-level = 3

View file

@ -6,5 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.7.4"
eyre = "0.6.11"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
sqlx = "0.7.3"
tracing = "0.1.40"
uuid = { version = "1.6.1", features = ["v7"] }

3
foxchat/src/http/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod response;
pub use response::{ApiError, ErrorCode};

View file

@ -0,0 +1,64 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use eyre::Report;
use serde::Serialize;
use serde_json::json;
use tracing::error;
pub struct ApiError {
status: StatusCode,
code: ErrorCode,
message: String,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(
self.status,
Json(json!({
"status": self.status.as_u16(),
"code": self.code,
"message": self.message,
})),
)
.into_response()
}
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
InternalServerError,
}
impl From<sqlx::Error> for ApiError {
fn from(err: sqlx::Error) -> Self {
ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: ErrorCode::InternalServerError,
message: if cfg!(debug_assertions) {
format!("Database error: {}", err)
} else {
"Database error".into()
},
}
}
}
impl From<Report> for ApiError {
fn from(err: Report) -> Self {
error!("Error in handler: {}", err);
ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: ErrorCode::InternalServerError,
message: if cfg!(debug_assertions) {
format!("Internal server error: {}", err)
} else {
"Internal server error".into()
},
}
}
}

View file

@ -1 +1,2 @@
pub mod s2s;
pub mod http;

View file

@ -17,9 +17,11 @@ serde_json = "1.0.111"
ulid = { version = "1.1.0", features = ["serde"] }
eyre = "0.6.11"
color-eyre = "0.6.2"
rsa = { version = "0.9.6", features = ["serde"] }
rsa = { version = "0.9.6", features = ["serde", "sha2"] }
rand = "0.8.5"
toml = "0.8.8"
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = "0.3.18"
tracing = "0.1.40"
tower-http = { version = "0.5.1", features = ["trace"] }
bcrypt = "0.15.0"

View file

@ -17,6 +17,7 @@ create type instance_status as enum ('active', 'suspended');
create table chat_instances (
id text primary key,
domain text not null unique,
base_url text not null,
public_key text not null,
status instance_status not null default 'active',
reason text

View file

@ -0,0 +1,8 @@
use sqlx::{Pool, Postgres};
use crate::config::Config;
pub struct AppState {
pub pool: Pool<Postgres>,
pub config: Config,
}

View file

@ -1,6 +1,5 @@
use eyre::{OptionExt, Result};
use rsa::pkcs1::LineEnding;
use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey};
use rsa::pkcs1::{LineEnding, EncodeRsaPublicKey, EncodeRsaPrivateKey};
use rsa::{RsaPrivateKey, RsaPublicKey};
use sqlx::postgres::PgPoolOptions;
use sqlx::{Pool, Postgres};
@ -34,13 +33,13 @@ pub async fn init_instance(pool: &Pool<Postgres>) -> Result<()> {
let priv_key = RsaPrivateKey::new(&mut rng, PRIVATE_KEY_BITS)?;
let pub_key = RsaPublicKey::from(&priv_key);
let priv_key_string = priv_key.to_pkcs8_pem(LineEnding::LF)?;
let pub_key_string = pub_key.to_public_key_pem(LineEnding::LF)?;
let priv_key_string = priv_key.to_pkcs1_pem(LineEnding::LF)?;
let pub_key_string = pub_key.to_pkcs1_pem(LineEnding::LF)?;
sqlx::query!(
"insert into instance (public_key, private_key) values ($1, $2)",
pub_key_string,
priv_key_string.to_string(),
pub_key_string
)
.execute(&mut *tx)
.await?;

View file

@ -0,0 +1,43 @@
use std::sync::Arc;
use axum::{Extension, Json};
use eyre::{Context, Result};
use foxchat::http::ApiError;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::app_state::AppState;
const PASSWORD_COST: u32 = 12;
pub async fn create_user(
Extension(state): Extension<Arc<AppState>>,
Json(data): Json<CreateUserRequest>,
) -> Result<Json<CreateUserResponse>, ApiError> {
let password_hash =
bcrypt::hash(data.password, PASSWORD_COST).wrap_err("failed to hash password")?;
let account = sqlx::query!(
"insert into accounts (id, username, email, password) values ($1, $2, $3, $4) returning id, username",
Ulid::new().to_string(), data.username, data.email, password_hash)
.fetch_one(&state.pool)
.await?;
Ok(Json(CreateUserResponse {
id: account.id,
username: account.username,
}))
}
#[derive(Deserialize)]
pub struct CreateUserRequest {
pub username: String,
pub email: String,
pub password: String,
}
#[derive(Serialize)]
pub struct CreateUserResponse {
pub id: String,
pub username: String,
}

View file

@ -0,0 +1,7 @@
mod create_user;
use axum::{routing::post, Router};
pub fn router() -> Router {
Router::new().route("/_fox/ident/accounts", post(create_user::create_user))
}

22
identity/src/http/mod.rs Normal file
View file

@ -0,0 +1,22 @@
mod node;
mod account;
use std::sync::Arc;
use axum::{routing::get, Extension, Router};
use sqlx::{Pool, Postgres};
use tower_http::trace::TraceLayer;
use crate::{app_state::AppState, config::Config};
pub fn new(pool: Pool<Postgres>, config: Config) -> Router {
let app_state = Arc::new(AppState { pool, config });
let app = Router::new()
.merge(account::router())
.route("/_fox/ident/node", get(node::get_node))
.layer(TraceLayer::new_for_http())
.layer(Extension(app_state));
return app;
}

31
identity/src/http/node.rs Normal file
View file

@ -0,0 +1,31 @@
use std::sync::Arc;
use axum::{Extension, Json};
use eyre::{Result, Context};
use foxchat::http::ApiError;
use rsa::pkcs1::EncodeRsaPublicKey;
use serde::Serialize;
use crate::{app_state::AppState, model::instance::Instance};
pub async fn get_node(Extension(state): Extension<Arc<AppState>>) -> Result<Json<NodeResponse>, ApiError> {
let instance = Instance::get(&state.pool).await?;
let public_key = instance
.public_key
.to_pkcs1_pem(rsa::pkcs8::LineEnding::LF)
.wrap_err("serializing public key")?;
Ok(Json(NodeResponse {
software: NODE_SOFTWARE,
public_key,
}))
}
const NODE_SOFTWARE: &str = "foxchat_ident";
#[derive(Serialize)]
pub struct NodeResponse {
pub software: &'static str,
pub public_key: String,
}

View file

@ -1,10 +1,15 @@
mod app_state;
mod config;
mod db;
mod http;
mod model;
use std::net::{Ipv4Addr, SocketAddrV4};
use crate::config::Config;
use clap::{Parser, Subcommand};
use color_eyre::eyre::Result;
use tokio::net::TcpListener;
use tracing::info;
#[derive(Debug, Parser)]
@ -59,5 +64,12 @@ async fn main_web(config: Config) -> Result<()> {
db::init_instance(&pool).await?;
info!("Initialized instance data!");
let port = config.port;
let app = http::new(pool, config);
let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port)).await?;
axum::serve(listener, app).await?;
Ok(())
}

View file

@ -4,6 +4,8 @@ use ulid::Ulid;
pub struct ChatInstance {
pub id: Ulid,
pub domain: String,
pub base_url: String,
pub public_key: String,
pub status: InstanceStatus,
pub reason: Option<String>,
}

View file

@ -0,0 +1,25 @@
use eyre::Result;
use sqlx::{Pool, Postgres};
use rsa::{RsaPrivateKey, RsaPublicKey, pkcs1::{DecodeRsaPublicKey, DecodeRsaPrivateKey}};
#[derive(Debug, Clone)]
pub struct Instance {
pub public_key: RsaPublicKey,
pub private_key: RsaPrivateKey,
}
impl Instance {
pub async fn get(pool: &Pool<Postgres>) -> Result<Self> {
let instance = sqlx::query!("SELECT * FROM instance WHERE id = 1")
.fetch_one(pool)
.await?;
let public_key = RsaPublicKey::from_pkcs1_pem(&instance.public_key)?;
let private_key = RsaPrivateKey::from_pkcs1_pem(&instance.private_key)?;
Ok(Self {
public_key,
private_key,
})
}
}

View file

@ -1,2 +1,3 @@
pub mod account;
pub mod chat_instance;
pub mod instance;