mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-13 21:27:25 +08:00
http redirector
This commit is contained in:
298
easytier/src/connector/http_connector.rs
Normal file
298
easytier/src/connector/http_connector.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user