docs, gurt:// <a> url

This commit is contained in:
Face
2025-08-15 13:52:01 +03:00
parent c117e602fe
commit 5dae5a4868
25 changed files with 1640 additions and 390 deletions

View File

@@ -224,6 +224,16 @@ dependencies = [
"cc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -365,6 +375,7 @@ dependencies = [
"base64",
"chrono",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"serde",
"serde_json",
@@ -686,6 +697,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "overload"
version = "0.1.1"
@@ -830,6 +847,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -872,6 +901,38 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "security-framework"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.219"

View File

@@ -30,6 +30,7 @@ chrono = { version = "0.4", features = ["serde"] }
tokio-rustls = "0.26"
rustls = "0.23"
rustls-pemfile = "2.0"
rustls-native-certs = "0.8"
base64 = "0.22"
url = "2.5"

View File

@@ -2,15 +2,18 @@ use crate::{
GurtError, Result, GurtRequest, GurtResponse,
protocol::{DEFAULT_PORT, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, DEFAULT_HANDSHAKE_TIMEOUT, BODY_SEPARATOR},
message::GurtMethod,
crypto::GURT_ALPN,
};
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{timeout, Duration};
use tokio_rustls::{TlsConnector, rustls::{ClientConfig as TlsClientConfig, RootCertStore, pki_types::ServerName}};
use std::sync::Arc;
use url::Url;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub struct GurtClientConfig {
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub handshake_timeout: Duration,
@@ -18,7 +21,7 @@ pub struct ClientConfig {
pub max_redirects: usize,
}
impl Default for ClientConfig {
impl Default for GurtClientConfig {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT),
@@ -30,29 +33,55 @@ impl Default for ClientConfig {
}
}
#[derive(Debug)]
enum Connection {
Plain(TcpStream),
Tls(tokio_rustls::client::TlsStream<TcpStream>),
}
impl Connection {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self {
Connection::Plain(stream) => stream.read(buf).await.map_err(|e| GurtError::connection(e.to_string())),
Connection::Tls(stream) => stream.read(buf).await.map_err(|e| GurtError::connection(e.to_string())),
}
}
async fn write_all(&mut self, buf: &[u8]) -> Result<()> {
match self {
Connection::Plain(stream) => stream.write_all(buf).await.map_err(|e| GurtError::connection(e.to_string())),
Connection::Tls(stream) => stream.write_all(buf).await.map_err(|e| GurtError::connection(e.to_string())),
}
}
}
#[derive(Debug)]
struct PooledConnection {
stream: TcpStream,
connection: Connection,
}
impl PooledConnection {
fn new(stream: TcpStream) -> Self {
Self { stream }
Self { connection: Connection::Plain(stream) }
}
fn with_tls(stream: tokio_rustls::client::TlsStream<TcpStream>) -> Self {
Self { connection: Connection::Tls(stream) }
}
}
pub struct GurtClient {
config: ClientConfig,
config: GurtClientConfig,
}
impl GurtClient {
pub fn new() -> Self {
Self {
config: ClientConfig::default(),
config: GurtClientConfig::default(),
}
}
pub fn with_config(config: ClientConfig) -> Self {
pub fn with_config(config: GurtClientConfig) -> Self {
Self {
config,
}
@@ -71,7 +100,7 @@ impl GurtClient {
Ok(conn)
}
async fn read_response_data(&self, stream: &mut TcpStream) -> Result<Vec<u8>> {
async fn read_response_data(&self, conn: &mut PooledConnection) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut temp_buffer = [0u8; 8192];
@@ -82,7 +111,7 @@ impl GurtClient {
return Err(GurtError::timeout("Response timeout"));
}
let bytes_read = stream.read(&mut temp_buffer).await?;
let bytes_read = conn.connection.read(&mut temp_buffer).await?;
if bytes_read == 0 {
break; // Connection closed
}
@@ -106,17 +135,92 @@ impl GurtClient {
}
}
async fn perform_handshake(&self, host: &str, port: u16) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
debug!("Starting GURT handshake with {}:{}", host, port);
let mut plain_conn = self.create_connection(host, port).await?;
let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string())
.with_header("Host", host)
.with_header("User-Agent", &self.config.user_agent);
let handshake_data = handshake_request.to_string();
plain_conn.connection.write_all(handshake_data.as_bytes()).await?;
let handshake_response_bytes = timeout(
self.config.handshake_timeout,
self.read_response_data(&mut plain_conn)
).await
.map_err(|_| GurtError::timeout("Handshake timeout"))??;
let handshake_response = GurtResponse::parse_bytes(&handshake_response_bytes)?;
if handshake_response.status_code != 101 {
return Err(GurtError::protocol(format!("Handshake failed: {} {}",
handshake_response.status_code,
handshake_response.status_message)));
}
let tcp_stream = match plain_conn.connection {
Connection::Plain(stream) => stream,
_ => return Err(GurtError::protocol("Expected plain connection for handshake")),
};
self.upgrade_to_tls(tcp_stream, host).await
}
async fn upgrade_to_tls(&self, stream: TcpStream, host: &str) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
debug!("Upgrading connection to TLS for {}", host);
let mut root_store = RootCertStore::empty();
let cert_result = rustls_native_certs::load_native_certs();
let mut added = 0;
for cert in cert_result.certs {
if root_store.add(cert).is_ok() {
added += 1;
}
}
if added == 0 {
return Err(GurtError::crypto("No valid system certificates found".to_string()));
}
let mut client_config = TlsClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
client_config.alpn_protocols = vec![GURT_ALPN.to_vec()];
let connector = TlsConnector::from(Arc::new(client_config));
let server_name = match host {
"127.0.0.1" => "localhost",
"localhost" => "localhost",
_ => host
};
let domain = ServerName::try_from(server_name.to_string())
.map_err(|e| GurtError::crypto(format!("Invalid server name '{}': {}", server_name, e)))?;
let tls_stream = connector.connect(domain, stream).await
.map_err(|e| GurtError::crypto(format!("TLS handshake failed: {}", e)))?;
debug!("TLS connection established with {}", host);
Ok(tls_stream)
}
async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest) -> Result<GurtResponse> {
debug!("Sending {} {} to {}:{}", request.method, request.path, host, port);
let mut conn = self.create_connection(host, port).await?;
let tls_stream = self.perform_handshake(host, port).await?;
let mut conn = PooledConnection::with_tls(tls_stream);
let request_data = request.to_string();
conn.stream.write_all(request_data.as_bytes()).await?;
conn.connection.write_all(request_data.as_bytes()).await?;
let response_bytes = timeout(
self.config.request_timeout,
self.read_response_data(&mut conn.stream)
self.read_response_data(&mut conn)
).await
.map_err(|_| GurtError::timeout("Request timeout"))??;

View File

@@ -10,7 +10,7 @@ pub use message::{GurtMessage, GurtRequest, GurtResponse, GurtMethod};
pub use protocol::{GurtStatusCode, GURT_VERSION, DEFAULT_PORT};
pub use crypto::{CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION};
pub use server::{GurtServer, GurtHandler, ServerContext, Route};
pub use client::{GurtClient, ClientConfig};
pub use client::{GurtClient, GurtClientConfig};
pub mod prelude {
pub use crate::{
@@ -19,6 +19,6 @@ pub mod prelude {
GURT_VERSION, DEFAULT_PORT,
CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION,
GurtServer, GurtHandler, ServerContext, Route,
GurtClient, ClientConfig,
GurtClient, GurtClientConfig,
};
}

View File

@@ -90,7 +90,7 @@ impl GurtRequest {
self.headers.get(&key.to_lowercase())
}
pub fn body_as_string(&self) -> Result<String> {
pub fn text(&self) -> Result<String> {
std::str::from_utf8(&self.body)
.map(|s| s.to_string())
.map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e)))
@@ -283,7 +283,7 @@ impl GurtResponse {
self.headers.get(&key.to_lowercase())
}
pub fn body_as_string(&self) -> Result<String> {
pub fn text(&self) -> Result<String> {
std::str::from_utf8(&self.body)
.map(|s| s.to_owned())
.map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e)))
@@ -524,7 +524,7 @@ mod tests {
assert_eq!(request.version, GURT_VERSION.to_string());
assert_eq!(request.header("host"), Some(&"example.com".to_string()));
assert_eq!(request.header("accept"), Some(&"text/html".to_string()));
assert_eq!(request.body_as_string().unwrap(), "test body");
assert_eq!(request.text().unwrap(), "test body");
}
#[test]
@@ -536,7 +536,7 @@ mod tests {
assert_eq!(response.status_code, 200);
assert_eq!(response.status_message, "OK");
assert_eq!(response.header("content-type"), Some(&"text/html".to_string()));
assert_eq!(response.body_as_string().unwrap(), "<html></html>");
assert_eq!(response.text().unwrap(), "<html></html>");
}
#[test]

View File

@@ -47,8 +47,8 @@ impl ServerContext {
&self.request.body
}
pub fn body_as_string(&self) -> Result<String> {
self.request.body_as_string()
pub fn text(&self) -> Result<String> {
self.request.text()
}
pub fn header(&self, key: &str) -> Option<&String> {
@@ -387,7 +387,7 @@ impl GurtServer {
let response = GurtResponse::new(GurtStatusCode::SwitchingProtocols)
.with_header("GURT-Version", crate::GURT_VERSION.to_string())
.with_header("Encryption", TLS_VERSION)
.with_header("ALPN", std::str::from_utf8(GURT_ALPN).unwrap_or("gurt/1.0"));
.with_header("ALPN", std::str::from_utf8(GURT_ALPN).unwrap_or("GURT/1.0"));
let response_bytes = response.to_string().into_bytes();
stream.write_all(&response_bytes).await?;