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:
Sijie.Sun
2024-10-26 00:04:22 +08:00
committed by GitHub
parent b5c3726e67
commit a78b759741
33 changed files with 1539 additions and 263 deletions

View File

@@ -4,4 +4,25 @@ version = "0.1.0"
edition = "2021"
[dependencies]
easytier = { path = "../easytier" }
easytier = { path = "../easytier" }
tracing = { version = "0.1", features = ["log"] }
anyhow = { version = "1.0" }
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
dashmap = "6.1"
url = "2.2"
async-trait = "0.1"
axum = { version = "0.7", features = ["macros"] }
clap = { version = "4.4.8", features = [
"string",
"unicode",
"derive",
"wrap_help",
] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }

View 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);
}
}

View 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()
}
}

View 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())
}
}

View File

@@ -1,3 +1,22 @@
fn main() {
println!("Hello, world!");
#![allow(dead_code)]
use std::sync::Arc;
use easytier::tunnel::udp::UdpTunnelListener;
mod client_manager;
mod restful;
#[tokio::main]
async fn main() {
let listener = UdpTunnelListener::new("udp://0.0.0.0:22020".parse().unwrap());
let mut mgr = client_manager::ClientManager::new();
mgr.serve(listener).await.unwrap();
let mgr = Arc::new(mgr);
let mut restful_server =
restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone());
restful_server.start().await.unwrap();
tokio::signal::ctrl_c().await.unwrap();
}

View File

@@ -0,0 +1,246 @@
use std::vec;
use std::{net::SocketAddr, sync::Arc};
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::routing::post;
use axum::{extract::State, routing::get, Json, Router};
use easytier::proto::{self, rpc_types, web::*};
use easytier::{common::scoped_task::ScopedTask, proto::rpc_types::controller::BaseController};
use tokio::net::TcpListener;
use crate::client_manager::session::Session;
use crate::client_manager::storage::StorageToken;
use crate::client_manager::ClientManager;
pub struct RestfulServer {
bind_addr: SocketAddr,
client_mgr: Arc<ClientManager>,
serve_task: Option<ScopedTask<()>>,
}
type AppStateInner = Arc<ClientManager>;
type AppState = State<AppStateInner>;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListSessionJsonResp(Vec<StorageToken>);
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ValidateConfigJsonReq {
config: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RunNetworkJsonReq {
config: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ColletNetworkInfoJsonReq {
inst_ids: Option<Vec<uuid::Uuid>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListNetworkInstanceIdsJsonResp(Vec<uuid::Uuid>);
type Error = proto::error::Error;
type ErrorKind = proto::error::error::ErrorKind;
type RpcError = rpc_types::error::Error;
type HttpHandleError = (StatusCode, Json<Error>);
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
let status_code = match &e {
RpcError::ExecutionError(_) => StatusCode::BAD_REQUEST,
RpcError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
_ => StatusCode::BAD_GATEWAY,
};
let error = Error::from(&e);
(status_code, Json(error))
}
impl RestfulServer {
pub fn new(bind_addr: SocketAddr, client_mgr: Arc<ClientManager>) -> Self {
assert!(client_mgr.is_running());
RestfulServer {
bind_addr,
client_mgr,
serve_task: None,
}
}
async fn get_session_by_machine_id(
client_mgr: &ClientManager,
machine_id: &uuid::Uuid,
) -> Result<Arc<Session>, HttpHandleError> {
let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else {
return Err((
StatusCode::NOT_FOUND,
Error {
error_kind: Some(ErrorKind::OtherError(proto::error::OtherError {
error_message: "No such session".to_string(),
})),
}
.into(),
));
};
Ok(result)
}
async fn handle_list_all_sessions(
State(client_mgr): AppState,
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
let ret = client_mgr.list_sessions().await;
Ok(ListSessionJsonResp(ret).into())
}
async fn handle_validate_config(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<ValidateConfigJsonReq>,
) -> Result<(), HttpHandleError> {
let config = payload.config;
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
c.validate_config(BaseController::default(), ValidateConfigRequest { config })
.await
.map_err(convert_rpc_error)?;
Ok(())
}
async fn handle_run_network_instance(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<RunNetworkJsonReq>,
) -> Result<(), HttpHandleError> {
let config = payload.config;
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
c.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest { config },
)
.await
.map_err(convert_rpc_error)?;
Ok(())
}
async fn handle_collect_one_network_info(
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.collect_network_info(
BaseController::default(),
CollectNetworkInfoRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
}
async fn handle_collect_network_info(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Query(payload): Query<ColletNetworkInfoJsonReq>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.collect_network_info(
BaseController::default(),
CollectNetworkInfoRequest {
inst_ids: payload
.inst_ids
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
},
)
.await
.map_err(convert_rpc_error)?;
Ok(ret.into())
}
async fn handle_list_network_instance_ids(
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
let ret = c
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
.await
.map_err(convert_rpc_error)?;
Ok(
ListNetworkInstanceIdsJsonResp(ret.inst_ids.into_iter().map(Into::into).collect())
.into(),
)
}
async fn handle_remove_network_instance(
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<(), HttpHandleError> {
let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?;
let c = result.scoped_rpc_client();
c.delete_network_instance(
BaseController::default(),
DeleteNetworkInstanceRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
Ok(())
}
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
let listener = TcpListener::bind(self.bind_addr).await.unwrap();
let app = Router::new()
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
.route(
"/api/v1/network/:machine-id/validate-config",
post(Self::handle_validate_config),
)
.route(
"/api/v1/network/:machine-id",
post(Self::handle_run_network_instance).get(Self::handle_list_network_instance_ids),
)
.route(
"/api/v1/network/:machine-id/info",
get(Self::handle_collect_network_info),
)
.route(
"/api/v1/network/:machine-id/:inst-id",
get(Self::handle_collect_one_network_info)
.delete(Self::handle_remove_network_instance),
)
.with_state(self.client_mgr.clone());
let task = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
self.serve_task = Some(task.into());
Ok(())
}
}