Merge branch 'oxidation'

This commit is contained in:
Samuel Lorch 2023-10-28 18:47:47 +02:00
commit 797c6403ba
21 changed files with 2583 additions and 7 deletions

9
.gitignore vendored
View file

@ -1,9 +1,4 @@
config.json
pending.json
nftables.conf
interfaces.conf
go.work
nfsense
nfsense.exe
out/*
out
/target

View file

@ -3,5 +3,9 @@
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
}
}

1638
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "nfsense"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = "0.1.74"
axum = "0.6.20"
custom_error = "1.9.2"
ipnet = { version = "2.8.0", features = ["serde"] }
jsonrpsee = { version = "0.20.3", features = ["server"] }
macaddr = { version = "1.0.1", features = ["serde"] }
pwhash = "1.0.0"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
thiserror = "1.0.50"
tokio = { version = "1.33.0", features = ["full"] }
tower-cookies = "0.9.0"
tower-http = "0.4.4"
tracing = "0.1.40"
tracing-subscriber = "0.3.17"
uuid = { version = "1.5.0", features = ["v4"] }
validator = { version = "0.15", features = ["derive"] }

48
src/api/mod.rs Normal file
View file

@ -0,0 +1,48 @@
mod network;
mod system;
use crate::state::RpcState;
use jsonrpsee::{
types::{error::ErrorCode, ErrorObject},
RpcModule,
};
use custom_error::custom_error;
use tracing::info;
custom_error! { pub ApiError
InvalidParams = "Invalid Parameters",
Leet = "1337",
}
impl Into<ErrorObject<'static>> for ApiError {
fn into(self) -> ErrorObject<'static> {
match self {
Self::InvalidParams => ErrorCode::InvalidParams,
Self::Leet => ErrorCode::ServerError(1337),
_ => ErrorCode::InternalError,
}
.into()
}
}
pub fn new_rpc_module(state: RpcState) -> RpcModule<RpcState> {
let mut module = RpcModule::new(state);
module
.register_method("ping", |_, _| {
info!("ping called");
"pong"
})
.unwrap();
module
.register_method("system.get_users", system::get_users)
.unwrap();
module
.register_method("network.get_static_routes", network::get_static_routes)
.unwrap();
module
}

13
src/api/network.rs Normal file
View file

@ -0,0 +1,13 @@
use jsonrpsee::types::Params;
use crate::{definitions::network::StaticRoute, state::RpcState};
use super::ApiError;
pub fn get_static_routes(_: Params, state: &RpcState) -> Result<Vec<StaticRoute>, ApiError> {
Ok(state
.config_manager
.get_pending_config()
.network
.static_routes)
}

10
src/api/system.rs Normal file
View file

@ -0,0 +1,10 @@
use std::collections::HashMap;
use crate::{definitions::system::User, state::RpcState};
use jsonrpsee::types::Params;
use super::ApiError;
pub fn get_users(_: Params, state: &RpcState) -> Result<HashMap<String, User>, ApiError> {
Ok(state.config_manager.get_pending_config().system.users)
}

127
src/config_manager.rs Normal file
View file

@ -0,0 +1,127 @@
use validator::Validate;
use super::definitions::config::Config;
use std::fs;
use std::sync::{Arc, Mutex, MutexGuard};
use std::{io, result::Result};
use custom_error::custom_error;
use pwhash::sha512_crypt;
custom_error! { pub ConfigError
IoError{source: io::Error} = "io error",
SerdeError{source: serde_json::Error} = "serde json error",
ValidatonError{source: validator::ValidationErrors} = "validation failed",
HashError{source: pwhash::error::Error} = "password hash generation",
UnsupportedVersionError = "unsupported config version",
}
pub const CURRENT_CONFIG_PATH: &str = "config.json";
pub const PENDING_CONFIG_PATH: &str = "pending.json";
#[derive(Clone)]
pub struct ConfigManager {
shared_data: Arc<Mutex<SharedData>>,
}
struct SharedData {
current_config: Config,
pending_config: Config,
}
// Note, using unwarp on a mutex lock is ok since that only errors with mutex poisoning
impl ConfigManager {
pub fn new() -> Result<Self, ConfigError> {
Ok(Self {
shared_data: Arc::new(Mutex::new(SharedData {
current_config: read_file_to_config(CURRENT_CONFIG_PATH)?,
// TODO Dont Fail if pending config is missing, use current instead
pending_config: read_file_to_config(PENDING_CONFIG_PATH)?,
})),
})
}
pub fn get_current_config(&self) -> Config {
self.shared_data.lock().unwrap().current_config.clone()
}
pub fn get_pending_config(&self) -> Config {
self.shared_data.lock().unwrap().pending_config.clone()
}
pub fn apply_pending_changes(&mut self) -> Result<(), ConfigError> {
let mut data = self.shared_data.lock().unwrap();
// TODO run Apply functions
// TODO Revert on Apply Failure and Return
write_config_to_file(CURRENT_CONFIG_PATH, data.pending_config.clone())?;
// TODO revert if config save fails
// TODO Remove Pending Config File
data.current_config = data.pending_config.clone();
Ok(())
}
pub fn discard_pending_changes(&mut self) -> Result<(), ConfigError> {
let mut data = self.shared_data.lock().unwrap();
// TODO Remove Pending Config File
data.pending_config = data.current_config.clone();
Ok(())
}
pub fn start_transaction(&mut self) -> Result<ConfigTransaction, ConfigError> {
let data = self.shared_data.lock().unwrap();
Ok(ConfigTransaction {
finished: false,
changes: data.pending_config.clone(),
shared_data: data,
})
}
}
pub struct ConfigTransaction<'a> {
finished: bool,
shared_data: MutexGuard<'a, SharedData>,
pub changes: Config,
}
impl<'a> ConfigTransaction<'a> {
pub fn commit(mut self) -> Result<(), ConfigError> {
let ch = self.changes.clone();
ch.validate()?;
self.shared_data.pending_config = ch.clone();
write_config_to_file(PENDING_CONFIG_PATH, ch.clone())?;
Ok(())
}
}
fn read_file_to_config(path: &str) -> Result<Config, ConfigError> {
let data = fs::read_to_string(path)?;
let conf: Config = serde_json::from_str(&data)?;
if conf.config_version != 1 {
return Err(ConfigError::UnsupportedVersionError);
}
Ok(conf)
}
fn write_config_to_file(path: &str, conf: Config) -> Result<(), ConfigError> {
let data: String = serde_json::to_string_pretty(&conf)?;
fs::write(path, data)?;
Ok(())
}
pub fn generate_default_config(path: &str) -> Result<(), ConfigError> {
let mut conf = Config::default();
let hash = sha512_crypt::hash("nfsense")?;
conf.config_version = 1;
conf.system.users.insert(
"admin".to_string(),
crate::definitions::system::User {
comment: "Default Admin".to_string(),
hash: hash,
},
);
write_config_to_file(path, conf)
}

20
src/definitions/config.rs Normal file
View file

@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
use validator::Validate;
use super::firewall;
use super::network;
use super::object;
use super::service;
use super::system;
use super::vpn;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct Config {
pub config_version: u64,
pub network: network::Network,
pub object: object::Object,
pub system: system::System,
pub service: service::Service,
pub vpn: vpn::VPN,
pub firewall: firewall::Firewall,
}

View file

@ -0,0 +1,61 @@
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct Firewall {
pub forward_rules: Vec<ForwardRule>,
pub destination_nat_rules: Vec<DestinationNATRule>,
pub source_nat_rules: Vec<SourceNATRule>,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct ForwardRule {
pub name: String,
pub services: Vec<String>,
pub source_addresses: Vec<String>,
pub destination_addresses: Vec<String>,
pub comment: String,
pub counter: bool,
pub verdict: Verdict,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct DestinationNATRule {
pub name: String,
pub services: Vec<String>,
pub source_addresses: Vec<String>,
pub destination_addresses: Vec<String>,
pub comment: String,
pub counter: bool,
pub dnat_address: String,
pub dnat_service: String,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct SourceNATRule {
pub name: String,
pub services: Vec<String>,
pub source_addresses: Vec<String>,
pub destination_addresses: Vec<String>,
pub comment: String,
pub counter: bool,
pub snat_type: SNATType,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
Accept,
Drop,
Continue,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum SNATType {
SNAT {
snat_address: String,
snat_service: String,
},
Masquerade,
}

7
src/definitions/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod config;
pub mod firewall;
pub mod network;
pub mod object;
pub mod service;
pub mod system;
pub mod vpn;

View file

@ -0,0 +1,44 @@
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, net::IpAddr};
use validator::Validate;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct Network {
pub interfaces: HashMap<String, NetworkInterface>,
pub static_routes: Vec<StaticRoute>,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct NetworkInterface {
pub alias: String,
pub comment: String,
pub interface_type: NetworkInterfaceType,
pub addressing_mode: AddressingMode,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum NetworkInterfaceType {
Hardware { device: String },
Vlan { id: i32, parent: String },
Bond { members: Vec<String> },
Bridge { members: Vec<String> },
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum AddressingMode {
None,
Static { address: String },
DHCP,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct StaticRoute {
pub name: String,
pub interface: String,
pub gateway: IpAddr,
pub destination: IpNet,
pub metric: u64,
}

54
src/definitions/object.rs Normal file
View file

@ -0,0 +1,54 @@
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, net::IpAddr};
use validator::Validate;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct Object {
pub addresses: HashMap<String, Address>,
pub services: HashMap<String, Service>,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct Address {
pub address_type: AddressType,
pub comment: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum AddressType {
Host { host: String },
Range { range: IpAddr },
Network { network: IpNet },
Group { children: Vec<String> },
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct Service {
pub service_type: ServiceType,
pub comment: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ServiceType {
TCP {
source_port: u64,
source_port_end: Option<u64>,
destination_port: u64,
destination_port_end: Option<u64>,
},
UDP {
source_port: u64,
source_port_end: Option<u64>,
destination_port: u64,
destination_port_end: Option<u64>,
},
ICMP {
code: u8,
},
Group {
children: Vec<String>,
},
}

View file

@ -0,0 +1,67 @@
use core::time;
use macaddr::MacAddr8;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use validator::Validate;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct Service {
pub dhcp_servers: Vec<DHCPServer>,
pub dns_servers: Vec<DNSServer>,
pub ntp_servers: Vec<NTPServer>,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct DHCPServer {
pub interface: String,
pub pool: Vec<String>,
pub lease_time: time::Duration,
pub gateway_mode: GatewayMode,
pub dns_server_mode: DNSServerMode,
pub ntp_server_mode: NTPServerMode,
pub reservations: Vec<Reservation>,
pub comment: String,
}
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct DNSServer {
pub interface: String,
pub comment: String,
}
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct NTPServer {
pub interface: String,
pub comment: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GatewayMode {
None,
Interface,
Specify { gateway: String },
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum DNSServerMode {
None,
Interface,
Specify { dns_servers: Vec<String> },
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum NTPServerMode {
None,
Interface,
Specify { ntp_servers: Vec<String> },
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Reservation {
pub ip_address: IpAddr,
pub hardware_address: MacAddr8,
pub comment: String,
}

14
src/definitions/system.rs Normal file
View file

@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use validator::Validate;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct System {
pub users: HashMap<String, User>,
}
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct User {
pub comment: String,
pub hash: String,
}

33
src/definitions/vpn.rs Normal file
View file

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use validator::Validate;
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct VPN {
pub wireguard: Wireguard,
}
#[derive(Serialize, Deserialize, Clone, Validate, Default, Debug)]
pub struct Wireguard {
pub interfaces: HashMap<String, WireguardInterface>,
pub peers: HashMap<String, WireguardPeer>,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct WireguardInterface {
pub public_key: String,
pub private_key: String,
pub listen_port: u64,
pub peers: Vec<String>,
pub comment: String,
}
#[derive(Serialize, Deserialize, Clone, Validate, Debug)]
pub struct WireguardPeer {
pub public_key: String,
pub preshared_key: Option<String>,
pub allowed_ips: Vec<String>,
pub endpoint: Option<String>,
pub persistent_keepalive: Option<u64>,
pub comment: String,
}

143
src/main.rs Normal file
View file

@ -0,0 +1,143 @@
#![allow(dead_code)]
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use crate::state::RpcState;
use axum::{middleware, Router};
use config_manager::ConfigManager;
use state::AppState;
use std::env;
use std::fs;
use tower_cookies::CookieManagerLayer;
use tracing::info;
use tracing_subscriber;
use web::auth::SessionState;
mod api;
mod config_manager;
mod definitions;
mod state;
mod web;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
info!("Starting...");
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args[1] == "generate-default" {
info!("Generating default config...");
config_manager::generate_default_config(config_manager::CURRENT_CONFIG_PATH).unwrap();
fs::copy(
config_manager::CURRENT_CONFIG_PATH,
config_manager::PENDING_CONFIG_PATH,
)
.unwrap();
info!("Done! Exiting...");
return;
}
// TODO Check Config Manager Setup Error
let config_manager = ConfigManager::new().unwrap();
let session_state = SessionState {
sessions: Arc::new(RwLock::new(HashMap::new())),
};
let app_state = AppState {
config_manager: config_manager.clone(),
session_state: session_state.clone(),
rpc_module: api::new_rpc_module(RpcState {
config_manager,
session_state,
}),
};
// Note: The Router Works Bottom Up, So the auth middleware will only applies to everything above it.
let main_router = Router::new()
.merge(web::rpc::routes())
.layer(middleware::from_fn_with_state(
app_state.clone(),
web::auth::mw_auth,
))
.merge(web::auth::routes())
.with_state(app_state)
.layer(CookieManagerLayer::new());
// .fallback_service(service)
info!("Server started successfully");
axum::Server::bind(&"[::]:8080".parse().unwrap())
.serve(main_router.into_make_service())
.await
.unwrap();
/*
let mut tx = config_manager.start_transaction().unwrap();
tx.changes
.firewall
.forward_rules
.push(definitions::firewall::ForwardRule {
name: "name".to_string(),
comment: "test".to_string(),
counter: true,
verdict: definitions::firewall::Verdict::Accept,
services: Vec::new(),
destination_addresses: Vec::new(),
source_addresses: Vec::new(),
});
tx.commit().unwrap();
config_manager.apply_pending_changes().unwrap();
let mut tx2 = config_manager.start_transaction().unwrap();
tx2.changes.network.interfaces.insert(
"inter1".to_string(),
definitions::network::NetworkInterface {
alias: "test".to_owned(),
comment: "test comment".to_owned(),
interface_type: definitions::network::NetworkInterfaceType::Hardware {
device: "eth0".to_owned(),
},
addressing_mode: definitions::network::AddressingMode::None,
},
);
tx2.changes.network.interfaces.insert(
"inter2".to_string(),
definitions::network::NetworkInterface {
alias: "test2".to_owned(),
comment: "test comment".to_owned(),
interface_type: definitions::network::NetworkInterfaceType::Hardware {
device: "eth0".to_owned(),
},
addressing_mode: definitions::network::AddressingMode::Static {
address: "192.168.1.1".to_owned(),
},
},
);
tx2.changes
.network
.static_routes
.push(definitions::network::StaticRoute {
name: "test1".to_string(),
interface: "eth0".to_string(),
gateway: "192.168.1.1".parse().unwrap(),
destination: "10.42.42.0/24".parse().unwrap(),
metric: 0,
});
tx2.commit().unwrap();
config_manager.apply_pending_changes().unwrap();
let applied_config = config_manager.get_current_config();
info!("applied_config = {:#?}", applied_config);
*/
}

17
src/state.rs Normal file
View file

@ -0,0 +1,17 @@
use jsonrpsee::RpcModule;
use super::config_manager::ConfigManager;
use super::web::auth::SessionState;
#[derive(Clone)]
pub struct AppState {
pub config_manager: ConfigManager,
pub session_state: SessionState,
pub rpc_module: RpcModule<RpcState>,
}
#[derive(Clone)]
pub struct RpcState {
pub config_manager: ConfigManager,
pub session_state: SessionState,
}

153
src/web/auth.rs Normal file
View file

@ -0,0 +1,153 @@
use std::collections::HashMap;
use std::error::Error;
use std::hash::Hash;
use std::sync::{Arc, RwLock};
use uuid::Uuid;
use super::super::AppState;
use axum::routing::post;
use axum::{Json, Router};
use serde::Deserialize;
use tower_cookies::{Cookie, Cookies};
use axum::{
extract::Extension,
extract::State,
http::{Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
};
use pwhash::sha512_crypt;
use tracing::info;
use custom_error::custom_error;
custom_error! { AuthError
NoSessionCookie = "No Session Cookie Found",
InvalidSession = "Invalid Session"
}
const SESSION_COOKIE: &str = "session";
#[derive(Clone, Debug)]
pub struct SessionState {
pub sessions: Arc<RwLock<HashMap<String, Session>>>,
}
#[derive(Clone, Debug)]
pub struct Session {
pub username: String,
//expires: time,
// TODO have permissions here for fast access, update Permissions with a config manager apply function
// permissions
}
#[derive(Clone, Debug, Deserialize)]
struct LoginParameters {
username: String,
password: String,
}
pub fn routes() -> Router<AppState> {
Router::new()
.route("/login", post(login_handler))
.route("/logout", post(logout_handler))
.route("/session", post(session_handler))
}
async fn login_handler(
cookies: Cookies,
State(state): State<AppState>,
Json(payload): Json<LoginParameters>,
) -> impl IntoResponse {
if let Some(user) = state
.config_manager
.get_current_config()
.system
.users
.get(&payload.username.to_string())
{
if sha512_crypt::verify(payload.password, &user.hash) {
let mut sessions = state.session_state.sessions.write().unwrap();
let id = Uuid::new_v4().to_string();
sessions.insert(
id.clone(),
Session {
username: payload.username.clone(),
},
);
cookies.add(Cookie::new(SESSION_COOKIE, id));
info!("user logged in: {:?}", payload.username);
return StatusCode::OK;
}
}
info!("user login failed: {:?}", payload.username);
StatusCode::UNAUTHORIZED
}
async fn logout_handler(cookies: Cookies, state: State<AppState>) -> impl IntoResponse {
let session_cookie = cookies.get(SESSION_COOKIE);
match session_cookie {
Some(s) => {
let session_id = s.value();
let mut sessions = state.session_state.sessions.write().unwrap();
// TODO Fix Cookie remove
// cookies.remove(s.clone());
if let Some(session) = sessions.get(session_id) {
info!("user logged out: {:?}", session.username);
sessions.remove(session_id);
return StatusCode::OK;
}
return StatusCode::UNAUTHORIZED;
}
None => return StatusCode::UNAUTHORIZED,
}
}
fn get_session(cookies: Cookies, state: SessionState) -> Result<Session, AuthError> {
let session_cookie = cookies.get(SESSION_COOKIE);
match session_cookie {
Some(s) => {
let session_id = s.value();
let sessions = state.sessions.write().unwrap();
if let Some(session) = sessions.get(session_id) {
return Ok(session.clone());
}
return Err(AuthError::InvalidSession);
}
None => return Err(AuthError::NoSessionCookie),
}
}
async fn session_handler(cookies: Cookies, State(state): State<AppState>) -> impl IntoResponse {
match get_session(cookies, state.session_state) {
// TODO Return build git commit hash as json result for frontend reloading
Ok(_) => return StatusCode::OK,
Err(_) => return StatusCode::UNAUTHORIZED,
}
}
pub async fn mw_auth<B>(
state: State<AppState>,
cookies: Cookies,
mut req: Request<B>,
next: Next<B>,
// session_state: SessionState,
) -> Result<Response, StatusCode> {
match get_session(cookies, state.session_state.clone()) {
Ok(session) => {
req.extensions_mut().insert(session.clone());
return Ok(next.run(req).await);
}
Err(_) => return Err(StatusCode::UNAUTHORIZED),
}
}

2
src/web/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod auth;
pub mod rpc;

101
src/web/rpc.rs Normal file
View file

@ -0,0 +1,101 @@
use crate::AppState;
use axum::routing::post;
use axum::{Json, Router};
use jsonrpsee::core::traits::ToRpcParams;
use jsonrpsee::core::Error;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use axum::{extract::Extension, extract::State, response::IntoResponse};
use tracing::info;
// TODO fix this "workaround"
struct ParamConverter {
params: Option<Box<RawValue>>,
}
impl ToRpcParams for ParamConverter {
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, Error> {
let s = String::from_utf8(serde_json::to_vec(&self.params)?);
match s {
Ok(s) => {
return RawValue::from_string(s)
.map(Some)
.map_err(Error::ParseError)
}
// TODO make this a Parse error wrapping Utf8Error
Err(err) => return Err(Error::AlreadyStopped),
}
}
}
#[derive(Deserialize)]
struct RpcRequest {
id: i64,
params: Option<Box<RawValue>>,
jsonrpc: String,
method: String,
}
#[derive(Clone, Deserialize, Serialize)]
struct RpcResponse {
id: i64,
jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Box<RawValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<RpcErrorObject>,
}
#[derive(Clone, Deserialize, Serialize)]
struct RpcErrorObject {
code: i64,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Box<RawValue>>,
}
pub fn routes() -> Router<super::super::AppState> {
Router::new().route("/api", post(api_handler))
}
async fn api_handler(
State(state): State<AppState>,
session: Extension<super::auth::Session>,
body: String,
) -> impl IntoResponse {
info!("api hit! user: {:?}", session.username);
// TODO handle Parse Error
let req: RpcRequest = serde_json::from_str(&body).unwrap();
// TODO check version
let params = ParamConverter { params: req.params };
// TODO check Permissions for method here?
let res: Result<Option<Box<RawValue>>, Error> =
state.rpc_module.call(&req.method, params).await;
match res {
Ok(res) => Json(RpcResponse {
id: req.id,
jsonrpc: req.jsonrpc,
result: res,
error: None,
}),
Err(err) => Json(RpcResponse {
id: req.id,
jsonrpc: req.jsonrpc,
result: None,
error: Some(RpcErrorObject {
code: 10,
message: err.to_string(),
data: None,
}),
}),
}
}