http redirector

This commit is contained in:
sijie.sun
2025-02-19 23:30:10 +08:00
committed by Sijie.Sun
parent 2050ed78d0
commit 673c34cf5a
5 changed files with 379 additions and 112 deletions

View File

@@ -0,0 +1,298 @@
use std::{
net::SocketAddr,
pin::Pin,
sync::{Arc, RwLock},
};
use anyhow::Context;
use http_req::request::{RedirectPolicy, Request};
use rand::seq::SliceRandom as _;
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx},
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, ZCPacketSink, ZCPacketStream},
};
use crate::proto::common::TunnelInfo;
use super::create_connector_by_url;
pub struct TunnelWithInfo {
inner: Box<dyn Tunnel>,
info: TunnelInfo,
}
impl TunnelWithInfo {
pub fn new(inner: Box<dyn Tunnel>, info: TunnelInfo) -> Self {
Self { inner, info }
}
}
impl Tunnel for TunnelWithInfo {
fn split(&self) -> (Pin<Box<dyn ZCPacketStream>>, Pin<Box<dyn ZCPacketSink>>) {
self.inner.split()
}
fn info(&self) -> Option<TunnelInfo> {
Some(self.info.clone())
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
enum HttpRedirectType {
Unknown,
// redirected url is in the path of new url
RedirectToQuery,
// redirected url is the entire new url
RedirectToUrl,
// redirected url is in the body of response
BodyUrls,
}
#[derive(Debug)]
pub struct HttpTunnelConnector {
addr: url::Url,
bind_addrs: Vec<SocketAddr>,
ip_version: IpVersion,
global_ctx: ArcGlobalCtx,
redirect_type: HttpRedirectType,
}
impl HttpTunnelConnector {
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
Self {
addr,
bind_addrs: Vec::new(),
ip_version: IpVersion::Both,
global_ctx,
redirect_type: HttpRedirectType::Unknown,
}
}
#[tracing::instrument(ret)]
async fn handle_302_redirect(
&mut self,
new_url: url::Url,
) -> Result<Box<dyn TunnelConnector>, Error> {
// the url should be in following format:
// 1: http(s)://easytier.cn/?url=tcp://10.147.22.22:11010 (scheme is http, domain is ignored, path is splitted into proto type and addr)
// 2: tcp://10.137.22.22:11010 (scheme is protocol type, the url is used to construct a connector directly)
tracing::info!("redirect to {}", new_url);
let url = url::Url::parse(new_url.as_str())
.with_context(|| format!("parsing redirect url failed. url: {}", new_url))?;
if url.scheme() == "http" || url.scheme() == "https" {
let mut query = new_url
.query_pairs()
.filter_map(|x| url::Url::parse(&x.1).ok())
.collect::<Vec<_>>();
query.shuffle(&mut rand::thread_rng());
if query.is_empty() {
return Err(Error::InvalidUrl(format!(
"no valid connector url found in url: url: {}",
url
)));
}
tracing::info!("try to create connector by url: {}", query[0]);
self.redirect_type = HttpRedirectType::RedirectToQuery;
return create_connector_by_url(&query[0].to_string(), &self.global_ctx).await;
} else {
self.redirect_type = HttpRedirectType::RedirectToUrl;
return create_connector_by_url(new_url.as_str(), &self.global_ctx).await;
}
}
#[tracing::instrument]
async fn handle_200_success(
&mut self,
body: &String,
) -> Result<Box<dyn TunnelConnector>, Error> {
// resp body should be line of connector urls, like:
// tcp://10.1.1.1:11010
// udp://10.1.1.1:11010
let mut lines = body
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<&str>>();
tracing::info!("get {} lines of connector urls", lines.len());
// shuffle the lines and pick the usable one
lines.shuffle(&mut rand::thread_rng());
for line in lines {
let url = url::Url::parse(line);
if url.is_err() {
tracing::warn!("invalid url: {}, skip it", line);
continue;
}
self.redirect_type = HttpRedirectType::BodyUrls;
return create_connector_by_url(line, &self.global_ctx).await;
}
Err(Error::InvalidUrl(
"no valid connector url found".to_string(),
))
}
#[tracing::instrument(ret)]
pub async fn get_redirected_connector(
&mut self,
original_url: &str,
) -> Result<Box<dyn TunnelConnector>, Error> {
self.redirect_type = HttpRedirectType::Unknown;
tracing::info!("get_redirected_url: {}", original_url);
// Container for body of a response.
let body = Arc::new(RwLock::new(Vec::new()));
let original_url_clone = original_url.to_string();
let body_clone = body.clone();
let res = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(original_url_clone.as_ref())
.with_context(|| format!("parsing url failed. url: {}", original_url_clone))?;
tracing::info!("sending http request to {}", uri);
Request::new(&uri)
.redirect_policy(RedirectPolicy::Limit(0))
.timeout(std::time::Duration::from_secs(5))
.send(&mut *body_clone.write().unwrap())
.with_context(|| format!("sending http request failed. url: {}", uri))
})
.await
.map_err(|e| Error::InvalidUrl(format!("task join error: {}", e)))??;
let body = String::from_utf8_lossy(&body.read().unwrap()).to_string();
if res.status_code().is_redirect() {
let redirect_url = res
.headers()
.get("Location")
.ok_or_else(|| Error::InvalidUrl("no redirect address found".to_string()))?;
let new_url = url::Url::parse(redirect_url.as_str())
.with_context(|| format!("parsing redirect url failed. url: {}", redirect_url))?;
return self.handle_302_redirect(new_url).await;
} else if res.status_code().is_success() {
return self.handle_200_success(&body).await;
} else {
return Err(Error::InvalidUrl(format!(
"unexpected response, resp: {:?}, body: {}",
res, body,
)));
}
}
}
#[async_trait::async_trait]
impl super::TunnelConnector for HttpTunnelConnector {
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
let mut conn = self
.get_redirected_connector(self.addr.to_string().as_str())
.await
.with_context(|| "get redirected url failed")?;
conn.set_ip_version(self.ip_version);
let t = conn.connect().await?;
let info = t.info().unwrap_or_default();
Ok(Box::new(TunnelWithInfo::new(
t,
TunnelInfo {
local_addr: info.local_addr.clone(),
remote_addr: Some(self.addr.clone().into()),
tunnel_type: format!(
"{:?}-{}",
self.redirect_type,
info.remote_addr.unwrap_or_default()
),
},
)))
}
fn remote_url(&self) -> url::Url {
self.addr.clone()
}
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
self.bind_addrs = addrs;
}
fn set_ip_version(&mut self, ip_version: IpVersion) {
self.ip_version = ip_version;
}
}
#[cfg(test)]
mod tests {
use tokio::{io::AsyncWriteExt as _, net::TcpListener};
use crate::{
common::global_ctx::tests::get_mock_global_ctx,
tunnel::{tcp::TcpTunnelListener, TunnelConnector, TunnelListener},
};
use super::*;
async fn run_http_redirect_server(port: u16, test_type: HttpRedirectType) -> Result<(), Error> {
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
let (mut stream, _) = listener.accept().await?;
match test_type {
HttpRedirectType::RedirectToQuery => {
let resp = "HTTP/1.1 301 Moved Permanently\r\nLocation: http://test.com/?url=tcp://127.0.0.1:25888\r\n\r\n";
stream.write_all(resp.as_bytes()).await?;
}
HttpRedirectType::RedirectToUrl => {
let resp =
"HTTP/1.1 301 Moved Permanently\r\nLocation: tcp://127.0.0.1:25888\r\n\r\n";
stream.write_all(resp.as_bytes()).await?;
}
HttpRedirectType::BodyUrls => {
let resp = "HTTP/1.1 200 OK\r\n\r\ntcp://127.0.0.1:25888";
stream.write_all(resp.as_bytes()).await?;
}
HttpRedirectType::Unknown => {
panic!("unexpected test type");
}
}
Ok(())
}
#[rstest::rstest]
#[serial_test::serial(http_redirect_test)]
#[tokio::test]
async fn http_redirect_test(
// 1. 301 redirect
// 2. 200 success with valid connector urls
#[values(
HttpRedirectType::RedirectToQuery,
HttpRedirectType::RedirectToUrl,
HttpRedirectType::BodyUrls
)]
test_type: HttpRedirectType,
) {
let http_task = tokio::spawn(run_http_redirect_server(35888, test_type));
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let test_url: url::Url = "http://127.0.0.1:35888".parse().unwrap();
let global_ctx = get_mock_global_ctx();
let mut flags = global_ctx.config.get_flags();
flags.bind_device = false;
global_ctx.config.set_flags(flags);
let mut connector = HttpTunnelConnector::new(test_url.clone(), global_ctx.clone());
let mut listener = TcpTunnelListener::new("tcp://0.0.0.0:25888".parse().unwrap());
listener.listen().await.unwrap();
let task = tokio::spawn(async move {
let _conn = listener.accept().await.unwrap();
});
let t = connector.connect().await.unwrap();
assert_eq!(connector.redirect_type, test_type);
let info = t.info().unwrap();
let remote_addr = info.remote_addr.unwrap();
assert_eq!(remote_addr, test_url.into());
tokio::join!(task).0.unwrap();
tokio::join!(http_task).0.unwrap().unwrap();
}
}

View File

@@ -3,6 +3,8 @@ use std::{
sync::Arc,
};
use http_connector::HttpTunnelConnector;
#[cfg(feature = "quic")]
use crate::tunnel::quic::QUICTunnelConnector;
#[cfg(feature = "wireguard")]
@@ -19,7 +21,7 @@ pub mod direct;
pub mod manual;
pub mod udp_hole_punch;
mod http_connector;
pub mod http_connector;
async fn set_bind_addr_for_peer_connector(
connector: &mut (impl TunnelConnector + ?Sized),
@@ -81,6 +83,10 @@ pub async fn create_connector_by_url(
}
return Ok(Box::new(connector));
}
"http" | "https" => {
let connector = HttpTunnelConnector::new(url, global_ctx.clone());
return Ok(Box::new(connector));
}
"ring" => {
check_scheme_and_get_socket_addr::<uuid::Uuid>(&url, "ring")?;
let connector = RingTunnelConnector::new(url);

View File

@@ -201,9 +201,21 @@ where
Ok(T::from_url(url.clone(), IpVersion::Both)?)
}
fn default_port(scheme: &str) -> Option<u16> {
match scheme {
"tcp" => Some(11010),
"udp" => Some(11010),
"ws" => Some(11011),
"wss" => Some(11012),
"quic" => Some(11012),
"wg" => Some(11011),
_ => None,
}
}
impl FromUrl for SocketAddr {
fn from_url(url: url::Url, ip_version: IpVersion) -> Result<Self, TunnelError> {
let addrs = url.socket_addrs(|| None)?;
let addrs = url.socket_addrs(|| default_port(url.scheme()))?;
tracing::debug!(?addrs, ?ip_version, ?url, "convert url to socket addrs");
let addrs = addrs
.into_iter()