Compare commits

...

2 commits

Author SHA1 Message Date
1a99cfd1a2 Implement init config command 2025-06-01 16:29:35 +09:00
8aa96d091a Merge server config to node config 2025-06-01 15:18:17 +09:00
11 changed files with 233 additions and 135 deletions

View file

@ -11,7 +11,7 @@ repository = "https://forgejo.fireturlte.net"
[workspace.dependencies]
lazy-supplements.path = "lazy-supplements"
lazy-supplements-migration.path = "lazy-supplements-migration"
libp2p = "0.55.0"
libp2p = { version = "0.55.0", features = ["noise", "ping", "tcp", "tokio", "yamux" ] }
[workspace.dependencies.sea-orm-migration]
version = "1.1.0"

View file

@ -3,26 +3,31 @@ use std::path::PathBuf;
use clap::Args;
use libp2p::identity;
use crate::{config::RawNodeConfig, global::DEFAULT_CONFIG_FILE_PATH};
#[derive(Args, Debug)]
pub struct InitArgs {
#[arg(long)]
config: Option<PathBuf>
config: Option<PathBuf>,
#[arg(short, long)]
force: bool,
#[command(flatten)]
node_config: RawNodeConfig,
}
impl InitArgs {
fn main(self) {
pub async fn init_config(self) {
let config_path = if let Some(x) = self.config {
x
} else {
crate::cli::default_config_path()
DEFAULT_CONFIG_FILE_PATH.to_path_buf()
};
if config_path.exists() {
if config_path.exists() && !self.force {
println!("Config file already exists!");
return;
} else {
let keypair = identity::Keypair::generate_ed25519();
let buf = keypair.to_protobuf_encoding().unwrap();
let config = self.node_config.with_new_secret();
config.write_to(config_path).await.unwrap()
}
}

View file

@ -2,10 +2,6 @@ use std::path::PathBuf;
mod init;
mod node;
mod server;
pub use server::ServerArgs;
pub fn default_config_path() -> PathBuf {
todo!()
}
pub use init::InitArgs;
pub use node::{ NodeArgs, NodeCommand, JoinNodeArgs };

View file

@ -1,24 +1,30 @@
use std::{net::IpAddr, path::PathBuf};
use clap::{Args, Subcommand};
use libp2p::PeerId;
use futures::StreamExt;
use libp2p::{
multiaddr::Protocol, noise, ping, swarm::SwarmEvent, tcp, yamux, Multiaddr
};
use tracing_subscriber::EnvFilter;
use crate::error::Error;
#[derive(Args, Debug)]
pub struct NodeArgs {
#[command(subcommand)]
command: NodeCommand
pub command: NodeCommand
}
#[derive(Args, Debug)]
pub struct JoinNodeArgs {
#[arg(long)]
endpoint: IpAddr,
pub endpoint: IpAddr,
#[arg(long)]
port: i32,
pub port: u16,
#[arg(long)]
peer_id: String,
pub peer_id: String,
#[arg(long)]
config: Option<PathBuf>,
pub config: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
@ -28,3 +34,40 @@ pub enum NodeCommand {
}
impl JoinNodeArgs {
pub async fn ping(self) -> Result<(), Error> {
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.try_init();
let mut swarm = libp2p::SwarmBuilder::with_new_identity()
.with_tokio()
.with_tcp(
tcp::Config::default(),
noise::Config::new,
yamux::Config::default,
)?
.with_behaviour(|_| ping::Behaviour::default())?
.build();
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
let mut remote: Multiaddr = Multiaddr::empty();
remote.push(match self.endpoint {
IpAddr::V4(x) => Protocol::Ip4(x),
IpAddr::V6(x) => Protocol::Ip6(x),
});
remote.push(Protocol::Tcp(self.port));
let addr = remote.to_string();
swarm.dial(remote)?;
println!("Dialed {addr}");
loop{
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"),
SwarmEvent::Behaviour(event) => println!("{event:?}"),
_ => {}
}
}
}
}

View file

@ -1,13 +0,0 @@
use std::{net::IpAddr, path::PathBuf};
use clap::Args;
use crate::config::PartialServerConfig;
#[derive(Args, Debug)]
pub struct ServerArgs {
#[command(flatten)]
server_config: PartialServerConfig,
#[arg(long)]
config: PathBuf,
}

View file

@ -1,30 +1,23 @@
mod node;
mod server;
use std::path::Path;
use crate::error::Error;
pub use node::NodeConfig;
pub use node::{ NodeConfig, RawNodeConfig };
use serde::{Deserialize, Serialize};
pub use server::{
PartialServerConfig,
ServerConfig,
pub use crate::global::{
DEFAULT_LISTEN_IPS,
DEFAULT_PORT,
DEFAULT_PARTIAL_SERVER_CONFIG,
DEFAULT_SERVER_CONFIG
};
use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}};
#[derive(Debug, Deserialize, Serialize)]
pub struct PartialConfig {
node: Option<NodeConfig>,
server: Option<ServerConfig>,
}
impl PartialConfig {
pub fn new() -> Self {
PartialConfig {
node: Some(NodeConfig::new()),
server: None,
node: Some(NodeConfig::default()),
}
}
pub async fn read_or_create<T>(path: T) -> Result<Self, Error>

View file

@ -1,51 +1,142 @@
use std::path::PathBuf;
use std::{net::IpAddr, path::{Path, PathBuf}};
use libp2p::identity::{self, Keypair};
use base64::{prelude::BASE64_STANDARD, Engine};
use clap::Args;
use libp2p::identity::{self, DecodingError, Keypair};
use serde::{Deserialize, Serialize};
use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}};
use crate::global::DEFAULT_DATABASE_FILE_PATH;
use crate::{error::Error, global::DEFAULT_DATABASE_FILE_PATH};
use super::{PartialConfig, DEFAULT_LISTEN_IPS, DEFAULT_PORT};
fn keypair_to_base64(keypair: &Keypair) -> Result<String, Error> {
let vec = keypair.to_protobuf_encoding()?;
let base64 = BASE64_STANDARD.encode(vec);
Ok(base64)
}
fn base64_to_keypair(base64: &str) -> Result<Keypair, Error> {
let vec = BASE64_STANDARD.decode(base64)?;
Ok(Keypair::from_protobuf_encoding(&vec)?)
}
#[derive(Debug, Deserialize, Serialize)]
pub struct NodeConfig {
#[serde(with = "keypair")]
#[serde(with = "keypair_parser")]
secret: Keypair,
database_path: Option<PathBuf>
database_path: PathBuf,
listen_ips: Vec<IpAddr>,
port: u16,
}
impl NodeConfig {
pub fn new() -> Self {
Self {
impl Default for NodeConfig {
fn default() -> NodeConfig{
NodeConfig {
secret: identity::Keypair::generate_ed25519(),
database_path: None,
}
}
pub fn get_database_path(&self) -> PathBuf {
if let Some(x) = self.database_path.clone() {
x
} else {
DEFAULT_DATABASE_FILE_PATH.clone()
database_path: DEFAULT_DATABASE_FILE_PATH.to_path_buf(),
listen_ips: DEFAULT_LISTEN_IPS.to_vec(),
port: DEFAULT_PORT,
}
}
}
mod keypair {
use base64::{prelude::BASE64_STANDARD, Engine};
impl TryFrom<RawNodeConfig> for NodeConfig {
type Error = Error;
fn try_from(raw: RawNodeConfig) -> Result<NodeConfig, Self::Error> {
Ok(NodeConfig {
secret: base64_to_keypair(&raw.secret.ok_or(Error::MissingConfig("secret"))?)?,
database_path: raw.database_path.ok_or(Error::MissingConfig("database_path"))?,
listen_ips: raw.listen_ips.ok_or(Error::MissingConfig("listen_ips"))?,
port: raw.port.ok_or(Error::MissingConfig("port"))?
})
}
}
mod keypair_parser {
use libp2p::identity::Keypair;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(keypair: &Keypair, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
let vec = keypair.to_protobuf_encoding().unwrap();
let base64 = BASE64_STANDARD.encode(vec);
serializer.serialize_str(&base64)
match super::keypair_to_base64(keypair) {
Ok(x) => serializer.serialize_str(&x),
Err(_) => Err(serde::ser::Error::custom("Decoding keypair error"))
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Keypair, D::Error>
where D: Deserializer<'de>
{
let base64 = String::deserialize(deserializer)?;
let vec = BASE64_STANDARD.decode(base64).unwrap();
Ok(Keypair::from_protobuf_encoding(&vec).unwrap())
match super::base64_to_keypair(&String::deserialize(deserializer)?) {
Ok(x) => Ok(x),
Err(crate::error::Error::Base64Decode(_)) => Err(serde::de::Error::custom("Decoding base64 error")),
Err(_) => unreachable!()
}
}
}
#[derive(Args, Debug, Deserialize, Serialize)]
pub struct RawNodeConfig {
#[arg(skip)]
secret: Option<String>,
#[arg(long)]
database_path: Option<PathBuf>,
#[arg(long)]
listen_ips: Option<Vec<IpAddr>>,
#[arg(long)]
port: Option<u16>,
}
impl RawNodeConfig {
pub fn with_new_secret(mut self) -> Self {
self.secret = Some(keypair_to_base64(&Keypair::generate_ed25519()).unwrap());
self
}
pub fn new() -> Self {
RawNodeConfig {
secret: None,
database_path: None,
listen_ips: None,
port: None,
}
}
pub async fn read_or_create<T>(path: T) -> Result<Self, Error>
where
T: AsRef<Path>
{
if !path.as_ref().exists() {
Self::new().write_to(&path).await?;
}
Self::read_from(&path).await
}
pub async fn read_from<T>(path:T) -> Result<Self, Error>
where
T: AsRef<Path>
{
let mut file = File::open(path.as_ref()).await?;
let mut content = String::new();
file.read_to_string(&mut content).await?;
let config: RawNodeConfig = toml::from_str(&content)?;
Ok(config)
}
pub async fn write_to<T>(&self, path:T) -> Result<(), Error>
where
T: AsRef<Path>
{
if !path.as_ref().exists() {
if let Some(x) = path.as_ref().parent() {
std::fs::create_dir_all(x)?;
};
let _ = File::create(&path).await?;
}
let mut file = File::create(&path).await?;
file.write_all(toml::to_string(self)?.as_bytes()).await?;
Ok(())
}
}
@ -56,15 +147,10 @@ mod tests {
#[tokio::test]
async fn serialize_deserialize() {
async fn parse_keypair() {
let keypair = identity::Keypair::generate_ed25519();
let config = NodeConfig {
secret: keypair.clone(),
database_path: None,
};
let string = toml::to_string(&config).unwrap();
println!("Parsed config: {}", &string);
let parsed_config: NodeConfig = toml::from_str(&string).unwrap();
assert_eq!(keypair.public(), parsed_config.secret.public());
let keypair2 = base64_to_keypair(&keypair_to_base64(&keypair).unwrap()).unwrap();
assert_eq!(keypair.public(), keypair2.public());
}
}

View file

@ -1,54 +0,0 @@
use std::{collections::HashSet, net::{IpAddr, Ipv4Addr}, str::FromStr, sync::LazyLock};
use clap::Args;
use serde::{Deserialize, Serialize};
use crate::error::Error;
pub static DEFAULT_LISTEN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))];
pub static DEFAULT_PORT: u16 = 8080;
pub static DEFAULT_SERVER_CONFIG: LazyLock<ServerConfig> = LazyLock::new(|| {
ServerConfig{
listen_ips: Vec::from(DEFAULT_LISTEN_IPS),
port: DEFAULT_PORT
}
});
pub static DEFAULT_PARTIAL_SERVER_CONFIG: LazyLock<PartialServerConfig> = LazyLock::new(|| {
PartialServerConfig::from((*DEFAULT_SERVER_CONFIG).clone())
});
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ServerConfig {
listen_ips: Vec<IpAddr>,
port: u16,
}
impl TryFrom<PartialServerConfig> for ServerConfig {
type Error = Error;
fn try_from(config: PartialServerConfig) -> Result<ServerConfig, Self::Error>{
Ok(ServerConfig {
listen_ips: config.listen_ips.ok_or(Error::MissingConfig("listen_ips".to_string()))?,
port: config.port.ok_or(Error::MissingConfig("port".to_string()))?
})
}
}
#[derive(Args, Debug, Deserialize, Serialize)]
pub struct PartialServerConfig {
#[arg(long)]
listen_ips: Option<Vec<IpAddr>>,
#[arg(long)]
port: Option<u16>,
}
impl From<ServerConfig> for PartialServerConfig {
fn from(config: ServerConfig) -> PartialServerConfig {
PartialServerConfig {
listen_ips: Some(config.listen_ips),
port: Some(config.port)
}
}
}

View file

@ -1,13 +1,27 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Base64 decode error: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[error("DB Error: {0}")]
Db(#[from]sea_orm::DbErr),
#[error("Dial Error: {0}")]
Dial(#[from] libp2p::swarm::DialError),
#[error("Decoding identity error: {0}")]
IdentityDecoding(#[from] libp2p::identity::DecodingError),
#[error("Infallible: {0}")]
Infallible(#[from] std::convert::Infallible),
#[error("IO Error: {0}")]
Io(#[from]std::io::Error),
#[error("mandatory config `{0}` is missing")]
MissingConfig(String),
MissingConfig(&'static str),
#[error("Multiaddr error: {0}")]
Multiaddr(#[from] libp2p::multiaddr::Error),
#[error("Noise error: {0}")]
Noise(#[from] libp2p::noise::Error),
#[error("toml deserialization error: {0}")]
TomlDe(#[from] toml::de::Error),
#[error("toml serialization error: {0}")]
TomlSer(#[from] toml::ser::Error),
#[error("Transport error: {0}")]
Transport(#[from]libp2p::TransportError<std::io::Error>)
}

View file

@ -1,6 +1,6 @@
use std::{path::PathBuf, sync::LazyLock};
use std::{net::{IpAddr, Ipv4Addr}, path::PathBuf, sync::LazyLock};
use crate::config::{NodeConfig, ServerConfig};
use crate::config::NodeConfig;
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
@ -10,6 +10,10 @@ pub static PRODUCT_NAME: LazyLock<String> = LazyLock::new(|| {
env!("CARGO_PKG_NAME").to_string()
});
pub static DEFAULT_LISTEN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))];
pub static DEFAULT_PORT: u16 = 8080;
pub static DEFAULT_CONFIG_DIR_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
let dir = if let Some(x) = dirs::config_local_dir() {
x
@ -48,11 +52,9 @@ pub static DEFAULT_DATABASE_FILE_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
pub static GLOBAL: Global = Global{
node_config: OnceCell::const_new(),
server_config: OnceCell::const_new(),
database: OnceCell::const_new(),
};
pub struct Global {
pub server_config: OnceCell<ServerConfig>,
pub node_config: OnceCell<NodeConfig>,
pub database: OnceCell<DatabaseConnection>,
}

View file

@ -0,0 +1,26 @@
use clap::{Parser, Subcommand};
use lazy_supplements::{cli::{InitArgs, NodeArgs, NodeCommand}, *};
#[derive(Debug, Parser)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Node(NodeArgs),
Init(InitArgs),
}
#[tokio::main]
async fn main() {
match Cli::parse().command {
Command::Node(x) => match x.command {
NodeCommand::Ping(y) => y.ping().await.unwrap(),
NodeCommand::Join(y) => println!("{y:?}"),
},
Command::Init(x) => x.init_config().await,
}
}