geth/crates/geth-pipe/src/lib.rs

110 lines
3.4 KiB
Rust
Raw Normal View History

2026-05-17 20:17:26 +02:00
use geth_types::{PipeId, ResourceId, UnixMillis};
2026-05-15 15:08:20 +02:00
use serde::{Deserialize, Serialize};
2026-05-21 01:12:01 +02:00
use std::net::SocketAddr;
2026-05-15 15:08:20 +02:00
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PipeResource {
pub id: PipeId,
pub resource: ResourceId,
pub name: String,
}
2026-05-17 20:17:26 +02:00
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PipeListener {
pub id: PipeId,
pub name: String,
pub listened_at: UnixMillis,
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PipeConnection {
pub target: String,
pub connected_at: UnixMillis,
pub local_listener_found: bool,
pub note: String,
}
2026-05-20 13:57:14 +02:00
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PipeMessage {
pub pipe: String,
pub data_base64: String,
pub received_at: UnixMillis,
pub source_node: Option<String>,
pub note: String,
}
2026-05-17 20:17:26 +02:00
#[derive(Debug, thiserror::Error)]
pub enum PipeError {
#[error("invalid pipe name or target: {0}")]
InvalidName(String),
2026-05-21 01:12:01 +02:00
#[error("invalid TCP address: {0}")]
InvalidTcpAddress(String),
#[error("TCP forwarding addresses must be loopback addresses: {0}")]
NonLoopbackTcpAddress(String),
2026-05-17 20:17:26 +02:00
}
pub fn validate_pipe_name(name: &str) -> Result<(), PipeError> {
if name.is_empty()
|| !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':'))
{
return Err(PipeError::InvalidName(name.to_owned()));
}
Ok(())
}
2026-05-21 01:12:01 +02:00
pub fn validate_tcp_forward_listen_addr(addr: &str) -> Result<SocketAddr, PipeError> {
let addr = addr
.parse::<SocketAddr>()
.map_err(|_| PipeError::InvalidTcpAddress(addr.to_owned()))?;
if !addr.ip().is_loopback() {
return Err(PipeError::NonLoopbackTcpAddress(addr.to_string()));
}
Ok(addr)
}
pub fn validate_tcp_forward_target_addr(addr: &str) -> Result<SocketAddr, PipeError> {
let addr = addr
.parse::<SocketAddr>()
.map_err(|_| PipeError::InvalidTcpAddress(addr.to_owned()))?;
if !addr.ip().is_loopback() {
return Err(PipeError::NonLoopbackTcpAddress(addr.to_string()));
}
Ok(addr)
}
2026-05-15 15:08:20 +02:00
#[must_use]
pub fn pipe_roadmap() -> &'static str {
"future pipes are authorized Iroh bidirectional streams for stdin/stdout and forwarding"
}
2026-05-17 20:17:26 +02:00
#[must_use]
pub fn local_pipe_runtime_note() -> &'static str {
2026-05-20 13:57:14 +02:00
"daemon-lifetime pipe runtime; byte messages are buffered locally and remote writes use the Iroh pipe ALPN"
2026-05-17 20:17:26 +02:00
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pipe_name_validation_rejects_paths_and_empty_names() {
assert!(validate_pipe_name("inbox").is_ok());
assert!(validate_pipe_name("node:laptop").is_ok());
assert!(validate_pipe_name("").is_err());
assert!(validate_pipe_name("../inbox").is_err());
assert!(validate_pipe_name("inbox/main").is_err());
assert!(validate_pipe_name("inbox main").is_err());
}
2026-05-21 01:12:01 +02:00
#[test]
fn tcp_forward_addresses_must_be_explicit_loopback_socket_addrs() {
assert!(validate_tcp_forward_listen_addr("127.0.0.1:9000").is_ok());
assert!(validate_tcp_forward_target_addr("[::1]:22").is_ok());
assert!(validate_tcp_forward_listen_addr("localhost:9000").is_err());
assert!(validate_tcp_forward_target_addr("0.0.0.0:22").is_err());
assert!(validate_tcp_forward_target_addr("192.0.2.10:22").is_err());
}
2026-05-17 20:17:26 +02:00
}