Skip to main content

io_proxy/
client.rs

1//! Standard, blocking pump driving the proxy coroutines over any
2//! `Read + Write` stream.
3//!
4//! On success the stream is a live tunnel to the target, positioned at
5//! its first byte, ready for the caller's TLS handshake or plaintext
6//! protocol.
7
8use std::io::{Read, Write};
9
10use thiserror::Error;
11
12use crate::coroutine::{ProxyCoroutine, ProxyCoroutineState, ProxyYield};
13#[cfg(feature = "http")]
14use crate::http::connect::{HttpConnect, HttpCredentials};
15#[cfg(feature = "socks5")]
16use crate::socks::v5::{
17    address::Socks5Address,
18    auth::Socks5Credentials,
19    connect::{Socks5Connect, Socks5ConnectError},
20};
21
22/// Largest single read any handshake requests: a SOCKS5 domain-typed
23/// bound address is `1 + 255 + 2` bytes; every other read is smaller.
24const READ_BUFFER_SIZE: usize = 258;
25
26/// Errors returned by the client pump.
27#[derive(Debug, Error)]
28pub enum ProxyClientError {
29    /// The SOCKS5 handshake coroutine failed.
30    #[cfg(feature = "socks5")]
31    #[error(transparent)]
32    Socks5(#[from] Socks5ConnectError),
33    /// The HTTP `CONNECT` handshake coroutine failed.
34    #[cfg(feature = "http")]
35    #[error(transparent)]
36    Http(#[from] crate::http::connect::HttpConnectError),
37    /// The underlying stream failed to read or write.
38    #[error(transparent)]
39    Io(#[from] std::io::Error),
40}
41
42/// Drives a proxy coroutine against `stream` until it completes.
43///
44/// Every read is exact (`read_exact`), so on return the stream holds no
45/// buffered tunnel bytes.
46fn run<S, C, E>(stream: &mut S, mut coroutine: C) -> Result<(), ProxyClientError>
47where
48    S: Read + Write,
49    C: ProxyCoroutine<Yield = ProxyYield, Return = Result<(), E>>,
50    ProxyClientError: From<E>,
51{
52    let mut buf = [0u8; READ_BUFFER_SIZE];
53    let mut arg: Option<&[u8]> = None;
54
55    loop {
56        match coroutine.resume(arg.take()) {
57            ProxyCoroutineState::Complete(Ok(())) => return Ok(()),
58            ProxyCoroutineState::Complete(Err(err)) => return Err(err.into()),
59            ProxyCoroutineState::Yielded(ProxyYield::WantsWrite(bytes)) => {
60                stream.write_all(&bytes)?;
61                arg = None;
62            }
63            ProxyCoroutineState::Yielded(ProxyYield::WantsRead(n)) => {
64                stream.read_exact(&mut buf[..n])?;
65                arg = Some(&buf[..n]);
66            }
67        }
68    }
69}
70
71/// Runs the SOCKS5 `CONNECT` handshake on `stream` (already connected to
72/// the proxy), tunnelling to `target` and authenticating with
73/// `credentials` if the proxy asks for username/password.
74#[cfg(feature = "socks5")]
75#[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
76pub fn connect_socks5<S: Read + Write>(
77    stream: &mut S,
78    target: Socks5Address,
79    credentials: Option<Socks5Credentials>,
80) -> Result<(), ProxyClientError> {
81    run(stream, Socks5Connect::new(target, credentials))
82}
83
84/// Runs the HTTP `CONNECT` handshake on `stream` (already connected to the
85/// proxy), tunnelling to `host:port` and sending `Proxy-Authorization:
86/// Basic` if `credentials` are provided.
87#[cfg(feature = "http")]
88#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
89pub fn connect_http<S: Read + Write>(
90    stream: &mut S,
91    host: &str,
92    port: u16,
93    credentials: Option<HttpCredentials>,
94) -> Result<(), ProxyClientError> {
95    run(stream, HttpConnect::new(host, port, credentials))
96}
97
98#[cfg(all(test, feature = "socks5"))]
99mod socks5_tests {
100    use std::{
101        io::{self, Cursor, Read, Write},
102        vec::Vec,
103    };
104
105    use super::*;
106
107    /// A fake proxy: hands out `to_read` on reads, records writes.
108    struct Fake {
109        to_read: Cursor<Vec<u8>>,
110        written: Vec<u8>,
111    }
112
113    impl Read for Fake {
114        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
115            self.to_read.read(buf)
116        }
117    }
118
119    impl Write for Fake {
120        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
121            self.written.extend_from_slice(buf);
122            Ok(buf.len())
123        }
124        fn flush(&mut self) -> io::Result<()> {
125            Ok(())
126        }
127    }
128
129    #[test]
130    fn drives_handshake_and_leaves_tunnel_bytes() {
131        // method selection (no-auth) + success reply (IPv4) + one extra
132        // byte that belongs to the tunnel and must NOT be consumed.
133        let mut server = vec![0x05, 0x00];
134        server.extend_from_slice(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
135        server.push(0xAB); // tunnel payload
136
137        let mut fake = Fake {
138            to_read: Cursor::new(server),
139            written: Vec::new(),
140        };
141
142        connect_socks5(
143            &mut fake,
144            Socks5Address::Domain("example.com".into(), 993),
145            None,
146        )
147        .unwrap();
148
149        // greeting was written
150        assert_eq!(&fake.written[..3], [0x05, 0x01, 0x00]);
151
152        // the tunnel byte is still unread on the stream
153        let mut rest = Vec::new();
154        fake.read_to_end(&mut rest).unwrap();
155        assert_eq!(rest, [0xAB]);
156    }
157}
158
159#[cfg(all(test, feature = "http"))]
160mod http_tests {
161    use std::{
162        io::{self, Cursor, Read, Write},
163        vec::Vec,
164    };
165
166    use super::*;
167
168    struct Fake {
169        to_read: Cursor<Vec<u8>>,
170        written: Vec<u8>,
171    }
172
173    impl Read for Fake {
174        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
175            self.to_read.read(buf)
176        }
177    }
178
179    impl Write for Fake {
180        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
181            self.written.extend_from_slice(buf);
182            Ok(buf.len())
183        }
184        fn flush(&mut self) -> io::Result<()> {
185            Ok(())
186        }
187    }
188
189    #[test]
190    fn drives_connect_and_leaves_tunnel_bytes() {
191        // a 200 response head, then a tunnel byte the byte-at-a-time reader
192        // must leave untouched.
193        let mut server = b"HTTP/1.1 200 Connection established\r\n\r\n".to_vec();
194        server.push(0xAB);
195
196        let mut fake = Fake {
197            to_read: Cursor::new(server),
198            written: Vec::new(),
199        };
200
201        connect_http(&mut fake, "imap.example.com", 993, None).unwrap();
202
203        assert!(fake.written.starts_with(b"CONNECT imap.example.com:993"));
204
205        let mut rest = Vec::new();
206        fake.read_to_end(&mut rest).unwrap();
207        assert_eq!(rest, [0xAB]);
208    }
209}