mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-16 14:47:25 +08:00
feat/web (Patchset 2) (#444)
This patch implement a restful server without any auth.
usage:
```bash
# run easytier-web, which acts as an gateway and registry for all easytier-core
$> easytier-web
# run easytier-core and connect to easytier-web with a token
$> easytier-core --config-server udp://127.0.0.1:22020/fdsafdsa
# use restful api to list session
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/sessions
[{"token":"fdsafdsa","client_url":"udp://127.0.0.1:48915","machine_id":"de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f"}]%
# use restful api to run a network instance
$> curl -H "Content-Type: application/json" -X POST 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f -d '{"config": "listeners = [\"udp://0.0.0.0:12344\"]"}'
# use restful api to get network instance info
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f/65437e50-b286-4098-a624-74429f2cb839
```
This commit is contained in:
134
easytier-web/src/client_manager/mod.rs
Normal file
134
easytier-web/src/client_manager/mod.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::{common::scoped_task::ScopedTask, tunnel::TunnelListener};
|
||||
use session::Session;
|
||||
use storage::{Storage, StorageToken};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientManager {
|
||||
accept_task: Option<ScopedTask<()>>,
|
||||
clear_task: Option<ScopedTask<()>>,
|
||||
|
||||
client_sessions: Arc<DashMap<url::Url, Arc<Session>>>,
|
||||
storage: Storage,
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new() -> Self {
|
||||
ClientManager {
|
||||
accept_task: None,
|
||||
clear_task: None,
|
||||
|
||||
client_sessions: Arc::new(DashMap::new()),
|
||||
storage: Storage::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve<L: TunnelListener + 'static>(
|
||||
&mut self,
|
||||
mut listener: L,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
listener.listen().await?;
|
||||
|
||||
let sessions = self.client_sessions.clone();
|
||||
let storage = self.storage.weak_ref();
|
||||
let task = tokio::spawn(async move {
|
||||
while let Ok(tunnel) = listener.accept().await {
|
||||
let info = tunnel.info().unwrap();
|
||||
let client_url: url::Url = info.remote_addr.unwrap().into();
|
||||
println!("New session from {:?}", tunnel.info());
|
||||
let session = Session::new(tunnel, storage.clone(), client_url.clone());
|
||||
sessions.insert(client_url, Arc::new(session));
|
||||
}
|
||||
});
|
||||
|
||||
self.accept_task = Some(ScopedTask::from(task));
|
||||
|
||||
let sessions = self.client_sessions.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
||||
sessions.retain(|_, session| session.is_running());
|
||||
}
|
||||
});
|
||||
self.clear_task = Some(ScopedTask::from(task));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.accept_task.is_some() && self.clear_task.is_some()
|
||||
}
|
||||
|
||||
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
||||
let sessions = self
|
||||
.client_sessions
|
||||
.iter()
|
||||
.map(|item| item.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut ret: Vec<StorageToken> = vec![];
|
||||
for s in sessions {
|
||||
if let Some(t) = s.get_token().await {
|
||||
ret.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_session_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<Arc<Session>> {
|
||||
let c_url = self.storage.get_client_url_by_machine_id(machine_id)?;
|
||||
self.client_sessions
|
||||
.get(&c_url)
|
||||
.map(|item| item.value().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use easytier::{
|
||||
tunnel::{
|
||||
common::tests::wait_for_condition,
|
||||
udp::{UdpTunnelConnector, UdpTunnelListener},
|
||||
},
|
||||
web_client::WebClient,
|
||||
};
|
||||
|
||||
use crate::client_manager::ClientManager;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client() {
|
||||
let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap());
|
||||
let mut mgr = ClientManager::new();
|
||||
mgr.serve(Box::new(listener)).await.unwrap();
|
||||
|
||||
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
|
||||
let _c = WebClient::new(connector, "test");
|
||||
|
||||
wait_for_condition(
|
||||
|| async { mgr.client_sessions.len() == 1 },
|
||||
Duration::from_secs(6),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut a = mgr
|
||||
.client_sessions
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.data()
|
||||
.read()
|
||||
.await
|
||||
.heartbeat_waiter();
|
||||
let req = a.recv().await.unwrap();
|
||||
println!("{:?}", req);
|
||||
println!("{:?}", mgr);
|
||||
}
|
||||
}
|
||||
144
easytier-web/src/client_manager/session.rs
Normal file
144
easytier-web/src/client_manager/session.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use easytier::{
|
||||
proto::{
|
||||
rpc_impl::bidirect::BidirectRpcManager,
|
||||
rpc_types::{self, controller::BaseController},
|
||||
web::{
|
||||
HeartbeatRequest, HeartbeatResponse, WebClientService, WebClientServiceClientFactory,
|
||||
WebServerService, WebServerServiceServer,
|
||||
},
|
||||
},
|
||||
tunnel::Tunnel,
|
||||
};
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
|
||||
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionData {
|
||||
storage: WeakRefStorage,
|
||||
client_url: url::Url,
|
||||
|
||||
storage_token: Option<StorageToken>,
|
||||
notifier: broadcast::Sender<HeartbeatRequest>,
|
||||
req: Option<HeartbeatRequest>,
|
||||
}
|
||||
|
||||
impl SessionData {
|
||||
fn new(storage: WeakRefStorage, client_url: url::Url) -> Self {
|
||||
let (tx, _rx1) = broadcast::channel(2);
|
||||
|
||||
SessionData {
|
||||
storage,
|
||||
client_url,
|
||||
storage_token: None,
|
||||
notifier: tx,
|
||||
req: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn req(&self) -> Option<HeartbeatRequest> {
|
||||
self.req.clone()
|
||||
}
|
||||
|
||||
pub fn heartbeat_waiter(&self) -> broadcast::Receiver<HeartbeatRequest> {
|
||||
self.notifier.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SessionData {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(storage) = Storage::try_from(self.storage.clone()) {
|
||||
if let Some(token) = self.storage_token.as_ref() {
|
||||
storage.remove_client(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedSessionData = Arc<RwLock<SessionData>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SessionRpcService {
|
||||
data: SharedSessionData,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WebServerService for SessionRpcService {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
_: BaseController,
|
||||
req: HeartbeatRequest,
|
||||
) -> rpc_types::error::Result<HeartbeatResponse> {
|
||||
let mut data = self.data.write().await;
|
||||
if data.req.replace(req.clone()).is_none() {
|
||||
assert!(data.storage_token.is_none());
|
||||
data.storage_token = Some(StorageToken {
|
||||
token: req.user_token.clone().into(),
|
||||
client_url: data.client_url.clone(),
|
||||
machine_id: req
|
||||
.machine_id
|
||||
.clone()
|
||||
.map(Into::into)
|
||||
.unwrap_or(uuid::Uuid::new_v4()),
|
||||
});
|
||||
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
|
||||
storage.add_client(data.storage_token.as_ref().unwrap().clone());
|
||||
}
|
||||
}
|
||||
let _ = data.notifier.send(req);
|
||||
Ok(HeartbeatResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
rpc_mgr: BidirectRpcManager,
|
||||
|
||||
data: SharedSessionData,
|
||||
}
|
||||
|
||||
impl Debug for Session {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Session").field("data", &self.data).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(tunnel: Box<dyn Tunnel>, storage: WeakRefStorage, client_url: url::Url) -> Self {
|
||||
let rpc_mgr =
|
||||
BidirectRpcManager::new().set_rx_timeout(Some(std::time::Duration::from_secs(30)));
|
||||
rpc_mgr.run_with_tunnel(tunnel);
|
||||
|
||||
let data = Arc::new(RwLock::new(SessionData::new(storage, client_url)));
|
||||
|
||||
rpc_mgr.rpc_server().registry().register(
|
||||
WebServerServiceServer::new(SessionRpcService { data: data.clone() }),
|
||||
"",
|
||||
);
|
||||
|
||||
Session { rpc_mgr, data }
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.rpc_mgr.is_running()
|
||||
}
|
||||
|
||||
pub fn data(&self) -> SharedSessionData {
|
||||
self.data.clone()
|
||||
}
|
||||
|
||||
pub fn scoped_rpc_client(
|
||||
&self,
|
||||
) -> Box<dyn WebClientService<Controller = BaseController> + Send> {
|
||||
self.rpc_mgr
|
||||
.rpc_client()
|
||||
.scoped_client::<WebClientServiceClientFactory<BaseController>>(1, 1, "".to_string())
|
||||
}
|
||||
|
||||
pub async fn get_token(&self) -> Option<StorageToken> {
|
||||
self.data.read().await.storage_token.clone()
|
||||
}
|
||||
}
|
||||
72
easytier-web/src/client_manager/storage.rs
Normal file
72
easytier-web/src/client_manager/storage.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
|
||||
// use this to maintain Storage
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StorageToken {
|
||||
pub token: String,
|
||||
pub client_url: url::Url,
|
||||
pub machine_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StorageInner {
|
||||
// some map for indexing
|
||||
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
|
||||
pub machine_client_url_map: DashMap<uuid::Uuid, url::Url>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Storage(Arc<StorageInner>);
|
||||
pub type WeakRefStorage = Weak<StorageInner>;
|
||||
|
||||
impl TryFrom<WeakRefStorage> for Storage {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(weak: Weak<StorageInner>) -> Result<Self, Self::Error> {
|
||||
weak.upgrade().map(|inner| Storage(inner)).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new() -> Self {
|
||||
Storage(Arc::new(StorageInner {
|
||||
token_clients_map: DashMap::new(),
|
||||
machine_client_url_map: DashMap::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn add_client(&self, stoken: StorageToken) {
|
||||
let inner = self
|
||||
.0
|
||||
.token_clients_map
|
||||
.entry(stoken.token)
|
||||
.or_insert_with(DashSet::new);
|
||||
inner.insert(stoken.client_url.clone());
|
||||
|
||||
self.0
|
||||
.machine_client_url_map
|
||||
.insert(stoken.machine_id, stoken.client_url.clone());
|
||||
}
|
||||
|
||||
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||
self.0.token_clients_map.remove_if(&stoken.token, |_, set| {
|
||||
set.remove(&stoken.client_url);
|
||||
set.is_empty()
|
||||
});
|
||||
|
||||
self.0.machine_client_url_map.remove(&stoken.machine_id);
|
||||
}
|
||||
|
||||
pub fn weak_ref(&self) -> WeakRefStorage {
|
||||
Arc::downgrade(&self.0)
|
||||
}
|
||||
|
||||
pub fn get_client_url_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<url::Url> {
|
||||
self.0
|
||||
.machine_client_url_map
|
||||
.get(&machine_id)
|
||||
.map(|url| url.clone())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user