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

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"
sha256 = "1.5.0"
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 database_url: String,
pub port: u16,
pub domain: String,
pub auto_migrate: Option<bool>,
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 {
@ -43,21 +41,16 @@ impl Config {
_ => 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 {
fn default() -> Self {
Config {
database_url: env::var("DATABASE_URL").unwrap_or("".into()),
domain: "".into(),
port: 3000,
auto_migrate: None,
log_level: None,
insecure_requests: None,
}
}
}

View file

@ -1,6 +1,6 @@
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
use eyre::{Context, Report, Result};
use foxchat::QueryError;
use foxchat::FoxError;
use rand::RngCore;
use sqlx::{PgExecutor, Pool, Postgres};
@ -22,16 +22,16 @@ pub async fn get_user_by_username_and_password(
.await
.map_err(|e| -> Report {
match e {
sqlx::Error::RowNotFound => QueryError::NotFound.into(),
sqlx::Error::RowNotFound => FoxError::NotFound.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);
}
Err(QueryError::NotFound.into())
Err(FoxError::NotFound.into())
}
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 {
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()
.merge(account::router())
.route("/_fox/ident/node", get(node::get_node))
.route("/_fox/ident/node/:domain", get(node::get_chat_node))
.layer(TraceLayer::new_for_http())
.layer(Extension(app_state));

View file

@ -1,14 +1,16 @@
use std::sync::Arc;
use axum::{Extension, Json};
use eyre::{Result, Context};
use axum::{Extension, Json, extract::Path};
use eyre::{Context, Result};
use foxchat::http::ApiError;
use rsa::pkcs1::EncodeRsaPublicKey;
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 public_key = instance
@ -29,3 +31,12 @@ pub struct NodeResponse {
pub software: &'static str,
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 eyre::Result;
use foxchat::{fed::{self, request::is_valid_domain}, FoxError};
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 id: String,
pub domain: String,
@ -23,6 +26,10 @@ pub enum InstanceStatus {
impl ChatInstance {
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!(
Self,
r#"select id, domain, base_url, public_key,
@ -36,9 +43,49 @@ impl ChatInstance {
return Ok(instance);
}
// TODO: identify server process
// only try HTTP if `state.config.should_try_insecure()`
let current_instance = Instance::get(&state.pool).await?;
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,
}