add http.rs

This commit is contained in:
break27 2024-06-07 16:24:33 +08:00
parent a9614f934d
commit 312ba329e5
6 changed files with 122 additions and 81 deletions

24
Cargo.lock generated
View File

@ -526,12 +526,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@ -635,17 +629,6 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@ -690,7 +673,6 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"clap", "clap",
"env_logger", "env_logger",
"http",
"httparse", "httparse",
"log", "log",
"native-tls", "native-tls",
@ -735,12 +717,6 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.69" version = "0.3.69"

View File

@ -11,7 +11,6 @@ async-std = { version = "1.12.0", features = ["attributes"] }
base64 = "0.22.1" base64 = "0.22.1"
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
env_logger = "0.11.3" env_logger = "0.11.3"
http = "1.1.0"
httparse = "1.8.0" httparse = "1.8.0"
log = "0.4.21" log = "0.4.21"
native-tls = "0.2.12" native-tls = "0.2.12"

View File

@ -3,6 +3,7 @@ use async_std::net::TcpStream;
use crate::connection::ConnectionBuilder; use crate::connection::ConnectionBuilder;
use crate::error::{Result, Error, BuildError, BuildResult}; use crate::error::{Result, Error, BuildError, BuildResult};
use crate::http;
pub struct AgentBuilder { pub struct AgentBuilder {
filter_url: Option<url::Url>, filter_url: Option<url::Url>,
@ -58,7 +59,7 @@ impl AgentBuilder {
}; };
if let Some(ref url) = self.filter_url { if let Some(ref url) = self.filter_url {
log::info!(target: "builder", "Try downloading rule list from '{}'", url); log::info!(target: "builder", "Try downloading rule list from '{url}'");
let https = native_tls::TlsConnector::new()?; let https = native_tls::TlsConnector::new()?;
let client = ureq::AgentBuilder::new() let client = ureq::AgentBuilder::new()
@ -68,9 +69,9 @@ impl AgentBuilder {
.build(); .build();
let resp = client.get(url.as_str()).call()?; let resp = client.get(url.as_str()).call()?;
let text = resp.into_string()?; let text = resp.into_string()?;
let kbs = text.len() as f32 / 1000f32; let len = text.len() as f32 / 1000f32;
log::info!(target: "builder", "Successfully downloaded data ({}/kB transmitted)", kbs); log::info!(target: "builder", "Successfully downloaded data ({len}/kB transmitted)");
ruleset = Some(self.build_rules(text)?); ruleset = Some(self.build_rules(text)?);
} }
@ -115,25 +116,17 @@ impl Agent {
where where
S: Read + Write + Send + Sync + Unpin + 'static S: Read + Write + Send + Sync + Unpin + 'static
{ {
let (request, payload) = self.read(&mut conn)?; let request = self.read(&mut conn)?;
let host = request.host();
let value = request.headers.get("host").unwrap(); log::info!("CLIENT --> {host}");
let mut host = value.to_str()?.to_string();
if ! host.ends_with(char::is_numeric) { if self.check_request_blocked(&request.path) {
// append a port number when without one log::info!("CLIENT --> PROXY --> {host}");
host += ":80";
}
log::info!("CLIENT --> {} ({}/bit request intercepted)",
host, payload.len());
if self.check_request_blocked(&request.uri.to_string()) {
log::info!("CLIENT --> PROXY --> {}", host);
let mut outbound = self.io(self.builder.connect(&host))?; let mut outbound = self.io(self.builder.connect(&host))?;
// forward intercepted request // forward intercepted request
outbound.write_all(&payload).await?; outbound.write_all(request.as_bytes()).await?;
outbound.flush().await?; outbound.flush().await?;
log::info!("CLIENT <-> PROXY (connection established)"); log::info!("CLIENT <-> PROXY (connection established)");
@ -173,7 +166,7 @@ impl Agent {
Ok(()) Ok(())
} }
fn read<S>(&self, conn: &mut S) -> Result<(http::request::Parts, Vec<u8>)> fn read<S>(&self, conn: &mut S) -> Result<http::Request>
where where
S: Read + Write + Send + Unpin + 'static S: Read + Write + Send + Unpin + 'static
{ {
@ -187,33 +180,31 @@ impl Agent {
let payload = buf[..offset].to_vec(); let payload = buf[..offset].to_vec();
let method = match request.method { let method = match request.method {
Some(x) => x, Some(x) => x.parse::<crate::http::Method>().unwrap(),
None => return Err(Error::BadRequest("METHOD".to_string())) None => return Err(Error::BadRequest("METHOD".to_string()))
}; };
let path = match request.path { let mut path = match request.path {
Some(x) => { Some(x) => x.to_string(),
let mut text = x.to_string();
if text.find("://").is_none() {
// in case of an cannot-be-a-base url
// find a port number, if any
let port = text
.rfind(":")
.and_then(|x| text.get(x + 1..));
let scheme = match port {
Some("443") => "https",
Some("21") => "ftp",
Some("80") | _ => "http",
};
text = format!("{}://{}", scheme, text);
}
text.parse::<http::Uri>()?
},
None => return Err(Error::BadRequest("PATH".to_string())) None => return Err(Error::BadRequest("PATH".to_string()))
}; };
if path.find("://").is_none() {
// in case of an cannot-be-a-base url
// find a port number, if any
let port = path
.rfind(":")
.and_then(|x| path.get(x + 1..));
let scheme = match port {
Some("443") => "https",
Some("21") => "ftp",
Some("80") | _ => "http",
};
path = format!("{}://{}", scheme, path);
}
let version = match request.version { let version = match request.version {
Some(3) => http::Version::HTTP_3, Some(3) => http::Version::HTTP_3,
Some(2) => http::Version::HTTP_2, Some(2) => http::Version::HTTP_2,
@ -223,21 +214,26 @@ impl Agent {
None => return Err(Error::BadRequest("VERSION".to_string())) None => return Err(Error::BadRequest("VERSION".to_string()))
}; };
let (mut parts, _) = http::Request::builder() let mut host = headers.iter()
.method(method) .find_map(|x: _| (x.name == "Host").then_some(x.value))
.uri(path) .map(|x| std::str::from_utf8(x))
.version(version) .ok_or(Error::BadRequest("Host".to_string()))??
.body(())? .to_string();
.into_parts();
for (k, v) in headers.map(|x: _| (x.name, x.value)) { if host.find(":").is_none() {
if k.is_empty() { break } // append a port number when without one
let key = k.parse::<http::HeaderName>()?; host += ":80";
let value = std::str::from_utf8(v)?.parse::<http::HeaderValue>()?;
parts.headers.insert(key, value);
} }
Ok((parts, payload)) let request = crate::http::Request {
method,
path,
version,
host,
payload: payload.into(),
};
Ok(request)
} }
fn check_request_blocked(&self, url: &str) -> bool { fn check_request_blocked(&self, url: &str) -> bool {

View File

@ -30,11 +30,6 @@ impl_error! {
BadRequest("Missing part '{}'"), BadRequest("Missing part '{}'"),
Io(std::io::Error), Io(std::io::Error),
Parse(httparse::Error), Parse(httparse::Error),
Http(http::Error),
Uri(http::uri::InvalidUri),
HeaderName(http::header::InvalidHeaderName),
HeaderValue(http::header::InvalidHeaderValue),
ToStr(http::header::ToStrError),
Utf8(std::str::Utf8Error), Utf8(std::str::Utf8Error),
Timeout(async_std::future::TimeoutError), Timeout(async_std::future::TimeoutError),
} }

74
src/http.rs Normal file
View File

@ -0,0 +1,74 @@
macro_rules! impl_http {
(pub struct $name:ident($inner:ident) { $($variant:ident = $value:literal,)* }) => {
#[derive(PartialEq)]
pub struct $name($inner);
#[derive(PartialEq)]
#[allow(non_camel_case_types)]
enum $inner{ $($variant,)* }
impl $name {
$(
pub const $variant: Self = Self($inner::$variant);
)*
}
impl ToString for $name {
fn to_string(&self) -> String {
match self.0 {
$($inner::$variant => $value.to_string(),)*
}
}
}
impl std::str::FromStr for $name {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
$($value => Ok(Self::$variant),)*
_ => Err(())
}
}
}
};
}
impl_http! {
pub struct Method(InnerMethod) {
OPTIONS = "OPTIONS",
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
HEAD = "HEAD",
TRACE = "TRACE",
CONNECT = "CONNECT",
PATCH = "PATCH",
}
}
impl_http! {
pub struct Version(InnerVersion) {
HTTP_09 = "HTTP/0.9",
HTTP_10 = "HTTP/1.0",
HTTP_11 = "HTTP/1.1",
HTTP_2 = "HTTP/2.0",
HTTP_3 = "HTTP/3.0",
}
}
pub struct Request {
pub method: Method,
pub path: String,
pub version: Version,
pub(crate) host: String,
pub(crate) payload: Box<[u8]>,
}
impl Request {
pub fn as_bytes(&self) -> &[u8] {
&self.payload
}
pub fn host(&self) -> &str {
&self.host
}
}

View File

@ -1,5 +1,6 @@
use clap::Parser; use clap::Parser;
mod http;
pub mod agent; pub mod agent;
pub mod error; pub mod error;
pub mod connection; pub mod connection;