use axum::{
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Router,
};
use ipfrs_core::Result as CoreResult;
use ipfrs_semantic::{RouterConfig, SemanticRouter};
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
use ipfrs_tensorlogic::TensorLogicStore;
use std::sync::Arc;
use tower_http::trace::TraceLayer;
use tracing::{error, info};
use crate::auth::AuthState;
use crate::auth_handlers;
use crate::graphql::{create_schema, IpfrsSchema};
use crate::streaming;
use crate::tensor;
use crate::tls::TlsConfig;
#[derive(Clone)]
pub struct GatewayState {
pub(crate) store: Arc<SledBlockStore>,
semantic: Option<Arc<SemanticRouter>>,
tensorlogic: Option<Arc<TensorLogicStore<SledBlockStore>>>,
network: Option<Arc<tokio::sync::Mutex<ipfrs_network::NetworkNode>>>,
graphql_schema: Option<IpfrsSchema>,
pub(crate) auth: Option<AuthState>,
}
impl GatewayState {
pub fn new(config: BlockStoreConfig) -> CoreResult<Self> {
let store = SledBlockStore::new(config)?;
Ok(Self {
store: Arc::new(store),
semantic: None,
tensorlogic: None,
network: None,
graphql_schema: None,
auth: None,
})
}
pub fn with_auth(
mut self,
secret: &[u8],
default_admin_password: Option<&str>,
) -> CoreResult<Self> {
let auth_state = if let Some(password) = default_admin_password {
AuthState::with_default_admin(secret, password).map_err(|e| {
ipfrs_core::Error::Internal(format!("Failed to create auth state: {}", e))
})?
} else {
AuthState::new(secret)
};
self.auth = Some(auth_state);
Ok(self)
}
pub fn with_semantic(mut self, config: RouterConfig) -> CoreResult<Self> {
let semantic = SemanticRouter::new(config).map_err(|e| {
ipfrs_core::Error::Internal(format!("Failed to create semantic router: {}", e))
})?;
self.semantic = Some(Arc::new(semantic));
Ok(self)
}
pub fn with_tensorlogic(mut self) -> CoreResult<Self> {
let tensorlogic = TensorLogicStore::new(Arc::clone(&self.store))?;
self.tensorlogic = Some(Arc::new(tensorlogic));
Ok(self)
}
pub fn with_network(mut self, network: ipfrs_network::NetworkNode) -> Self {
self.network = Some(Arc::new(tokio::sync::Mutex::new(network)));
self
}
pub fn with_graphql(mut self) -> Self {
let schema = create_schema(
Arc::clone(&self.store),
self.semantic.clone(),
self.tensorlogic.clone(),
);
self.graphql_schema = Some(schema);
self
}
}
#[derive(Debug, Clone)]
pub struct GatewayConfig {
pub listen_addr: String,
pub storage_config: BlockStoreConfig,
pub tls_config: Option<TlsConfig>,
pub compression_config: crate::middleware::CompressionConfig,
}
impl Default for GatewayConfig {
fn default() -> Self {
Self {
listen_addr: "127.0.0.1:8080".to_string(),
storage_config: BlockStoreConfig::default(),
tls_config: None,
compression_config: crate::middleware::CompressionConfig::default(),
}
}
}
impl GatewayConfig {
pub fn production() -> Self {
Self {
listen_addr: "0.0.0.0:8080".to_string(),
storage_config: BlockStoreConfig::default()
.with_path("./ipfrs_data".into())
.with_cache_mb(500),
tls_config: None,
compression_config: crate::middleware::CompressionConfig {
enable_gzip: true,
level: crate::middleware::CompressionLevel::Best,
min_size: 512,
},
}
}
pub fn development() -> Self {
Self {
listen_addr: "127.0.0.1:8080".to_string(),
storage_config: BlockStoreConfig::default()
.with_path("./dev_data".into())
.with_cache_mb(50),
tls_config: None,
compression_config: crate::middleware::CompressionConfig {
enable_gzip: true,
level: crate::middleware::CompressionLevel::Fastest,
min_size: 2048,
},
}
}
pub fn testing() -> Self {
Self {
listen_addr: "127.0.0.1:0".to_string(),
storage_config: BlockStoreConfig::default()
.with_path(std::env::temp_dir().join("ipfrs_test"))
.with_cache_mb(10),
tls_config: None,
compression_config: crate::middleware::CompressionConfig {
enable_gzip: false,
level: crate::middleware::CompressionLevel::Fastest,
min_size: 1048576, },
}
}
pub fn with_listen_addr(mut self, addr: impl Into<String>) -> Self {
self.listen_addr = addr.into();
self
}
pub fn with_storage_path(mut self, path: impl Into<String>) -> Self {
self.storage_config = self.storage_config.with_path(path.into().into());
self
}
pub fn with_cache_mb(mut self, size_mb: usize) -> Self {
self.storage_config = self.storage_config.with_cache_mb(size_mb);
self
}
pub fn with_tls(mut self, tls_config: TlsConfig) -> Self {
self.tls_config = Some(tls_config);
self
}
pub fn with_compression_level(mut self, level: crate::middleware::CompressionLevel) -> Self {
self.compression_config.level = level;
self
}
pub fn with_full_compression(mut self) -> Self {
self.compression_config.enable_gzip = true;
self
}
pub fn without_compression(mut self) -> Self {
self.compression_config.enable_gzip = false;
self
}
pub fn validate(&self) -> CoreResult<()> {
if self.listen_addr.is_empty() {
return Err(ipfrs_core::Error::Internal(
"Listen address cannot be empty".to_string(),
));
}
self.listen_addr
.parse::<std::net::SocketAddr>()
.map_err(|e| ipfrs_core::Error::Internal(format!("Invalid listen address: {}", e)))?;
if self.storage_config.path.as_os_str().is_empty() {
return Err(ipfrs_core::Error::Internal(
"Storage path cannot be empty".to_string(),
));
}
if self.compression_config.min_size == 0 {
return Err(ipfrs_core::Error::Internal(
"Compression min_size must be greater than 0".to_string(),
));
}
Ok(())
}
}
pub struct Gateway {
config: GatewayConfig,
state: GatewayState,
}
impl Gateway {
pub fn new(config: GatewayConfig) -> CoreResult<Self> {
let state = GatewayState::new(config.storage_config.clone())?;
Ok(Self { config, state })
}
fn router(&self) -> Router {
let mut router = Router::new()
.route("/health", get(health_check))
.route("/metrics", get(metrics_endpoint))
.route("/ipfs/:cid", get(get_content))
.route("/api/v0/auth/login", post(auth_handlers::login_handler))
.route(
"/api/v0/auth/register",
post(auth_handlers::register_handler),
)
.route("/graphql", post(graphql_handler))
.route("/graphql", get(graphql_playground))
.route("/api/v0/version", get(api_version))
.route("/api/v0/add", post(api_add))
.route("/api/v0/block/get", post(api_block_get))
.route("/api/v0/block/put", post(api_block_put))
.route("/api/v0/block/stat", post(api_block_stat))
.route("/api/v0/cat", post(api_cat))
.route("/api/v0/dag/get", post(api_dag_get))
.route("/api/v0/dag/put", post(api_dag_put))
.route("/api/v0/dag/resolve", post(api_dag_resolve))
.route("/api/v0/semantic/index", post(api_semantic_index))
.route("/api/v0/semantic/search", post(api_semantic_search))
.route("/api/v0/semantic/stats", get(api_semantic_stats))
.route("/api/v0/semantic/save", post(api_semantic_save))
.route("/api/v0/semantic/load", post(api_semantic_load))
.route("/api/v0/logic/term", post(api_logic_store_term))
.route("/api/v0/logic/term/:cid", get(api_logic_get_term))
.route("/api/v0/logic/predicate", post(api_logic_store_predicate))
.route("/api/v0/logic/rule", post(api_logic_store_rule))
.route("/api/v0/logic/stats", get(api_logic_stats))
.route("/api/v0/logic/fact", post(api_logic_add_fact))
.route("/api/v0/logic/rule/add", post(api_logic_add_rule))
.route("/api/v0/logic/infer", post(api_logic_infer))
.route("/api/v0/logic/prove", post(api_logic_prove))
.route("/api/v0/logic/verify", post(api_logic_verify))
.route("/api/v0/logic/proof/:cid", get(api_logic_get_proof))
.route("/api/v0/logic/kb/stats", get(api_logic_kb_stats))
.route("/api/v0/logic/kb/save", post(api_logic_kb_save))
.route("/api/v0/logic/kb/load", post(api_logic_kb_load))
.route("/api/v0/id", get(api_network_id))
.route("/api/v0/swarm/peers", get(api_swarm_peers))
.route("/api/v0/swarm/connect", post(api_swarm_connect))
.route("/api/v0/swarm/disconnect", post(api_swarm_disconnect))
.route("/api/v0/dht/findprovs", post(api_dht_findprovs))
.route("/api/v0/dht/provide", post(api_dht_provide))
.route("/v1/stream/download/:cid", get(streaming::stream_download))
.route("/v1/stream/upload", post(streaming::stream_upload))
.route(
"/v1/progress/:operation_id",
get(streaming::progress_stream),
)
.route("/v1/block/batch/get", post(streaming::batch_get))
.route("/v1/block/batch/put", post(streaming::batch_put))
.route("/v1/block/batch/has", post(streaming::batch_has))
.route("/v1/tensor/:cid", get(tensor::get_tensor))
.route("/v1/tensor/:cid/info", get(tensor::get_tensor_info))
.route("/v1/tensor/:cid/arrow", get(tensor::get_tensor_arrow));
if self.state.auth.is_some() {
router = router
.route("/api/v0/auth/me", get(auth_handlers::me_handler))
.route(
"/api/v0/auth/permissions",
post(auth_handlers::update_permissions_handler),
)
.route(
"/api/v0/auth/deactivate/:username",
post(auth_handlers::deactivate_user_handler),
)
.route(
"/api/v0/auth/keys",
post(auth_handlers::create_api_key_handler),
)
.route(
"/api/v0/auth/keys",
get(auth_handlers::list_api_keys_handler),
)
.route(
"/api/v0/auth/keys/:key_id/revoke",
post(auth_handlers::revoke_api_key_handler),
)
.route(
"/api/v0/auth/keys/:key_id",
axum::routing::delete(auth_handlers::delete_api_key_handler),
);
}
router
.with_state(self.state.clone())
.layer(TraceLayer::new_for_http())
}
pub async fn start(self) -> CoreResult<()> {
let app = self.router();
self.print_endpoints();
if let Some(ref tls_config) = self.config.tls_config {
info!(
"Starting IPFRS HTTPS Gateway on {}",
self.config.listen_addr
);
let rustls_config = tls_config.build_server_config().await.map_err(|e| {
ipfrs_core::Error::Internal(format!("TLS configuration error: {}", e))
})?;
let addr: std::net::SocketAddr = self
.config
.listen_addr
.parse()
.map_err(|e| ipfrs_core::Error::Internal(format!("Invalid address: {}", e)))?;
info!("Gateway listening on https://{}", self.config.listen_addr);
info!("TLS/SSL enabled");
axum_server::bind_rustls(addr, rustls_config)
.serve(app.into_make_service())
.await
.map_err(|e| ipfrs_core::Error::Internal(format!("HTTPS server error: {}", e)))?;
} else {
info!("Starting IPFRS HTTP Gateway on {}", self.config.listen_addr);
let listener = tokio::net::TcpListener::bind(&self.config.listen_addr)
.await
.map_err(|e| {
ipfrs_core::Error::Internal(format!("Failed to bind to address: {}", e))
})?;
info!("Gateway listening on http://{}", self.config.listen_addr);
info!("Warning: TLS not enabled, using plain HTTP");
axum::serve(listener, app)
.await
.map_err(|e| ipfrs_core::Error::Internal(format!("HTTP server error: {}", e)))?;
}
Ok(())
}
fn print_endpoints(&self) {
info!("Endpoints:");
info!(" GET /health - Health check");
info!(" GET /ipfs/{{cid}} - Retrieve content");
if self.state.auth.is_some() {
info!(" POST /api/v0/auth/login - User login");
info!(" POST /api/v0/auth/register - User registration");
info!(" GET /api/v0/auth/me - Current user info");
info!(" POST /api/v0/auth/permissions - Update permissions (admin)");
info!(" POST /api/v0/auth/deactivate/:user - Deactivate user (admin)");
info!(" POST /api/v0/auth/keys - Create API key");
info!(" GET /api/v0/auth/keys - List API keys");
info!(" POST /api/v0/auth/keys/:id/revoke - Revoke API key");
info!(" DEL /api/v0/auth/keys/:id - Delete API key");
}
info!(" POST /api/v0/version - Get version");
info!(" POST /api/v0/add - Upload file");
info!(" POST /api/v0/block/get - Get block");
info!(" POST /api/v0/block/put - Store raw block");
info!(" POST /api/v0/block/stat - Get block stats");
info!(" POST /api/v0/cat - Output content");
info!(" POST /api/v0/dag/get - Get DAG node");
info!(" POST /api/v0/dag/put - Store DAG node");
info!(" POST /api/v0/dag/resolve - Resolve IPLD path");
info!(" POST /api/v0/semantic/index - Index content");
info!(" POST /api/v0/semantic/search - Search similar");
info!(" GET /api/v0/semantic/stats - Semantic stats");
info!(" POST /api/v0/semantic/save - Save semantic index");
info!(" POST /api/v0/semantic/load - Load semantic index");
info!(" POST /api/v0/logic/term - Store term");
info!(" GET /api/v0/logic/term/{{cid}} - Get term");
info!(" POST /api/v0/logic/predicate - Store predicate");
info!(" POST /api/v0/logic/rule - Store rule");
info!(" GET /api/v0/logic/stats - Logic stats");
info!(" POST /api/v0/logic/kb/save - Save knowledge base");
info!(" POST /api/v0/logic/kb/load - Load knowledge base");
info!(" GET /api/v0/id - Show peer ID");
info!(" GET /api/v0/swarm/peers - List peers");
info!(" POST /api/v0/swarm/connect - Connect to peer");
info!(" POST /api/v0/swarm/disconnect - Disconnect peer");
info!(" POST /api/v0/dht/findprovs - Find providers");
info!(" POST /api/v0/dht/provide - Announce content");
info!(" GET /v1/stream/download/:cid - Stream download");
info!(" POST /v1/stream/upload - Stream upload");
info!(" GET /v1/progress/:operation_id - SSE progress");
info!(" POST /v1/block/batch/get - Batch get blocks");
info!(" POST /v1/block/batch/put - Batch put blocks");
info!(" POST /v1/block/batch/has - Batch check blocks");
}
pub fn with_graphql(mut self) -> Self {
self.state = self.state.with_graphql();
self
}
pub fn with_auth(
mut self,
secret: &[u8],
default_admin_password: Option<&str>,
) -> CoreResult<Self> {
self.state = self.state.with_auth(secret, default_admin_password)?;
Ok(self)
}
pub fn with_semantic(mut self, config: RouterConfig) -> CoreResult<Self> {
self.state = self.state.with_semantic(config)?;
Ok(self)
}
pub fn with_tensorlogic(mut self) -> CoreResult<Self> {
self.state = self.state.with_tensorlogic()?;
Ok(self)
}
pub fn with_network(mut self, network: ipfrs_network::NetworkNode) -> Self {
self.state = self.state.with_network(network);
self
}
}
pub(crate) mod routes;
#[allow(unused_imports)]
use routes::*;
#[derive(Debug)]
enum AppError {
InvalidCid(String),
BlockNotFound(String),
NotFound(String),
Upload(String),
Storage(ipfrs_core::Error),
FeatureDisabled(String),
Semantic(String),
Logic(String),
Network(String),
}
impl From<ipfrs_core::Error> for AppError {
fn from(err: ipfrs_core::Error) -> Self {
AppError::Storage(err)
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::InvalidCid(cid) => (StatusCode::BAD_REQUEST, format!("Invalid CID: {}", cid)),
AppError::BlockNotFound(cid) => {
(StatusCode::NOT_FOUND, format!("Block not found: {}", cid))
}
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::Upload(msg) => {
error!("Upload error: {}", msg);
(StatusCode::BAD_REQUEST, format!("Upload error: {}", msg))
}
AppError::Storage(err) => {
error!("Storage error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Storage error: {}", err),
)
}
AppError::FeatureDisabled(msg) => (
StatusCode::SERVICE_UNAVAILABLE,
format!("Feature not available: {}", msg),
),
AppError::Semantic(msg) => {
error!("Semantic error: {}", msg);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Semantic error: {}", msg),
)
}
AppError::Logic(msg) => {
error!("Logic error: {}", msg);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Logic error: {}", msg),
)
}
AppError::Network(msg) => {
error!("Network error: {}", msg);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Network error: {}", msg),
)
}
};
(status, message).into_response()
}
}
#[cfg(test)]
mod tests {
use super::routes::{build_multipart_response, merge_ranges, parse_multi_range, parse_range};
use super::*;
use crate::middleware::CacheConfig;
use axum::http::header;
#[test]
fn test_parse_single_range() {
assert_eq!(parse_range("bytes=0-100", 1000), Some((0, 101)));
assert_eq!(parse_range("bytes=500-", 1000), Some((500, 1000)));
assert_eq!(parse_range("bytes=1000-1100", 1000), None);
assert_eq!(parse_range("bytes=500-100", 1000), None);
assert_eq!(parse_range("bytes=abc-100", 1000), None);
assert_eq!(parse_range("invalid", 1000), None);
}
#[test]
fn test_parse_multi_range() {
let ranges = parse_multi_range("bytes=0-100", 1000);
assert_eq!(ranges, Some(vec![(0, 101)]));
let ranges = parse_multi_range("bytes=0-100,200-300", 1000);
assert_eq!(ranges, Some(vec![(0, 101), (200, 301)]));
let ranges = parse_multi_range("bytes=0-100, 200-300, 500-600", 1000);
assert_eq!(ranges, Some(vec![(0, 101), (200, 301), (500, 601)]));
let ranges = parse_multi_range("bytes=-500", 1000);
assert_eq!(ranges, Some(vec![(500, 1000)]));
assert_eq!(parse_multi_range("bytes=1000-1100", 1000), None);
assert_eq!(parse_multi_range("invalid", 1000), None);
}
#[test]
fn test_merge_ranges() {
let ranges = vec![(0, 100), (200, 300)];
assert_eq!(merge_ranges(ranges), vec![(0, 100), (200, 300)]);
let ranges = vec![(0, 150), (100, 200)];
assert_eq!(merge_ranges(ranges), vec![(0, 200)]);
let ranges = vec![(0, 100), (100, 200)];
assert_eq!(merge_ranges(ranges), vec![(0, 200)]);
let ranges = vec![(200, 300), (0, 100), (50, 150)];
assert_eq!(merge_ranges(ranges), vec![(0, 150), (200, 300)]);
let ranges = vec![(50, 100)];
assert_eq!(merge_ranges(ranges), vec![(50, 100)]);
let ranges: Vec<(usize, usize)> = vec![];
assert_eq!(merge_ranges(ranges), vec![]);
}
#[test]
fn test_build_multipart_response() {
let data = b"Hello, World! This is test data for multi-range requests.";
let ranges = vec![(0, 5), (7, 12)];
let total_size = data.len();
let config = CacheConfig::default();
let response = build_multipart_response(data, &ranges, total_size, "QmTest123", &config);
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.expect("test: CONTENT_TYPE header must be present")
.to_str()
.expect("test: CONTENT_TYPE header value must be valid UTF-8");
assert!(content_type.starts_with("multipart/byteranges"));
assert!(content_type.contains("boundary="));
assert!(response.headers().contains_key(header::ETAG));
assert!(response.headers().contains_key(header::CACHE_CONTROL));
}
#[test]
fn test_config_presets() {
let prod = GatewayConfig::production();
assert_eq!(prod.listen_addr, "0.0.0.0:8080");
assert!(prod.compression_config.enable_gzip);
let dev = GatewayConfig::development();
assert_eq!(dev.listen_addr, "127.0.0.1:8080");
assert!(dev.compression_config.enable_gzip);
let test = GatewayConfig::testing();
assert_eq!(test.listen_addr, "127.0.0.1:0");
assert!(!test.compression_config.enable_gzip);
}
#[test]
fn test_config_builders() {
let config = GatewayConfig::default()
.with_listen_addr("0.0.0.0:9090")
.with_storage_path("/custom/path")
.with_cache_mb(200)
.with_full_compression();
assert_eq!(config.listen_addr, "0.0.0.0:9090");
assert!(config.compression_config.enable_gzip);
}
#[test]
fn test_config_validation() {
let valid_config = GatewayConfig::default();
assert!(valid_config.validate().is_ok());
let invalid_addr = GatewayConfig {
listen_addr: "invalid-address".to_string(),
..Default::default()
};
assert!(invalid_addr.validate().is_err());
let empty_addr = GatewayConfig {
listen_addr: "".to_string(),
..Default::default()
};
assert!(empty_addr.validate().is_err());
}
#[test]
fn test_compression_helpers() {
let config_with = GatewayConfig::default().with_full_compression();
assert!(config_with.compression_config.enable_gzip);
let config_without = GatewayConfig::default().without_compression();
assert!(!config_without.compression_config.enable_gzip);
}
}