mirror of
https://github.com/tun2proxy/tun2proxy.git
synced 2025-04-22 14:59:09 +00:00
Initial support digest auth scheme
This commit is contained in:
parent
6767076a6b
commit
86429ee8eb
6 changed files with 321 additions and 67 deletions
|
@ -23,6 +23,9 @@ prctl = "1.0"
|
|||
smoltcp = { version = "0.9.1", git = "https://github.com/smoltcp-rs/smoltcp", features = ["std", "phy-tuntap_interface"] }
|
||||
thiserror = "1.0"
|
||||
url = "2.3"
|
||||
digest_auth = "0.3.1"
|
||||
httparse = "1.8.0"
|
||||
unicase = "2.6.0"
|
||||
|
||||
[target.'cfg(target_os="android")'.dependencies]
|
||||
android_logger = "0.13"
|
||||
|
|
|
@ -48,6 +48,12 @@ pub enum Error {
|
|||
|
||||
#[error("std::num::ParseIntError {0:?}")]
|
||||
IntParseError(#[from] std::num::ParseIntError),
|
||||
|
||||
#[error("httparse::Error {0}")]
|
||||
HttpError(#[from] httparse::Error),
|
||||
|
||||
#[error("digest_auth::Error {0}")]
|
||||
DigestAuthError(#[from] digest_auth::Error),
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
|
|
318
src/http.rs
318
src/http.rs
|
@ -1,24 +1,41 @@
|
|||
use crate::error::Error;
|
||||
use crate::tun2proxy::{
|
||||
Connection, ConnectionManager, Direction, IncomingDataEvent, IncomingDirection,
|
||||
Connection, ConnectionManager, Destination, Direction, IncomingDataEvent, IncomingDirection,
|
||||
OutgoingDataEvent, OutgoingDirection, TcpProxy,
|
||||
};
|
||||
use crate::Credentials;
|
||||
use base64::Engine;
|
||||
use httparse::Response;
|
||||
use smoltcp::wire::IpProtocol;
|
||||
use std::collections::VecDeque;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::RandomState;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::iter::FromIterator;
|
||||
use std::net::SocketAddr;
|
||||
use std::rc::Rc;
|
||||
use std::str;
|
||||
use unicase::UniCase;
|
||||
|
||||
#[derive(Eq, PartialEq, Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum AuthenticationScheme {
|
||||
None,
|
||||
Basic,
|
||||
Digest,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum HttpState {
|
||||
SendRequest,
|
||||
ExpectStatusCode,
|
||||
ExpectResponseHeaders,
|
||||
ExpectResponse,
|
||||
Reset,
|
||||
Established,
|
||||
}
|
||||
|
||||
pub(crate) type DigestState = digest_auth::WwwAuthenticateHeader;
|
||||
|
||||
pub struct HttpConnection {
|
||||
state: HttpState,
|
||||
client_inbuf: VecDeque<u8>,
|
||||
|
@ -27,82 +44,264 @@ pub struct HttpConnection {
|
|||
server_outbuf: VecDeque<u8>,
|
||||
data_buf: VecDeque<u8>,
|
||||
crlf_state: u8,
|
||||
counter: usize,
|
||||
skip: usize,
|
||||
digest_state: Rc<RefCell<Option<DigestState>>>,
|
||||
before: bool,
|
||||
credentials: Option<Credentials>,
|
||||
destination: Destination,
|
||||
}
|
||||
|
||||
static PROXY_AUTHENTICATE: &str = "Proxy-Authenticate";
|
||||
static PROXY_AUTHORIZATION: &str = "Proxy-Authorization";
|
||||
static CONNECTION: &str = "Connection";
|
||||
static TRANSFER_ENCODING: &str = "Transfer-Encoding";
|
||||
static CONTENT_LENGTH: &str = "Content-Length";
|
||||
|
||||
impl HttpConnection {
|
||||
fn new(connection: &Connection, manager: Rc<dyn ConnectionManager>) -> Self {
|
||||
let mut server_outbuf: VecDeque<u8> = VecDeque::new();
|
||||
{
|
||||
let credentials = manager.get_credentials();
|
||||
server_outbuf.extend(b"CONNECT ".iter());
|
||||
server_outbuf.extend(connection.dst.to_string().as_bytes());
|
||||
server_outbuf.extend(b" HTTP/1.1\r\nHost: ".iter());
|
||||
server_outbuf.extend(connection.dst.to_string().as_bytes());
|
||||
server_outbuf.extend(b"\r\n".iter());
|
||||
if let Some(credentials) = credentials {
|
||||
server_outbuf.extend(b"Proxy-Authorization: Basic ");
|
||||
let mut auth_plain = credentials.username.clone();
|
||||
auth_plain.extend(b":".iter());
|
||||
auth_plain.extend(&credentials.password);
|
||||
let auth_b64 = base64::engine::general_purpose::STANDARD.encode(auth_plain);
|
||||
server_outbuf.extend(auth_b64.as_bytes().iter());
|
||||
server_outbuf.extend(b"\r\n".iter());
|
||||
}
|
||||
server_outbuf.extend(b"\r\n".iter());
|
||||
}
|
||||
|
||||
Self {
|
||||
state: HttpState::ExpectStatusCode,
|
||||
fn new(
|
||||
connection: &Connection,
|
||||
manager: Rc<dyn ConnectionManager>,
|
||||
digest_state: Rc<RefCell<Option<DigestState>>>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut res = Self {
|
||||
state: HttpState::ExpectResponseHeaders,
|
||||
client_inbuf: Default::default(),
|
||||
server_inbuf: Default::default(),
|
||||
client_outbuf: Default::default(),
|
||||
server_outbuf,
|
||||
server_outbuf: Default::default(),
|
||||
data_buf: Default::default(),
|
||||
crlf_state: Default::default(),
|
||||
skip: 0,
|
||||
counter: 0,
|
||||
crlf_state: 0,
|
||||
digest_state,
|
||||
before: false,
|
||||
credentials: manager.get_credentials().clone(),
|
||||
destination: connection.dst.clone(),
|
||||
};
|
||||
|
||||
res.send_tunnel_request()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn send_tunnel_request(&mut self) -> Result<(), Error> {
|
||||
self.server_outbuf.extend(b"CONNECT ");
|
||||
self.server_outbuf
|
||||
.extend(self.destination.to_string().as_bytes());
|
||||
self.server_outbuf.extend(b" HTTP/1.1\r\nHost: ");
|
||||
self.server_outbuf
|
||||
.extend(self.destination.to_string().as_bytes());
|
||||
self.server_outbuf.extend(b"\r\n");
|
||||
|
||||
self.send_auth_data(if self.digest_state.borrow().is_none() {
|
||||
AuthenticationScheme::Basic
|
||||
} else {
|
||||
AuthenticationScheme::Digest
|
||||
})?;
|
||||
|
||||
self.server_outbuf.extend(b"\r\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_auth_data(&mut self, scheme: AuthenticationScheme) -> Result<(), Error> {
|
||||
let Some(credentials) = &self.credentials else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match scheme {
|
||||
AuthenticationScheme::Digest => {
|
||||
let uri = format!("{}:{}", self.destination.host, self.destination.port);
|
||||
|
||||
let context = digest_auth::AuthContext::new_with_method(
|
||||
&credentials.username,
|
||||
&credentials.password,
|
||||
&uri,
|
||||
Option::<&'_ [u8]>::None,
|
||||
digest_auth::HttpMethod::CONNECT,
|
||||
);
|
||||
|
||||
let mut state = self.digest_state.borrow_mut();
|
||||
let response = state.as_mut().unwrap().respond(&context)?;
|
||||
|
||||
self.server_outbuf.extend(
|
||||
format!(
|
||||
"{}: {}\r\n",
|
||||
PROXY_AUTHORIZATION,
|
||||
response.to_header_string()
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
}
|
||||
AuthenticationScheme::Basic => {
|
||||
let cred = format!("{}:{}", credentials.username, credentials.password);
|
||||
let auth_b64 = base64::engine::general_purpose::STANDARD.encode(cred);
|
||||
self.server_outbuf
|
||||
.extend(format!("{}: Basic {}\r\n", PROXY_AUTHORIZATION, auth_b64).as_bytes());
|
||||
}
|
||||
AuthenticationScheme::None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn state_change(&mut self) -> Result<(), Error> {
|
||||
let http_len = "HTTP/1.1 200".len();
|
||||
match self.state {
|
||||
HttpState::ExpectStatusCode if self.server_inbuf.len() > http_len => {
|
||||
let status_line: Vec<u8> =
|
||||
self.server_inbuf.range(0..http_len + 1).copied().collect();
|
||||
let slice = &status_line.as_slice()[0.."HTTP/1.1 2".len()];
|
||||
if slice != b"HTTP/1.1 2" && slice != b"HTTP/1.0 2"
|
||||
|| self.server_inbuf[http_len] != b' '
|
||||
{
|
||||
let status_str = String::from_utf8_lossy(&status_line.as_slice()[0..http_len]);
|
||||
let e =
|
||||
format!("Expected success status code. Server replied with {status_str}.");
|
||||
return Err(e.into());
|
||||
}
|
||||
self.state = HttpState::ExpectResponse;
|
||||
return self.state_change();
|
||||
}
|
||||
HttpState::ExpectResponse => {
|
||||
let mut counter = 0usize;
|
||||
for b_ref in self.server_inbuf.iter() {
|
||||
let b = *b_ref;
|
||||
HttpState::ExpectResponseHeaders => {
|
||||
while self.counter < self.server_inbuf.len() {
|
||||
let b = self.server_inbuf[self.counter];
|
||||
if b == b'\n' {
|
||||
self.crlf_state += 1;
|
||||
} else if b != b'\r' {
|
||||
self.crlf_state = 0;
|
||||
}
|
||||
counter += 1;
|
||||
|
||||
self.counter += 1;
|
||||
if self.crlf_state == 2 {
|
||||
self.server_inbuf.drain(0..counter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if self.crlf_state != 2 {
|
||||
// Waiting for the end of the headers yet
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.counter = 0;
|
||||
self.crlf_state = 0;
|
||||
|
||||
let mut headers = [httparse::EMPTY_HEADER; 16];
|
||||
let mut res = Response::new(&mut headers);
|
||||
|
||||
// First make the buffer contiguous
|
||||
let slice = self.server_inbuf.make_contiguous();
|
||||
let status = res.parse(slice)?;
|
||||
if status.is_partial() {
|
||||
// TODO: Optimize in order to detect 200
|
||||
return Ok(());
|
||||
}
|
||||
let len = status.unwrap();
|
||||
let status_code = res.code.unwrap();
|
||||
let version = res.version.unwrap();
|
||||
|
||||
if status_code == 200 {
|
||||
// Connection successful
|
||||
self.state = HttpState::Established;
|
||||
self.server_inbuf.clear();
|
||||
|
||||
self.server_outbuf.append(&mut self.data_buf);
|
||||
self.data_buf.clear();
|
||||
|
||||
self.state = HttpState::Established;
|
||||
return self.state_change();
|
||||
}
|
||||
|
||||
if status_code != 407 {
|
||||
let e =
|
||||
format!("Expected success status code. Server replied with {status_code} [Reason: {}].", res.reason.unwrap());
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
self.server_inbuf.drain(0..counter);
|
||||
let headers_map: HashMap<UniCase<&str>, &[u8], RandomState> =
|
||||
HashMap::from_iter(headers.map(|x| (UniCase::new(x.name), x.value)));
|
||||
|
||||
let Some(auth_data) = headers_map.get(&UniCase::new(PROXY_AUTHENTICATE)) else {
|
||||
return Err("Proxy requires auth but doesn't send it datails".into());
|
||||
};
|
||||
|
||||
if !auth_data[..6].eq_ignore_ascii_case(b"digest") {
|
||||
// Fail to auth and the scheme isn't in the
|
||||
// supported auth method schemes
|
||||
return Err("Bad credentials".into());
|
||||
}
|
||||
|
||||
// Analize challenge params
|
||||
let data = str::from_utf8(auth_data)?;
|
||||
let state = digest_auth::parse(data)?;
|
||||
if self.before && !state.stale {
|
||||
return Err("Bad credentials".into());
|
||||
}
|
||||
|
||||
// Update the digest state
|
||||
self.digest_state.replace(Some(state));
|
||||
self.before = true;
|
||||
|
||||
let closed = match headers_map.get(&UniCase::new(CONNECTION)) {
|
||||
Some(conn_header) => conn_header.eq_ignore_ascii_case(b"close"),
|
||||
None => false,
|
||||
};
|
||||
|
||||
if closed || version == 0 {
|
||||
// Close mio stream connection and reset it
|
||||
// Reset all the buffers
|
||||
self.server_inbuf.clear();
|
||||
self.server_outbuf.clear();
|
||||
self.send_tunnel_request()?;
|
||||
|
||||
self.state = HttpState::Reset;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The HTTP/1.1 expected to be keep alive waiting for the next frame so, we must
|
||||
// compute the lenght of the response in order to detect the next frame (response)
|
||||
// [RFC-9112](https://datatracker.ietf.org/doc/html/rfc9112#body.content-length)
|
||||
|
||||
// Transfer-Encoding isn't supported yet
|
||||
if let Some(_) = headers_map.get(&UniCase::new(TRANSFER_ENCODING)) {
|
||||
unimplemented!("Header Transfer-Encoding not supported");
|
||||
}
|
||||
|
||||
let content_length = match headers_map.get(&UniCase::new(CONTENT_LENGTH)) {
|
||||
Some(v) => {
|
||||
let value = str::from_utf8(v)?;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1
|
||||
match value.parse::<usize>() {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
let mut it = value.split(',').map(|x| x.parse::<usize>());
|
||||
let f = it.next().unwrap()?;
|
||||
for k in it {
|
||||
if k? != f {
|
||||
return Err("Malformed response".into());
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Close the connection by information miss
|
||||
self.server_inbuf.clear();
|
||||
self.server_outbuf.clear();
|
||||
self.send_tunnel_request()?;
|
||||
|
||||
self.state = HttpState::Reset;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Handshake state
|
||||
self.state = HttpState::ExpectResponse;
|
||||
self.skip = content_length + len;
|
||||
|
||||
return self.state_change();
|
||||
}
|
||||
HttpState::ExpectResponse => {
|
||||
if self.skip > 0 {
|
||||
let cnt = self.skip.min(self.server_inbuf.len());
|
||||
self.server_inbuf.drain(..cnt);
|
||||
self.skip -= cnt;
|
||||
}
|
||||
|
||||
if self.skip == 0 {
|
||||
// Expected to the server_inbuff to be empty
|
||||
|
||||
// self.server_outbuf.append(&mut self.data_buf);
|
||||
// self.data_buf.clear();
|
||||
self.send_tunnel_request()?;
|
||||
self.state = HttpState::ExpectResponseHeaders;
|
||||
|
||||
return self.state_change();
|
||||
}
|
||||
}
|
||||
HttpState::Established => {
|
||||
self.client_outbuf.extend(self.server_inbuf.iter());
|
||||
|
@ -110,6 +309,9 @@ impl HttpConnection {
|
|||
self.server_inbuf.clear();
|
||||
self.client_inbuf.clear();
|
||||
}
|
||||
HttpState::Reset => {
|
||||
self.state = HttpState::ExpectResponseHeaders;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -175,11 +377,16 @@ impl TcpProxy for HttpConnection {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_connection(&self) -> bool {
|
||||
self.state == HttpState::Reset
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HttpManager {
|
||||
server: SocketAddr,
|
||||
credentials: Option<Credentials>,
|
||||
digest_state: Rc<RefCell<Option<DigestState>>>,
|
||||
}
|
||||
|
||||
impl ConnectionManager for HttpManager {
|
||||
|
@ -195,7 +402,11 @@ impl ConnectionManager for HttpManager {
|
|||
if connection.proto != IpProtocol::Tcp {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(Box::new(HttpConnection::new(connection, manager))))
|
||||
Ok(Some(Box::new(HttpConnection::new(
|
||||
connection,
|
||||
manager,
|
||||
self.digest_state.clone(),
|
||||
)?)))
|
||||
}
|
||||
|
||||
fn close_connection(&self, _: &Connection) {}
|
||||
|
@ -214,6 +425,7 @@ impl HttpManager {
|
|||
Rc::new(Self {
|
||||
server,
|
||||
credentials,
|
||||
digest_state: Rc::new(RefCell::new(None)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,15 +110,15 @@ impl Options {
|
|||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Credentials {
|
||||
pub(crate) username: Vec<u8>,
|
||||
pub(crate) password: Vec<u8>,
|
||||
pub(crate) username: String,
|
||||
pub(crate) password: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
pub fn new(username: &str, password: &str) -> Self {
|
||||
Self {
|
||||
username: username.as_bytes().to_vec(),
|
||||
password: password.as_bytes().to_vec(),
|
||||
username: String::from(username),
|
||||
password: String::from(password),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
src/socks.rs
12
src/socks.rs
|
@ -156,10 +156,10 @@ impl SocksConnection {
|
|||
}
|
||||
self.server_outbuf.extend(ip_vec);
|
||||
if let Some(credentials) = credentials {
|
||||
self.server_outbuf.extend(&credentials.username);
|
||||
self.server_outbuf.extend(credentials.username.as_bytes());
|
||||
if !credentials.password.is_empty() {
|
||||
self.server_outbuf.push_back(b':');
|
||||
self.server_outbuf.extend(&credentials.password);
|
||||
self.server_outbuf.extend(credentials.password.as_bytes());
|
||||
}
|
||||
}
|
||||
self.server_outbuf.push_back(0);
|
||||
|
@ -250,10 +250,10 @@ impl SocksConnection {
|
|||
let credentials = self.credentials.as_ref().unwrap_or(&tmp);
|
||||
self.server_outbuf
|
||||
.extend(&[1u8, credentials.username.len() as u8]);
|
||||
self.server_outbuf.extend(&credentials.username);
|
||||
self.server_outbuf.extend(credentials.username.as_bytes());
|
||||
self.server_outbuf
|
||||
.extend(&[credentials.password.len() as u8]);
|
||||
self.server_outbuf.extend(&credentials.password);
|
||||
self.server_outbuf.extend(credentials.password.as_bytes());
|
||||
self.state = SocksState::ReceiveAuthResponse;
|
||||
self.state_change()
|
||||
}
|
||||
|
@ -424,6 +424,10 @@ impl TcpProxy for SocksConnection {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_connection(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SocksManager {
|
||||
|
|
|
@ -228,6 +228,7 @@ pub(crate) trait TcpProxy {
|
|||
fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent;
|
||||
fn connection_established(&self) -> bool;
|
||||
fn have_data(&mut self, dir: Direction) -> bool;
|
||||
fn reset_connection(&self) -> bool;
|
||||
}
|
||||
|
||||
pub(crate) trait ConnectionManager {
|
||||
|
@ -291,7 +292,7 @@ impl<'a> TunToProxy<'a> {
|
|||
let mut virt = VirtualTunDevice::new(tun.capabilities());
|
||||
let gateway4: Ipv4Addr = Ipv4Addr::from_str("0.0.0.1")?;
|
||||
let gateway6: Ipv6Addr = Ipv6Addr::from_str("::1")?;
|
||||
let mut iface = Interface::new(config, &mut virt);
|
||||
let mut iface = Interface::new(config, &mut virt, smoltcp::time::Instant::now());
|
||||
iface.update_ip_addrs(|ip_addrs| {
|
||||
ip_addrs.push(IpCidr::new(gateway4.into(), 0)).unwrap();
|
||||
ip_addrs.push(IpCidr::new(gateway6.into(), 0)).unwrap()
|
||||
|
@ -699,6 +700,10 @@ impl<'a> TunToProxy<'a> {
|
|||
return Ok(());
|
||||
}
|
||||
let connection = conn_ref.unwrap().clone();
|
||||
let server = self
|
||||
.get_connection_manager(&connection)
|
||||
.unwrap()
|
||||
.get_server();
|
||||
|
||||
(|| -> Result<(), Error> {
|
||||
if event.is_readable() || event.is_read_closed() {
|
||||
|
@ -737,6 +742,30 @@ impl<'a> TunToProxy<'a> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// The handler request for reset the server connection
|
||||
if state.handler.reset_connection() {
|
||||
// Closes the connection with the proxy
|
||||
state.mio_stream.shutdown(Both)?;
|
||||
|
||||
info!("RESETED {}", connection);
|
||||
|
||||
// TODO: Improve the call upstairs
|
||||
state.mio_stream = TcpStream::connect(server)?;
|
||||
|
||||
_ = self.poll.registry().deregister(&mut state.mio_stream);
|
||||
self.poll.registry().register(
|
||||
&mut state.mio_stream,
|
||||
state.token,
|
||||
Interest::WRITABLE,
|
||||
)?;
|
||||
|
||||
state.wait_read = true;
|
||||
state.wait_write = true;
|
||||
state.close_state = 0;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if read == 0 || event.is_read_closed() {
|
||||
state.wait_read = false;
|
||||
state.close_state |= SERVER_WRITE_CLOSED;
|
||||
|
|
Loading…
Add table
Reference in a new issue