133 lines
4.3 KiB
Rust
133 lines
4.3 KiB
Rust
use geth_types::{PipeId, ResourceId, UnixMillis};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::net::SocketAddr;
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct PipeResource {
|
|
pub id: PipeId,
|
|
pub resource: ResourceId,
|
|
pub name: String,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum PipeError {
|
|
#[error("invalid pipe name or target: {0}")]
|
|
InvalidName(String),
|
|
#[error("invalid TCP address: {0}")]
|
|
InvalidTcpAddress(String),
|
|
#[error("TCP forwarding addresses must be loopback addresses: {0}")]
|
|
NonLoopbackTcpAddress(String),
|
|
#[error("invalid Unix socket path: {0}")]
|
|
InvalidUnixSocketPath(String),
|
|
}
|
|
|
|
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(())
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
pub fn validate_unix_forward_path(path: &Path) -> Result<PathBuf, PipeError> {
|
|
if path.as_os_str().is_empty() || !path.is_absolute() {
|
|
return Err(PipeError::InvalidUnixSocketPath(path.display().to_string()));
|
|
}
|
|
if path
|
|
.components()
|
|
.any(|component| matches!(component, Component::ParentDir))
|
|
{
|
|
return Err(PipeError::InvalidUnixSocketPath(path.display().to_string()));
|
|
}
|
|
Ok(path.to_path_buf())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn pipe_roadmap() -> &'static str {
|
|
"future pipes are authorized Iroh bidirectional streams for stdin/stdout and forwarding"
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn local_pipe_runtime_note() -> &'static str {
|
|
"daemon-lifetime pipe runtime; byte messages are buffered locally and remote writes use the Iroh pipe ALPN"
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
|
|
#[test]
|
|
fn unix_forward_paths_must_be_absolute_without_parent_components() {
|
|
assert!(validate_unix_forward_path(Path::new("/tmp/geth.sock")).is_ok());
|
|
assert!(validate_unix_forward_path(Path::new("relative.sock")).is_err());
|
|
assert!(validate_unix_forward_path(Path::new("/tmp/../geth.sock")).is_err());
|
|
}
|
|
}
|