docs, gurt:// <a> url
This commit is contained in:
7
protocol/README.md
Normal file
7
protocol/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
The implementation of the GURT protocol.
|
||||
|
||||
- [cli](./cli) - Gurty, the command-line tool for managing GURT servers
|
||||
- [library](./library) - client/server GURT protocol library for Rust (the core, used in Flumi)
|
||||
- [gdextension](./gdextension) - the Godot bindings for the GURT protocol (used in Flumi)
|
||||
|
||||
For the full spec, see the [Gurted Documentation](https://docs.gurted.com).
|
||||
61
protocol/cli/Cargo.lock
generated
61
protocol/cli/Cargo.lock
generated
@@ -299,6 +299,16 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -395,6 +405,7 @@ dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -772,6 +783,12 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[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"
|
||||
@@ -948,6 +965,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"
|
||||
@@ -990,12 +1019,44 @@ 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 = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -95,11 +95,10 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
|
||||
let server = server
|
||||
.get("/", {
|
||||
let base_dir = base_dir.clone();
|
||||
move |ctx| {
|
||||
let client_ip = ctx.client_ip();
|
||||
move |_| {
|
||||
let base_dir = base_dir.clone();
|
||||
async move {
|
||||
// Try to serve index.html if it exists, otherwise show server info
|
||||
// Try to serve index.html if it exists
|
||||
let index_path = base_dir.join("index.html");
|
||||
|
||||
if index_path.exists() && index_path.is_file() {
|
||||
@@ -110,36 +109,54 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
|
||||
.with_string_body(content));
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall through to default page
|
||||
// Fall through to directory listing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default server info page
|
||||
Ok(GurtResponse::ok()
|
||||
.with_header("Content-Type", "text/html")
|
||||
.with_string_body(format!(r#"
|
||||
// No index.html found, show directory listing
|
||||
match std::fs::read_dir(base_dir.as_ref()) {
|
||||
Ok(entries) => {
|
||||
let mut listing = String::from(r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GURT Protocol Server</title>
|
||||
<title>Directory Listing</title>
|
||||
<style>
|
||||
body {{ font-sans m-[30px] bg-[#f5f5f5] }}
|
||||
.header {{ text-[#0066cc] }}
|
||||
.status {{ text-[#28a745] font-bold }}
|
||||
body { font-sans m-[40px] }
|
||||
.dir { font-bold text-[#0066cc] }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="header">Welcome to the GURT Protocol!</h1>
|
||||
<p class="status">This server is successfully running. We couldn't find index.html though :(</p>
|
||||
<p>Protocol: <strong>GURT/{}</strong></p>
|
||||
<p>Client IP: <strong>{}</strong></p>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
gurt::GURT_VERSION,
|
||||
client_ip,
|
||||
)))
|
||||
<h1>Directory Listing</h1>
|
||||
<div style="flex flex-col gap-2">
|
||||
"#);
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy();
|
||||
let is_dir = entry.path().is_dir();
|
||||
let display_name = if is_dir { format!("{}/", name) } else { name.to_string() };
|
||||
let class = if is_dir { "style=\"dir\"" } else { "" };
|
||||
|
||||
listing.push_str(&format!(
|
||||
r#" <a {} href="/{}">{}</a>"#,
|
||||
class, name, display_name
|
||||
));
|
||||
listing.push('\n');
|
||||
}
|
||||
|
||||
listing.push_str("</div></body>\n</html>");
|
||||
|
||||
Ok(GurtResponse::ok()
|
||||
.with_header("Content-Type", "text/html")
|
||||
.with_string_body(listing))
|
||||
}
|
||||
Err(_) => {
|
||||
Ok(GurtResponse::internal_server_error()
|
||||
.with_header("Content-Type", "text/plain")
|
||||
.with_string_body("Failed to read directory"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -214,7 +231,6 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
|
||||
<title>Directory Listing</title>
|
||||
<style>
|
||||
body { font-sans m-[40px] }
|
||||
.file { my-1 }
|
||||
.dir { font-bold text-[#0066cc] }
|
||||
</style>
|
||||
</head>
|
||||
@@ -228,10 +244,10 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
|
||||
let name = file_name.to_string_lossy();
|
||||
let is_dir = entry.path().is_dir();
|
||||
let display_name = if is_dir { format!("{}/", name) } else { name.to_string() };
|
||||
let class = if is_dir { "file dir" } else { "file" };
|
||||
let class = if is_dir { "style=\"dir\"" } else { "" };
|
||||
|
||||
listing.push_str(&format!(
|
||||
r#" <a style={} href="/{}">{}</a>"#,
|
||||
r#" <a {} href="{}">{}</a>"#,
|
||||
class, name, display_name
|
||||
));
|
||||
listing.push('\n');
|
||||
|
||||
@@ -16,17 +16,7 @@ gurt = { path = "../library" }
|
||||
|
||||
godot = "0.1"
|
||||
|
||||
tokio = { version = "1.0", features = [
|
||||
"net",
|
||||
"io-util",
|
||||
"rt",
|
||||
"time"
|
||||
] }
|
||||
tokio-rustls = "0.26"
|
||||
rustls-native-certs = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tokio = { version = "1.0", features = ["rt"] }
|
||||
url = "2.5"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use godot::prelude::*;
|
||||
use gurt::prelude::*;
|
||||
use gurt::{GurtMethod, GurtRequest};
|
||||
use gurt::{GurtMethod, GurtClientConfig};
|
||||
use tokio::runtime::Runtime;
|
||||
use std::sync::Arc;
|
||||
use std::cell::RefCell;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
struct GurtGodotExtension;
|
||||
|
||||
@@ -109,7 +107,7 @@ impl GurtProtocolClient {
|
||||
}
|
||||
};
|
||||
|
||||
let mut config = ClientConfig::default();
|
||||
let mut config = GurtClientConfig::default();
|
||||
config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64);
|
||||
|
||||
let client = GurtClient::with_config(config);
|
||||
@@ -168,7 +166,31 @@ impl GurtProtocolClient {
|
||||
}
|
||||
};
|
||||
|
||||
let response = match runtime.block_on(self.gurt_request_with_handshake(host, port, method, path)) {
|
||||
let client_binding = self.client.borrow();
|
||||
let client = match client_binding.as_ref() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
godot_print!("No client available");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let url = format!("gurt://{}:{}{}", host, port, path);
|
||||
let response = match runtime.block_on(async {
|
||||
match method {
|
||||
GurtMethod::GET => client.get(&url).await,
|
||||
GurtMethod::POST => client.post(&url, "").await,
|
||||
GurtMethod::PUT => client.put(&url, "").await,
|
||||
GurtMethod::DELETE => client.delete(&url).await,
|
||||
GurtMethod::HEAD => client.head(&url).await,
|
||||
GurtMethod::OPTIONS => client.options(&url).await,
|
||||
GurtMethod::PATCH => client.patch(&url, "").await,
|
||||
_ => {
|
||||
godot_print!("Unsupported method: {:?}", method);
|
||||
return Err(GurtError::invalid_message("Unsupported method"));
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
godot_print!("GURT request failed: {}", e);
|
||||
@@ -179,145 +201,6 @@ impl GurtProtocolClient {
|
||||
Some(self.convert_response(response))
|
||||
}
|
||||
|
||||
async fn gurt_request_with_handshake(&self, host: &str, port: u16, method: GurtMethod, path: &str) -> gurt::Result<GurtResponse> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let mut stream = TcpStream::connect(&addr).await?;
|
||||
|
||||
let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string())
|
||||
.with_header("Host", host)
|
||||
.with_header("User-Agent", &format!("GURT-Client/{}", gurt::GURT_VERSION));
|
||||
|
||||
let handshake_data = handshake_request.to_string();
|
||||
stream.write_all(handshake_data.as_bytes()).await?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut temp_buffer = [0u8; 8192];
|
||||
|
||||
loop {
|
||||
let bytes_read = stream.read(&mut temp_buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
|
||||
|
||||
let separator = b"\r\n\r\n";
|
||||
if buffer.windows(separator.len()).any(|w| w == separator) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let handshake_response = GurtResponse::parse_bytes(&buffer)?;
|
||||
|
||||
if handshake_response.status_code != 101 {
|
||||
return Err(GurtError::handshake(format!("Handshake failed: {} {}",
|
||||
handshake_response.status_code,
|
||||
handshake_response.status_message)));
|
||||
}
|
||||
|
||||
let tls_stream = self.create_secure_tls_connection(stream, host).await?;
|
||||
let (mut reader, mut writer) = tokio::io::split(tls_stream);
|
||||
|
||||
let actual_request = GurtRequest::new(method, path.to_string())
|
||||
.with_header("Host", host)
|
||||
.with_header("User-Agent", &format!("GURT-Client/{}", gurt::GURT_VERSION))
|
||||
.with_header("Accept", "*/*");
|
||||
|
||||
let request_data = actual_request.to_string();
|
||||
writer.write_all(request_data.as_bytes()).await?;
|
||||
|
||||
let mut response_buffer = Vec::new();
|
||||
let mut temp_buf = [0u8; 8192];
|
||||
|
||||
let mut headers_complete = false;
|
||||
while !headers_complete {
|
||||
let bytes_read = reader.read(&mut temp_buf).await?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
response_buffer.extend_from_slice(&temp_buf[..bytes_read]);
|
||||
|
||||
let separator = b"\r\n\r\n";
|
||||
if response_buffer.windows(separator.len()).any(|w| w == separator) {
|
||||
headers_complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
let response = GurtResponse::parse_bytes(&response_buffer)?;
|
||||
let content_length = response.header("content-length")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let separator_pos = response_buffer.windows(4).position(|w| w == b"\r\n\r\n").unwrap_or(0) + 4;
|
||||
let current_body_len = response_buffer.len().saturating_sub(separator_pos);
|
||||
|
||||
if content_length > current_body_len {
|
||||
let remaining = content_length - current_body_len;
|
||||
let mut remaining_buffer = vec![0u8; remaining];
|
||||
match reader.read_exact(&mut remaining_buffer).await {
|
||||
Ok(_) => {
|
||||
response_buffer.extend_from_slice(&remaining_buffer);
|
||||
}
|
||||
Err(e) => {
|
||||
godot_error!("Failed to read remaining {} bytes: {}", remaining, e);
|
||||
// Don't fail completely, try to parse what we have
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(reader);
|
||||
drop(writer);
|
||||
|
||||
let final_response = GurtResponse::parse_bytes(&response_buffer)?;
|
||||
|
||||
Ok(final_response)
|
||||
}
|
||||
|
||||
async fn create_secure_tls_connection(&self, stream: tokio::net::TcpStream, host: &str) -> gurt::Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>> {
|
||||
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut root_store = RootCertStore::empty();
|
||||
|
||||
let cert_result = rustls_native_certs::load_native_certs();
|
||||
let mut system_cert_count = 0;
|
||||
for cert in cert_result.certs {
|
||||
if root_store.add(cert).is_ok() {
|
||||
system_cert_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if system_cert_count <= 0 {
|
||||
godot_error!("No system certificates found. TLS connections will fail.");
|
||||
}
|
||||
|
||||
let mut client_config = ClientConfig::builder()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
client_config.alpn_protocols = vec![gurt::crypto::GURT_ALPN.to_vec()];
|
||||
|
||||
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
|
||||
|
||||
let server_name = match host {
|
||||
"127.0.0.1" => "localhost",
|
||||
"localhost" => "localhost",
|
||||
_ => host
|
||||
};
|
||||
|
||||
let domain = tokio_rustls::rustls::pki_types::ServerName::try_from(server_name.to_string())
|
||||
.map_err(|e| GurtError::connection(format!("Invalid server name '{}': {}", server_name, e)))?;
|
||||
|
||||
match connector.connect(domain, stream).await {
|
||||
Ok(tls_stream) => {
|
||||
Ok(tls_stream)
|
||||
}
|
||||
Err(e) => {
|
||||
godot_error!("TLS handshake failed: {}", e);
|
||||
Err(GurtError::connection(format!("TLS handshake failed: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn disconnect(&mut self) {
|
||||
*self.client.borrow_mut() = None;
|
||||
|
||||
61
protocol/library/Cargo.lock
generated
61
protocol/library/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"))??;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user