Skip to content

Commit c32fdfc

Browse files
committed
vey-proxy: add http connect-udp support to proxy_http escapers
1 parent adc7a17 commit c32fdfc

92 files changed

Lines changed: 2751 additions & 842 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/vey-http/src/connect/client.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub async fn http_connect_to<S>(
1717
where
1818
S: AsyncBufRead + AsyncWrite + Unpin,
1919
{
20-
let mut req = HttpConnectRequest::new(addr, &[]);
20+
let mut req = HttpConnectRequest::new(&[]);
2121

2222
match auth {
2323
HttpAuth::None => {}
@@ -27,7 +27,7 @@ where
2727
}
2828
}
2929

30-
req.send(buf_stream)
30+
req.send(addr, buf_stream)
3131
.await
3232
.map_err(HttpConnectError::WriteFailed)?;
3333

@@ -50,7 +50,6 @@ mod tests {
5050
async fn http_connect_to_success_no_auth() {
5151
let stream = Builder::new()
5252
.write(b"CONNECT example.com:8080 HTTP/1.1\r\n")
53-
.write(b"Host: example.com:8080\r\n")
5453
.write(b"Connection: keep-alive\r\n")
5554
.write(b"\r\n")
5655
.read(b"HTTP/1.1 200 OK\r\n\r\n")
@@ -68,7 +67,6 @@ mod tests {
6867
async fn http_connect_to_success_basic_auth() {
6968
let stream = Builder::new()
7069
.write(b"CONNECT example.com:8080 HTTP/1.1\r\n")
71-
.write(b"Host: example.com:8080\r\n")
7270
.write(b"Connection: keep-alive\r\n")
7371
.write(b"Proxy-Authorization: Basic dXNlcjpwYXNz\r\n")
7472
.write(b"\r\n")
@@ -108,7 +106,6 @@ mod tests {
108106
async fn http_connect_to_read_failed() {
109107
let stream = Builder::new()
110108
.write(b"CONNECT example.com:8080 HTTP/1.1\r\n")
111-
.write(b"Host: example.com:8080\r\n")
112109
.write(b"Connection: keep-alive\r\n")
113110
.write(b"\r\n")
114111
.read_error(io::Error::new(
@@ -131,7 +128,6 @@ mod tests {
131128
async fn http_connect_to_unexpected_status_code() {
132129
let stream = Builder::new()
133130
.write(b"CONNECT example.com:8080 HTTP/1.1\r\n")
134-
.write(b"Host: example.com:8080\r\n")
135131
.write(b"Connection: keep-alive\r\n")
136132
.write(b"\r\n")
137133
.read(b"HTTP/1.1 404 Not Found\r\n\r\n")
Lines changed: 66 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
/*
22
* SPDX-License-Identifier: Apache-2.0
33
* SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates.
4+
* SPDX-FileCopyrightText: 2026 VEY-OSS Developers.
45
*/
56

6-
use std::io;
7+
use std::io::{self, Write};
78

8-
use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter};
9+
use tokio::io::{AsyncWrite, AsyncWriteExt};
910

1011
use vey_types::net::UpstreamAddr;
1112

1213
/// the extra header lines should end with \r\n
1314
pub struct HttpConnectRequest<'a> {
14-
host: &'a UpstreamAddr,
1515
static_headers: &'a [String],
1616
dyn_headers: Vec<String>,
1717
}
1818

1919
impl<'a> HttpConnectRequest<'a> {
20-
pub fn new(host: &'a UpstreamAddr, static_headers: &'a [String]) -> Self {
20+
pub fn new(static_headers: &'a [String]) -> Self {
2121
HttpConnectRequest {
22-
host,
2322
static_headers,
2423
dyn_headers: Vec::new(),
2524
}
@@ -30,28 +29,23 @@ impl<'a> HttpConnectRequest<'a> {
3029
self.dyn_headers.push(line);
3130
}
3231

33-
/// the extra header lines should end with \r\n
34-
pub async fn send<W>(&'a self, writer: &mut W) -> io::Result<()>
32+
pub async fn send<W>(&'a self, target: &UpstreamAddr, writer: &mut W) -> io::Result<()>
3533
where
3634
W: AsyncWrite + Unpin,
3735
{
38-
let mut buf_writer = BufWriter::new(writer);
39-
buf_writer
40-
.write_all(format!("CONNECT {} HTTP/1.1\r\n", self.host).as_bytes())
41-
.await?;
42-
buf_writer
43-
.write_all(format!("Host: {}\r\n", self.host).as_bytes())
44-
.await?;
45-
buf_writer.write_all(b"Connection: keep-alive\r\n").await?;
36+
let mut buf = Vec::with_capacity(256);
37+
write!(&mut buf, "CONNECT {target} HTTP/1.1\r\n")?;
38+
buf.extend_from_slice(b"Connection: keep-alive\r\n");
4639
for line in self.static_headers {
4740
debug_assert!(line.ends_with("\r\n"));
48-
buf_writer.write_all(line.as_bytes()).await?;
41+
buf.extend_from_slice(line.as_bytes());
4942
}
5043
for line in &self.dyn_headers {
51-
buf_writer.write_all(line.as_bytes()).await?;
44+
buf.extend_from_slice(line.as_bytes());
5245
}
53-
buf_writer.write_all(b"\r\n").await?;
54-
buf_writer.flush().await
46+
buf.extend_from_slice(b"\r\n");
47+
writer.write_all(&buf).await?;
48+
writer.flush().await
5549
}
5650
}
5751

@@ -60,166 +54,104 @@ mod tests {
6054
use super::*;
6155
use std::str::FromStr;
6256

63-
// Helper function to create a test UpstreamAddr
64-
fn create_test_upstream_addr() -> UpstreamAddr {
57+
fn test_upstream_addr() -> UpstreamAddr {
6558
UpstreamAddr::from_str("example.com:8080").unwrap()
6659
}
6760

61+
async fn render_request(request: &HttpConnectRequest<'_>, target: &UpstreamAddr) -> String {
62+
let mut buffer = Vec::new();
63+
request.send(target, &mut buffer).await.unwrap();
64+
String::from_utf8(buffer).unwrap()
65+
}
66+
6867
#[test]
69-
fn new() {
70-
let host = create_test_upstream_addr();
68+
fn new_starts_with_static_headers_only() {
7169
let static_headers = vec![
7270
"User-Agent: test-agent\r\n".to_string(),
7371
"Accept: */*\r\n".to_string(),
7472
];
7573

76-
let request = HttpConnectRequest::new(&host, &static_headers);
74+
let request = HttpConnectRequest::new(&static_headers);
7775

78-
assert_eq!(request.host.host_str(), "example.com");
79-
assert_eq!(request.host.port(), 8080);
8076
assert_eq!(request.static_headers.len(), 2);
8177
assert!(request.dyn_headers.is_empty());
8278
}
8379

8480
#[test]
85-
fn append_dyn_header() {
86-
let host = create_test_upstream_addr();
81+
fn append_dyn_header_preserves_order() {
8782
let static_headers: Vec<String> = Vec::new();
88-
let mut request = HttpConnectRequest::new(&host, &static_headers);
83+
let mut request = HttpConnectRequest::new(&static_headers);
8984

90-
// Appending a single dynamic header
9185
request.append_dyn_header("X-Custom-Header: value1\r\n".to_string());
92-
assert_eq!(request.dyn_headers.len(), 1);
93-
assert_eq!(request.dyn_headers[0], "X-Custom-Header: value1\r\n");
94-
95-
// Appending multiple dynamic headers
9686
request.append_dyn_header("X-Another-Header: value2\r\n".to_string());
87+
9788
assert_eq!(request.dyn_headers.len(), 2);
89+
assert_eq!(request.dyn_headers[0], "X-Custom-Header: value1\r\n");
9890
assert_eq!(request.dyn_headers[1], "X-Another-Header: value2\r\n");
9991
}
10092

10193
#[tokio::test]
102-
async fn send_no_headers() {
103-
let host = create_test_upstream_addr();
94+
async fn send_without_extra_headers() {
95+
let target = test_upstream_addr();
10496
let static_headers: Vec<String> = Vec::new();
105-
let request = HttpConnectRequest::new(&host, &static_headers);
106-
107-
let mut buffer = Vec::new();
108-
request.send(&mut buffer).await.unwrap();
109-
110-
let output = String::from_utf8(buffer).unwrap();
111-
let expected = "CONNECT example.com:8080 HTTP/1.1\r\n\
112-
Host: example.com:8080\r\n\
113-
Connection: keep-alive\r\n\
114-
\r\n";
115-
116-
assert_eq!(output, expected);
97+
let request = HttpConnectRequest::new(&static_headers);
98+
99+
assert_eq!(
100+
render_request(&request, &target).await,
101+
"CONNECT example.com:8080 HTTP/1.1\r\n\
102+
Connection: keep-alive\r\n\
103+
\r\n"
104+
);
117105
}
118106

119107
#[tokio::test]
120-
async fn send_with_static_headers() {
121-
let host = create_test_upstream_addr();
108+
async fn send_with_static_and_dynamic_headers() {
109+
let target = test_upstream_addr();
122110
let static_headers = vec![
123111
"User-Agent: test-agent\r\n".to_string(),
124112
"Accept: */*\r\n".to_string(),
125113
];
126-
127-
let request = HttpConnectRequest::new(&host, &static_headers);
128-
129-
let mut buffer = Vec::new();
130-
request.send(&mut buffer).await.unwrap();
131-
132-
let output = String::from_utf8(buffer).unwrap();
133-
let expected = "CONNECT example.com:8080 HTTP/1.1\r\n\
134-
Host: example.com:8080\r\n\
135-
Connection: keep-alive\r\n\
136-
User-Agent: test-agent\r\n\
137-
Accept: */*\r\n\
138-
\r\n";
139-
140-
assert_eq!(output, expected);
141-
}
142-
143-
#[tokio::test]
144-
async fn send_with_dynamic_headers() {
145-
let host = create_test_upstream_addr();
146-
let static_headers: Vec<String> = Vec::new();
147-
let mut request = HttpConnectRequest::new(&host, &static_headers);
114+
let mut request = HttpConnectRequest::new(&static_headers);
148115

149116
request.append_dyn_header("X-Custom-Header: value1\r\n".to_string());
150117
request.append_dyn_header("X-Another-Header: value2\r\n".to_string());
151118

152-
let mut buffer = Vec::new();
153-
request.send(&mut buffer).await.unwrap();
154-
155-
let output = String::from_utf8(buffer).unwrap();
156-
let expected = "CONNECT example.com:8080 HTTP/1.1\r\n\
157-
Host: example.com:8080\r\n\
158-
Connection: keep-alive\r\n\
159-
X-Custom-Header: value1\r\n\
160-
X-Another-Header: value2\r\n\
161-
\r\n";
162-
163-
assert_eq!(output, expected);
164-
}
165-
166-
#[tokio::test]
167-
async fn send_with_both_header_types() {
168-
let host = create_test_upstream_addr();
169-
let static_headers = vec![
170-
"User-Agent: test-agent\r\n".to_string(),
171-
"Accept: */*\r\n".to_string(),
172-
];
173-
174-
let mut request = HttpConnectRequest::new(&host, &static_headers);
175-
request.append_dyn_header("X-Custom-Header: value1\r\n".to_string());
176-
request.append_dyn_header("X-Another-Header: value2\r\n".to_string());
177-
178-
let mut buffer = Vec::new();
179-
request.send(&mut buffer).await.unwrap();
180-
181-
let output = String::from_utf8(buffer).unwrap();
182-
let expected = "CONNECT example.com:8080 HTTP/1.1\r\n\
183-
Host: example.com:8080\r\n\
184-
Connection: keep-alive\r\n\
185-
User-Agent: test-agent\r\n\
186-
Accept: */*\r\n\
187-
X-Custom-Header: value1\r\n\
188-
X-Another-Header: value2\r\n\
189-
\r\n";
190-
191-
assert_eq!(output, expected);
119+
assert_eq!(
120+
render_request(&request, &target).await,
121+
"CONNECT example.com:8080 HTTP/1.1\r\n\
122+
Connection: keep-alive\r\n\
123+
User-Agent: test-agent\r\n\
124+
Accept: */*\r\n\
125+
X-Custom-Header: value1\r\n\
126+
X-Another-Header: value2\r\n\
127+
\r\n"
128+
);
192129
}
193130

194131
#[tokio::test]
195132
async fn send_with_ipv6_host() {
196-
let host = UpstreamAddr::from_str("[2001:db8::1]:8080").unwrap();
133+
let target = UpstreamAddr::from_str("[2001:db8::1]:8080").unwrap();
197134
let static_headers: Vec<String> = Vec::new();
198-
let request = HttpConnectRequest::new(&host, &static_headers);
199-
200-
let mut buffer = Vec::new();
201-
request.send(&mut buffer).await.unwrap();
202-
203-
let output = String::from_utf8(buffer).unwrap();
204-
let expected = "CONNECT [2001:db8::1]:8080 HTTP/1.1\r\n\
205-
Host: [2001:db8::1]:8080\r\n\
206-
Connection: keep-alive\r\n\
207-
\r\n";
208-
209-
assert_eq!(output, expected);
135+
let request = HttpConnectRequest::new(&static_headers);
136+
137+
assert_eq!(
138+
render_request(&request, &target).await,
139+
"CONNECT [2001:db8::1]:8080 HTTP/1.1\r\n\
140+
Connection: keep-alive\r\n\
141+
\r\n"
142+
);
210143
}
211144

212145
#[tokio::test]
213-
async fn send_flush_behavior() {
214-
let host = create_test_upstream_addr();
146+
async fn send_propagates_writer_error() {
147+
let target = test_upstream_addr();
215148
let static_headers: Vec<String> = Vec::new();
216-
let request = HttpConnectRequest::new(&host, &static_headers);
217-
218-
let mut buffer = Vec::new();
219-
let result = request.send(&mut buffer).await;
149+
let request = HttpConnectRequest::new(&static_headers);
150+
let mut writer = tokio_test::io::Builder::new()
151+
.write_error(io::Error::new(io::ErrorKind::BrokenPipe, "write failed"))
152+
.build();
220153

221-
assert!(result.is_ok());
222-
// Verify that data was actually written to the buffer
223-
assert!(!buffer.is_empty());
154+
let err = request.send(&target, &mut writer).await.unwrap_err();
155+
assert_eq!(err.kind(), io::ErrorKind::BrokenPipe);
224156
}
225157
}

lib/vey-http/src/connect/response.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ impl HttpConnectResponse {
177177
if self.code >= 200 && self.code < 300 {
178178
Ok(())
179179
} else if self.code == 504 || self.code == 522 || self.code == 524 {
180-
// Peer tells us it timeout
180+
// Peer tells us it times out
181181
Err(HttpConnectError::PeerTimeout(self.code))
182182
} else {
183183
Err(HttpConnectError::UnexpectedStatusCode(

lib/vey-http/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ pub mod client;
1818
pub mod connect;
1919
pub mod header;
2020
pub mod server;
21+
pub mod upgrade;
2122
pub mod uri;

lib/vey-http/src/upgrade/error.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* SPDX-FileCopyrightText: 2026 VEY-OSS Developers.
4+
*/
5+
6+
use std::io;
7+
8+
use thiserror::Error;
9+
10+
use crate::HttpLineParseError;
11+
12+
#[derive(Debug, Error)]
13+
pub enum HttpUpgradeResponseError {
14+
#[error("too large header, should be less than {0}")]
15+
TooLargeHeader(usize),
16+
#[error("invalid status line: {0}")]
17+
InvalidStatusLine(HttpLineParseError),
18+
#[error("invalid header line: {0}")]
19+
InvalidHeaderLine(HttpLineParseError),
20+
#[error("unsupported value in header {0}")]
21+
UnsupportedHeaderValue(&'static str),
22+
#[error("upgrade token not match")]
23+
UpgradeTokenNotMatch,
24+
#[error("invalid chunked transfer-encoding")]
25+
InvalidChunkedTransferEncoding,
26+
#[error("invalid content length")]
27+
InvalidContentLength,
28+
}
29+
30+
#[derive(Debug, Error)]
31+
pub enum HttpUpgradeError {
32+
#[error("remote closed")]
33+
RemoteClosed,
34+
#[error("read failed: {0:?}")]
35+
ReadFailed(io::Error),
36+
#[error("write failed: {0:?}")]
37+
WriteFailed(io::Error),
38+
#[error("invalid response: {0}")]
39+
InvalidResponse(#[from] HttpUpgradeResponseError),
40+
#[error("unexpected status code {0} {1}")]
41+
UnexpectedStatusCode(u16, String),
42+
#[error("peer timeout with status code {0}")]
43+
PeerTimeout(u16),
44+
}

0 commit comments

Comments
 (0)