chore: upgrade pubky

This commit is contained in:
coreyphillips
2024-09-29 08:57:21 -04:00
parent 6067a77868
commit 58008bdc45
17 changed files with 369 additions and 191 deletions

View File

@@ -16,7 +16,6 @@ class Pubky: NSObject {
@objc(parseAuthUrl:withResolver:withRejecter:) @objc(parseAuthUrl:withResolver:withRejecter:)
func parseAuthUrl(_ url: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { func parseAuthUrl(_ url: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
Task {
do { do {
let result = react_native_pubky.parseAuthUrl(url: url) let result = react_native_pubky.parseAuthUrl(url: url)
resolve(result) resolve(result)
@@ -24,13 +23,12 @@ class Pubky: NSObject {
reject("parseAuthUrl Error", "Failed to parse auth url", error) reject("parseAuthUrl Error", "Failed to parse auth url", error)
} }
} }
}
@objc(publish:recordContent:secretKey:withResolver:withRejecter:) @objc(publish:recordContent:secretKey:withResolver:withRejecter:)
func publish(recordName: String, recordContent: String, secretKey: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { func publish(recordName: String, recordContent: String, secretKey: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
Task { Task {
do { do {
let result = react_native_pubky.publish(recordName: recordName, recordContent: recordContent, secretKey: secretKey) let result = try await react_native_pubky.publish(recordName: recordName, recordContent: recordContent, secretKey: secretKey)
resolve(result) resolve(result)
} catch { } catch {
reject("publish Error", "Failed to publish", error) reject("publish Error", "Failed to publish", error)
@@ -42,7 +40,7 @@ class Pubky: NSObject {
func resolve(publicKey: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { func resolve(publicKey: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
Task { Task {
do { do {
let result = react_native_pubky.resolve(publicKey: publicKey) let result = try await react_native_pubky.resolve(publicKey: publicKey)
resolve(result) resolve(result)
} catch { } catch {
reject("resolve Error", "Failed to resolve", error) reject("resolve Error", "Failed to resolve", error)

View File

@@ -68,6 +68,10 @@ impl Session {
} }
pub fn deserialize(bytes: &[u8]) -> Result<Self> { pub fn deserialize(bytes: &[u8]) -> Result<Self> {
if bytes.is_empty() {
return Err(Error::EmptyPayload);
}
if bytes[0] > 0 { if bytes[0] > 0 {
return Err(Error::UnknownVersion); return Err(Error::UnknownVersion);
} }
@@ -80,8 +84,10 @@ impl Session {
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug, PartialEq)]
pub enum Error { pub enum Error {
#[error("Empty payload")]
EmptyPayload,
#[error("Unknown version")] #[error("Unknown version")]
UnknownVersion, UnknownVersion,
#[error(transparent)] #[error(transparent)]
@@ -123,4 +129,11 @@ mod tests {
assert_eq!(deseiralized, session) assert_eq!(deseiralized, session)
} }
#[test]
fn deserialize() {
let result = Session::deserialize(&[]);
assert_eq!(result, Err(Error::EmptyPayload));
}
} }

View File

@@ -2,7 +2,7 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use pkarr::Keypair; use pkarr::Keypair;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt::Debug, fmt::Debug,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -15,31 +15,92 @@ use pubky_common::timestamp::Timestamp;
const DEFAULT_HOMESERVER_PORT: u16 = 6287; const DEFAULT_HOMESERVER_PORT: u16 = 6287;
const DEFAULT_STORAGE_DIR: &str = "pubky"; const DEFAULT_STORAGE_DIR: &str = "pubky";
/// Server configuration pub const DEFAULT_LIST_LIMIT: u16 = 100;
#[derive(Serialize, Deserialize, Clone)] pub const DEFAULT_MAX_LIST_LIMIT: u16 = 1000;
pub struct Config {
testnet: bool, #[derive(Serialize, Deserialize, Clone, PartialEq)]
struct ConfigToml {
testnet: Option<bool>,
port: Option<u16>, port: Option<u16>,
bootstrap: Option<Vec<String>>, bootstrap: Option<Vec<String>>,
domain: String, domain: Option<String>,
/// Path to the storage directory storage: Option<PathBuf>,
secret_key: Option<String>,
dht_request_timeout: Option<Duration>,
default_list_limit: Option<u16>,
max_list_limit: Option<u16>,
}
/// Server configuration
#[derive(Debug, Clone)]
pub struct Config {
/// Whether or not this server is running in a testnet.
testnet: bool,
/// The configured port for this server.
port: Option<u16>,
/// Bootstrapping DHT nodes.
///
/// Helpful to run the server locally or in testnet.
bootstrap: Option<Vec<String>>,
/// A public domain for this server
/// necessary for web browsers running in https environment.
domain: Option<String>,
/// Path to the storage directory.
/// ///
/// Defaults to a directory in the OS data directory /// Defaults to a directory in the OS data directory
storage: Option<PathBuf>, storage: PathBuf,
#[serde(deserialize_with = "secret_key_deserialize")] /// Server keypair.
secret_key: Option<[u8; 32]>, ///
/// Defaults to a random keypair.
keypair: Keypair,
dht_request_timeout: Option<Duration>, dht_request_timeout: Option<Duration>,
/// The default limit of a list api if no `limit` query parameter is provided.
///
/// Defaults to `100`
default_list_limit: u16,
/// The maximum limit of a list api, even if a `limit` query parameter is provided.
///
/// Defaults to `1000`
max_list_limit: u16,
} }
impl Config { impl Config {
/// Load the config from a file. fn try_from_str(value: &str) -> Result<Self> {
pub async fn load(path: impl AsRef<Path>) -> Result<Config> { let config_toml: ConfigToml = toml::from_str(value)?;
let s = tokio::fs::read_to_string(path.as_ref())
.await
.with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?;
let config: Config = toml::from_str(&s)?; let keypair = if let Some(secret_key) = config_toml.secret_key {
let secret_key = deserialize_secret_key(secret_key)?;
Keypair::from_secret_key(&secret_key)
} else {
Keypair::random()
};
let storage = {
let dir = if let Some(storage) = config_toml.storage {
storage
} else {
let path = dirs_next::data_dir().ok_or_else(|| {
anyhow!("operating environment provides no directory for application data")
})?;
path.join(DEFAULT_STORAGE_DIR)
};
dir.join("homeserver")
};
let config = Config {
testnet: config_toml.testnet.unwrap_or(false),
port: config_toml.port,
bootstrap: config_toml.bootstrap,
domain: config_toml.domain,
keypair,
storage,
dht_request_timeout: config_toml.dht_request_timeout,
default_list_limit: config_toml.default_list_limit.unwrap_or(DEFAULT_LIST_LIMIT),
max_list_limit: config_toml
.default_list_limit
.unwrap_or(DEFAULT_MAX_LIST_LIMIT),
};
if config.testnet { if config.testnet {
let testnet_config = Config::testnet(); let testnet_config = Config::testnet();
@@ -53,17 +114,24 @@ impl Config {
Ok(config) Ok(config)
} }
/// Load the config from a file.
pub async fn load(path: impl AsRef<Path>) -> Result<Config> {
let s = tokio::fs::read_to_string(path.as_ref())
.await
.with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?;
Config::try_from_str(&s)
}
/// Testnet configurations /// Testnet configurations
pub fn testnet() -> Self { pub fn testnet() -> Self {
let testnet = pkarr::mainline::Testnet::new(10); let testnet = pkarr::mainline::Testnet::new(10);
info!(?testnet.bootstrap, "Testnet bootstrap nodes"); info!(?testnet.bootstrap, "Testnet bootstrap nodes");
let bootstrap = Some(testnet.bootstrap.to_owned()); let bootstrap = Some(testnet.bootstrap.to_owned());
let storage = Some( let storage = std::env::temp_dir()
std::env::temp_dir()
.join(Timestamp::now().to_string()) .join(Timestamp::now().to_string())
.join(DEFAULT_STORAGE_DIR), .join(DEFAULT_STORAGE_DIR);
);
Self { Self {
bootstrap, bootstrap,
@@ -77,11 +145,9 @@ impl Config {
/// Test configurations /// Test configurations
pub fn test(testnet: &pkarr::mainline::Testnet) -> Self { pub fn test(testnet: &pkarr::mainline::Testnet) -> Self {
let bootstrap = Some(testnet.bootstrap.to_owned()); let bootstrap = Some(testnet.bootstrap.to_owned());
let storage = Some( let storage = std::env::temp_dir()
std::env::temp_dir()
.join(Timestamp::now().to_string()) .join(Timestamp::now().to_string())
.join(DEFAULT_STORAGE_DIR), .join(DEFAULT_STORAGE_DIR);
);
Self { Self {
bootstrap, bootstrap,
@@ -98,26 +164,25 @@ impl Config {
self.bootstrap.to_owned() self.bootstrap.to_owned()
} }
pub fn domain(&self) -> &str { pub fn domain(&self) -> &Option<String> {
&self.domain &self.domain
} }
/// Get the path to the storage directory pub fn keypair(&self) -> &Keypair {
pub fn storage(&self) -> Result<PathBuf> { &self.keypair
let dir = if let Some(storage) = &self.storage {
PathBuf::from(storage)
} else {
let path = dirs_next::data_dir().ok_or_else(|| {
anyhow!("operating environment provides no directory for application data")
})?;
path.join(DEFAULT_STORAGE_DIR)
};
Ok(dir.join("homeserver"))
} }
pub fn keypair(&self) -> Keypair { pub fn default_list_limit(&self) -> u16 {
Keypair::from_secret_key(&self.secret_key.unwrap_or_default()) self.default_list_limit
}
pub fn max_list_limit(&self) -> u16 {
self.max_list_limit
}
/// Get the path to the storage directory
pub fn storage(&self) -> &PathBuf {
&self.storage
} }
pub(crate) fn dht_request_timeout(&self) -> Option<Duration> { pub(crate) fn dht_request_timeout(&self) -> Option<Duration> {
@@ -131,43 +196,53 @@ impl Default for Config {
testnet: false, testnet: false,
port: Some(0), port: Some(0),
bootstrap: None, bootstrap: None,
domain: "localhost".to_string(), domain: None,
storage: None, storage: storage(None)
secret_key: None, .expect("operating environment provides no directory for application data"),
keypair: Keypair::random(),
dht_request_timeout: None, dht_request_timeout: None,
default_list_limit: DEFAULT_LIST_LIMIT,
max_list_limit: DEFAULT_MAX_LIST_LIMIT,
} }
} }
} }
fn secret_key_deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error> fn deserialize_secret_key(s: String) -> anyhow::Result<[u8; 32]> {
where let bytes =
D: Deserializer<'de>, hex::decode(s).map_err(|_| anyhow!("secret_key in config.toml should hex encoded"))?;
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(s) => {
let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 { if bytes.len() != 32 {
return Err(serde::de::Error::custom("Expected a 32-byte array")); return Err(anyhow!(format!(
"secret_key in config.toml should be 32 bytes in hex (64 characters), got: {}",
bytes.len()
)));
} }
let mut arr = [0u8; 32]; let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes); arr.copy_from_slice(&bytes);
Ok(Some(arr))
} Ok(arr)
None => Ok(None),
}
} }
impl Debug for Config { fn storage(storage: Option<String>) -> Result<PathBuf> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let dir = if let Some(storage) = storage {
f.debug_map() PathBuf::from(storage)
.entry(&"testnet", &self.testnet) } else {
.entry(&"port", &self.port()) let path = dirs_next::data_dir().ok_or_else(|| {
.entry(&"storage", &self.storage()) anyhow!("operating environment provides no directory for application data")
.entry(&"public_key", &self.keypair().public_key()) })?;
.finish() path.join(DEFAULT_STORAGE_DIR)
};
Ok(dir.join("homeserver"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty() {
Config::try_from_str("").unwrap();
} }
} }

View File

@@ -1,31 +1,42 @@
use std::fs; use std::fs;
use std::path::Path;
use heed::{Env, EnvOpenOptions}; use heed::{Env, EnvOpenOptions};
mod migrations; mod migrations;
pub mod tables; pub mod tables;
use crate::config::Config;
use tables::{Tables, TABLES_COUNT}; use tables::{Tables, TABLES_COUNT};
pub const MAX_LIST_LIMIT: u16 = 100; pub const DEFAULT_MAP_SIZE: usize = 10995116277760; // 10TB (not = disk-space used)
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DB { pub struct DB {
pub(crate) env: Env, pub(crate) env: Env,
pub(crate) tables: Tables, pub(crate) tables: Tables,
pub(crate) config: Config,
} }
impl DB { impl DB {
pub fn open(storage: &Path) -> anyhow::Result<Self> { pub fn open(config: Config) -> anyhow::Result<Self> {
fs::create_dir_all(storage).unwrap(); fs::create_dir_all(config.storage())?;
let env = unsafe { EnvOpenOptions::new().max_dbs(TABLES_COUNT).open(storage) }?; let env = unsafe {
EnvOpenOptions::new()
.max_dbs(TABLES_COUNT)
// TODO: Add a configuration option?
.map_size(DEFAULT_MAP_SIZE)
.open(config.storage())
}?;
let tables = migrations::run(&env)?; let tables = migrations::run(&env)?;
let db = DB { env, tables }; let db = DB {
env,
tables,
config,
};
Ok(db) Ok(db)
} }
@@ -34,18 +45,15 @@ impl DB {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes; use bytes::Bytes;
use pkarr::Keypair; use pkarr::{mainline::Testnet, Keypair};
use pubky_common::timestamp::Timestamp;
use crate::config::Config;
use super::DB; use super::DB;
#[tokio::test] #[tokio::test]
async fn entries() { async fn entries() {
let storage = std::env::temp_dir() let db = DB::open(Config::test(&Testnet::new(0))).unwrap();
.join(Timestamp::now().to_string())
.join("pubky");
let db = DB::open(&storage).unwrap();
let keypair = Keypair::random(); let keypair = Keypair::random();
let path = "/pub/foo.txt"; let path = "/pub/foo.txt";

View File

@@ -13,7 +13,7 @@ use pubky_common::{
timestamp::Timestamp, timestamp::Timestamp,
}; };
use crate::database::{DB, MAX_LIST_LIMIT}; use crate::database::DB;
use super::events::Event; use super::events::Event;
@@ -157,7 +157,7 @@ impl DB {
/// Return a list of pubky urls. /// Return a list of pubky urls.
/// ///
/// - limit defaults to and capped by [MAX_LIST_LIMIT] /// - limit defaults to [Config::default_list_limit] and capped by [Config::max_list_limit]
pub fn list( pub fn list(
&self, &self,
txn: &RoTxn, txn: &RoTxn,
@@ -170,7 +170,9 @@ impl DB {
// Vector to store results // Vector to store results
let mut results = Vec::new(); let mut results = Vec::new();
let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); let limit = limit
.unwrap_or(self.config.default_list_limit())
.min(self.config.max_list_limit());
// TODO: make this more performant than split and allocations? // TODO: make this more performant than split and allocations?

View File

@@ -10,6 +10,8 @@ use heed::{
use postcard::{from_bytes, to_allocvec}; use postcard::{from_bytes, to_allocvec};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::database::DB;
/// Event [Timestamp] base32 => Encoded event. /// Event [Timestamp] base32 => Encoded event.
pub type EventsTable = Database<Str, Bytes>; pub type EventsTable = Database<Str, Bytes>;
@@ -56,3 +58,48 @@ impl Event {
} }
} }
} }
impl DB {
/// Returns a list of events formatted as `<OP> <url>`.
///
/// - limit defaults to [Config::default_list_limit] and capped by [Config::max_list_limit]
/// - cursor is a 13 character string encoding of a timestamp
pub fn list_events(
&self,
limit: Option<u16>,
cursor: Option<String>,
) -> anyhow::Result<Vec<String>> {
let txn = self.env.read_txn()?;
let limit = limit
.unwrap_or(self.config.default_list_limit())
.min(self.config.max_list_limit());
let cursor = cursor.unwrap_or("0000000000000".to_string());
let mut result: Vec<String> = vec![];
let mut next_cursor = cursor.to_string();
for _ in 0..limit {
match self.tables.events.get_greater_than(&txn, &next_cursor)? {
Some((timestamp, event_bytes)) => {
let event = Event::deserialize(event_bytes)?;
let line = format!("{} {}", event.operation(), event.url());
next_cursor = timestamp.to_string();
result.push(line);
}
None => break,
};
}
if !result.is_empty() {
result.push(format!("cursor: {next_cursor}"))
}
txn.commit()?;
Ok(result)
}
}

View File

@@ -5,6 +5,7 @@ use axum::{
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
}; };
use tracing::debug;
pub type Result<T, E = Error> = core::result::Result<T, E>; pub type Result<T, E = Error> = core::result::Result<T, E>;
@@ -86,36 +87,42 @@ impl From<pkarr::Error> for Error {
impl From<std::io::Error> for Error { impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self { fn from(error: std::io::Error) -> Self {
debug!(?error);
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into())
} }
} }
impl From<heed::Error> for Error { impl From<heed::Error> for Error {
fn from(error: heed::Error) -> Self { fn from(error: heed::Error) -> Self {
debug!(?error);
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into())
} }
} }
impl From<anyhow::Error> for Error { impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self { fn from(error: anyhow::Error) -> Self {
debug!(?error);
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into())
} }
} }
impl From<postcard::Error> for Error { impl From<postcard::Error> for Error {
fn from(error: postcard::Error) -> Self { fn from(error: postcard::Error) -> Self {
debug!(?error);
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into())
} }
} }
impl From<axum::Error> for Error { impl From<axum::Error> for Error {
fn from(error: axum::Error) -> Self { fn from(error: axum::Error) -> Self {
debug!(?error);
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into())
} }
} }
impl<T> From<flume::SendError<T>> for Error { impl<T> From<flume::SendError<T>> for Error {
fn from(error: flume::SendError<T>) -> Self { fn from(error: flume::SendError<T>) -> Self {
debug!(?error);
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into()) Self::new(StatusCode::INTERNAL_SERVER_ERROR, error.into())
} }
} }

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use axum::{ use axum::{
async_trait, async_trait,
extract::{FromRequestParts, Path}, extract::{FromRequestParts, Path, Query},
http::{request::Parts, StatusCode}, http::{request::Parts, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
RequestPartsExt, RequestPartsExt,
@@ -74,3 +74,50 @@ where
Ok(EntryPath(path.to_string())) Ok(EntryPath(path.to_string()))
} }
} }
#[derive(Debug)]
pub struct ListQueryParams {
pub limit: Option<u16>,
pub cursor: Option<String>,
pub reverse: bool,
pub shallow: bool,
}
#[async_trait]
impl<S> FromRequestParts<S> for ListQueryParams
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let params: Query<HashMap<String, String>> =
parts.extract().await.map_err(IntoResponse::into_response)?;
let reverse = params.contains_key("reverse");
let shallow = params.contains_key("shallow");
let limit = params
.get("limit")
// Treat `limit=` as None
.and_then(|l| if l.is_empty() { None } else { Some(l) })
.and_then(|l| l.parse::<u16>().ok());
let cursor = params
.get("cursor")
.map(|c| c.as_str())
// Treat `cursor=` as None
.and_then(|c| {
if c.is_empty() {
None
} else {
Some(c.to_string())
}
});
Ok(ListQueryParams {
reverse,
shallow,
limit,
cursor,
})
}
}

View File

@@ -5,8 +5,8 @@ use pkarr::{
Keypair, PkarrClientAsync, SignedPacket, Keypair, PkarrClientAsync, SignedPacket,
}; };
pub async fn publish_server_packet( pub(crate) async fn publish_server_packet(
pkarr_client: PkarrClientAsync, pkarr_client: &PkarrClientAsync,
keypair: &Keypair, keypair: &Keypair,
domain: &str, domain: &str,
port: u16, port: u16,

View File

@@ -1,67 +1,37 @@
use std::collections::HashMap;
use axum::{ use axum::{
body::Body, body::Body,
extract::{Query, State}, extract::State,
http::{header, Response, StatusCode}, http::{header, Response, StatusCode},
response::IntoResponse, response::IntoResponse,
}; };
use pubky_common::timestamp::{Timestamp, TimestampError};
use crate::{ use crate::{
database::{tables::events::Event, MAX_LIST_LIMIT}, error::{Error, Result},
error::Result, extractors::ListQueryParams,
server::AppState, server::AppState,
}; };
pub async fn feed( pub async fn feed(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<HashMap<String, String>>, params: ListQueryParams,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
let txn = state.db.env.read_txn()?; if let Some(ref cursor) = params.cursor {
if let Err(timestmap_error) = Timestamp::try_from(cursor.to_string()) {
let limit = params let cause = match timestmap_error {
.get("limit") TimestampError::InvalidEncoding => {
.and_then(|l| l.parse::<u16>().ok()) "Cursor should be valid base32 Crockford encoding of a timestamp"
.unwrap_or(MAX_LIST_LIMIT)
.min(MAX_LIST_LIMIT);
let mut cursor = params
.get("cursor")
.map(|c| c.as_str())
.unwrap_or("0000000000000");
// Guard against bad cursor
if cursor.len() < 13 {
cursor = "0000000000000"
} }
TimestampError::InvalidBytesLength(size) => {
let mut result: Vec<String> = vec![]; &format!("Cursor should be 13 characters long, got: {size}")
let mut next_cursor = cursor.to_string();
for _ in 0..limit {
match state
.db
.tables
.events
.get_greater_than(&txn, &next_cursor)?
{
Some((timestamp, event_bytes)) => {
let event = Event::deserialize(event_bytes)?;
let line = format!("{} {}", event.operation(), event.url());
next_cursor = timestamp.to_string();
result.push(line);
} }
None => break,
}; };
Err(Error::new(StatusCode::BAD_REQUEST, cause.into()))?
}
} }
if !result.is_empty() { let result = state.db.list_events(params.limit, params.cursor)?;
result.push(format!("cursor: {next_cursor}"))
}
txn.commit()?;
Ok(Response::builder() Ok(Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)

View File

@@ -1,8 +1,6 @@
use std::collections::HashMap;
use axum::{ use axum::{
body::{Body, Bytes}, body::{Body, Bytes},
extract::{Query, State}, extract::State,
http::{header, Response, StatusCode}, http::{header, Response, StatusCode},
response::IntoResponse, response::IntoResponse,
}; };
@@ -12,7 +10,7 @@ use tower_cookies::Cookies;
use crate::{ use crate::{
error::{Error, Result}, error::{Error, Result},
extractors::{EntryPath, Pubky}, extractors::{EntryPath, ListQueryParams, Pubky},
server::AppState, server::AppState,
}; };
@@ -65,7 +63,7 @@ pub async fn get(
State(state): State<AppState>, State(state): State<AppState>,
pubky: Pubky, pubky: Pubky,
path: EntryPath, path: EntryPath,
Query(params): Query<HashMap<String, String>>, params: ListQueryParams,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
verify(path.as_str())?; verify(path.as_str())?;
let public_key = pubky.public_key(); let public_key = pubky.public_key();
@@ -88,10 +86,10 @@ pub async fn get(
let vec = state.db.list( let vec = state.db.list(
&txn, &txn,
&path, &path,
params.contains_key("reverse"), params.reverse,
params.get("limit").and_then(|l| l.parse::<u16>().ok()), params.limit,
params.get("cursor").map(|cursor| cursor.into()), params.cursor,
params.contains_key("shallow"), params.shallow,
)?; )?;
return Ok(Response::builder() return Ok(Response::builder()

View File

@@ -14,25 +14,24 @@ use crate::{config::Config, database::DB, pkarr::publish_server_packet};
#[derive(Debug)] #[derive(Debug)]
pub struct Homeserver { pub struct Homeserver {
port: u16, state: AppState,
config: Config,
tasks: JoinSet<std::io::Result<()>>, tasks: JoinSet<std::io::Result<()>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct AppState { pub(crate) struct AppState {
pub verifier: AuthVerifier, pub(crate) verifier: AuthVerifier,
pub db: DB, pub(crate) db: DB,
pub pkarr_client: PkarrClientAsync, pub(crate) pkarr_client: PkarrClientAsync,
pub(crate) config: Config,
pub(crate) port: u16,
} }
impl Homeserver { impl Homeserver {
pub async fn start(config: Config) -> Result<Self> { pub async fn start(config: Config) -> Result<Self> {
debug!(?config); debug!(?config);
let keypair = config.keypair(); let db = DB::open(config.clone())?;
let db = DB::open(&config.storage()?)?;
let pkarr_client = PkarrClient::new(Settings { let pkarr_client = PkarrClient::new(Settings {
dht: DhtSettings { dht: DhtSettings {
@@ -44,22 +43,22 @@ impl Homeserver {
})? })?
.as_async(); .as_async();
let state = AppState {
verifier: AuthVerifier::default(),
db,
pkarr_client: pkarr_client.clone(),
};
let app = crate::routes::create_app(state);
let mut tasks = JoinSet::new(); let mut tasks = JoinSet::new();
let app = app.clone();
let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?; let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?;
let port = listener.local_addr()?.port(); let port = listener.local_addr()?.port();
let state = AppState {
verifier: AuthVerifier::default(),
db,
pkarr_client,
config: config.clone(),
port,
};
let app = crate::routes::create_app(state.clone());
// Spawn http server task // Spawn http server task
tasks.spawn( tasks.spawn(
axum::serve( axum::serve(
@@ -72,15 +71,24 @@ impl Homeserver {
info!("Homeserver listening on http://localhost:{port}"); info!("Homeserver listening on http://localhost:{port}");
publish_server_packet(pkarr_client, &keypair, config.domain(), port).await?; publish_server_packet(
&state.pkarr_client,
info!("Homeserver listening on pubky://{}", keypair.public_key()); config.keypair(),
&state
Ok(Self { .config
tasks, .domain()
config, .clone()
.unwrap_or("localhost".to_string()),
port, port,
}) )
.await?;
info!(
"Homeserver listening on pubky://{}",
config.keypair().public_key()
);
Ok(Self { tasks, state })
} }
/// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage.
@@ -93,11 +101,11 @@ impl Homeserver {
// === Getters === // === Getters ===
pub fn port(&self) -> u16 { pub fn port(&self) -> u16 {
self.port self.state.port
} }
pub fn public_key(&self) -> PublicKey { pub fn public_key(&self) -> PublicKey {
self.config.keypair().public_key() self.state.config.keypair().public_key()
} }
// === Public Methods === // === Public Methods ===

View File

@@ -11,7 +11,7 @@ use reqwest::{RequestBuilder, Response};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use url::Url; use url::Url;
use pkarr::Keypair; use pkarr::{Keypair, PkarrClientAsync};
use ::pkarr::{mainline::dht::Testnet, PkarrClient, PublicKey, SignedPacket}; use ::pkarr::{mainline::dht::Testnet, PkarrClient, PublicKey, SignedPacket};
@@ -112,6 +112,13 @@ impl PubkyClient {
builder.build() builder.build()
} }
// === Getters ===
/// Returns a reference to the internal [pkarr] Client.
pub fn pkarr(&self) -> &PkarrClientAsync {
&self.pkarr
}
// === Auth === // === Auth ===
/// Signup to a homeserver and update Pkarr accordingly. /// Signup to a homeserver and update Pkarr accordingly.

View File

@@ -305,10 +305,9 @@ mod tests {
.unwrap(); .unwrap();
} }
let session = pubkyauth_response.await.unwrap().unwrap(); let public_key = pubkyauth_response.await.unwrap();
assert_eq!(session.pubky(), &pubky); assert_eq!(&public_key, &pubky);
assert_eq!(session.capabilities(), &capabilities.0);
// Test access control enforcement // Test access control enforcement

View File

@@ -364,7 +364,6 @@ pub fn publish(record_name: String, record_content: String, secret_key: String)
} }
} }
} }
#[uniffi::export] #[uniffi::export]
pub fn list(url: String) -> Vec<String> { pub fn list(url: String) -> Vec<String> {
let runtime = TOKIO_RUNTIME.clone(); let runtime = TOKIO_RUNTIME.clone();