initial commit

This commit is contained in:
2024-02-27 10:07:50 +08:00
commit a6c0f916b7
17 changed files with 4929 additions and 0 deletions

57
src/bin/server.rs Normal file
View File

@ -0,0 +1,57 @@
use clap::Parser;
#[derive(clap::Parser)]
#[command(author, version, about, long_about = None)]
struct Opts {
#[command(subcommand)]
subcommand: Option<Subcommand>,
}
#[derive(clap::Subcommand)]
enum Subcommand {
Serve {
#[clap(long, short = 'l', default_value = "127.0.0.1", env)]
listen_address: String,
#[clap(long, short = 'p', default_value = "3000", env)]
listen_port: u16,
},
#[cfg(feature = "extract-static")]
Extract { destination: String },
}
impl Default for Subcommand {
fn default() -> Self {
Subcommand::Serve {
listen_address: "127.0.0.1".into(),
listen_port: 3000,
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let opts = Opts::parse();
tracing_subscriber::fmt::init();
match opts.subcommand.unwrap_or_default() {
Subcommand::Serve {
listen_address,
listen_port,
} => {
let service = hello::web::routes::make_service()?;
tracing::info!(listen_address, listen_port, "app is service");
let listener =
tokio::net::TcpListener::bind(format!("{listen_address}:{listen_port}")).await?;
axum::serve(listener, service).await?;
}
#[cfg(feature = "extract-static")]
Subcommand::Extract { destination } => {
staticfiles::extract(destination).await?;
}
}
Ok(())
}

3
src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod web;
pub mod types;
pub mod task;

51
src/task.rs Normal file
View File

@ -0,0 +1,51 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use webrtc::{ice_transport::{ice_candidate::RTCIceCandidate, ice_candidate_type::RTCIceCandidateType, ice_server::RTCIceServer}, *};
use lazy_static::lazy_static;
lazy_static! {
pub static ref MY_ADDRESS: Mutex<String> = Mutex::new(String::new());
}
pub async fn collect_local_address() -> Result<(), Box<dyn std::error::Error>> {
let SERVER_CONFIG: peer_connection::configuration::RTCConfiguration = peer_connection::configuration::RTCConfiguration {
ice_servers: vec![RTCIceServer {
urls: vec!["stun:nhz.jeffthecoder.xyz:3479".to_string()],
..Default::default()
}],
..Default::default()
};
let api = webrtc::api::APIBuilder::new().build();
let mut pc = api.new_peer_connection(SERVER_CONFIG.clone()).await?;
pc.on_ice_candidate(Box::new(|candicate: Option<RTCIceCandidate>|{
Box::pin(async {
tracing::info!(?candicate, "on_ice_candidate");
if let Some(c) = candicate {
if c.typ == RTCIceCandidateType::Srflx {
tracing::info!("+ {}", c.address);
let mut address = MY_ADDRESS.lock().await;
*address = c.address.clone();
} else {
tracing::info!(" {}", c.address);
}
} else {
tracing::debug!("gather done");
}
})
}));
let offer = pc.create_offer(None).await?;
pc.set_local_description(offer).await?;
let _dc = pc.create_data_channel("some_label", None).await?;
let mut gather_complete = pc.gathering_complete_promise().await;
gather_complete.recv().await;
Ok(())
}

11
src/types.rs Normal file
View File

@ -0,0 +1,11 @@
use serde::Serialize;
#[derive(Serialize, Default)]
pub struct PageContext {
pub address: String,
}
#[derive(Default, Clone, Debug)]
pub struct AppState {
pub templates: tera::Tera,
}

3
src/web.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod templates;
pub mod staticfiles;
pub mod routes;

75
src/web/routes.rs Normal file
View File

@ -0,0 +1,75 @@
use std::sync::Arc;
use axum::{
extract::State,
response::{ErrorResponse, Html, Redirect},
routing::{get, post, IntoMakeService},
Json, Router,
};
use serde::Serialize;
use tokio::sync::RwLock;
use crate::{
web::templates::load_templates,
types::{AppState, PageContext},
task::{MY_ADDRESS, collect_local_address},
};
async fn root(State(state): State<Arc<RwLock<AppState>>>) -> axum::response::Result<Html<String>> {
let state = state.read().await;
let address_guard = MY_ADDRESS.lock().await;
let address = address_guard.clone();
drop(address_guard);
let ctx = match tera::Context::from_serialize(&PageContext {
address,
}) {
Ok(ctx) => ctx,
Err(err) => return Err(ErrorResponse::from(format!("{err}"))),
};
match state.templates.render("index.html", &ctx) {
Ok(result) => Ok(Html::from(result)),
Err(err) => Err(ErrorResponse::from(format!("{err}"))),
}
}
#[derive(Serialize, Default)]
struct ReloadResult {
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
async fn reload(State(state): State<Arc<RwLock<AppState>>>) -> Result<Redirect, Json<ReloadResult>> {
let mut state = state.write_owned().await;
if let Err(err) = state.templates.full_reload() {
return Err(Json(ReloadResult {
error: Some(err.to_string()),
}));
}
drop(state);
if let Err(err) = collect_local_address().await {
return Err(Json(ReloadResult {
error: Some(err.to_string()),
}));
}
Ok(Redirect::to("/"))
}
pub fn make_service() -> Result<IntoMakeService<Router<()>>, Box<dyn std::error::Error>> {
let templates = load_templates()?;
let router = Router::new();
#[cfg(feature = "serve-static")]
let router = router.route_service("/static/*path", staticfiles::StaticFiles::strip("/static/"));
Ok(router
.route("/", get(root))
.route("/reload", post(reload))
.with_state(Arc::new(RwLock::new(crate::types::AppState {
templates: templates,
})))
.into_make_service())
}

155
src/web/staticfiles.rs Normal file
View File

@ -0,0 +1,155 @@
#[cfg(feature = "embed-static")]
static STATIC_DIR: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/static");
#[cfg(feature="extract-static")]
pub async fn extract<P: AsRef<std::path::Path>>(to: P) -> std::io::Result<()> {
let base_path = to.as_ref();
let mut dirs = vec![STATIC_DIR.clone()];
tracing::info!("extracing static assets...");
while let Some(dir) = dirs.pop() {
for entry in dir.entries() {
let path = base_path.join(entry.path());
match entry {
DirEntry::Dir(d) => {
tracing::trace!(dir=?d, "directory been put into queue");
tokio::fs::create_dir_all(&path).await?;
dirs.insert(0, d.clone());
}
DirEntry::File(f) => {
tracing::trace!(?path, "file extracted");
tokio::fs::write(path, f.contents()).await?;
}
}
}
}
Ok(())
}
#[cfg(feature="serve-static")]
pub use with_service::router;
#[cfg(feature="serve-static")]
pub use with_service::StaticFiles;
#[cfg(feature="serve-static")]
mod with_service {
use std::{convert::Infallible, task::Poll};
use axum::{
body::{Bytes, Full},
extract::Path,
http::{Request, StatusCode},
response::{IntoResponse, Response},
routing::{MethodFilter, MethodRouter},
};
use futures::Future;
use include_dir::Dir;
use super::STATIC_DIR;
async fn head(Path(static_path): Path<String>) -> Response {
if super::STATIC_DIR.contains(static_path) {
(StatusCode::OK, "").into_response()
} else {
(StatusCode::NOT_FOUND, "").into_response()
}
}
async fn get(Path(static_path): Path<String>) -> Response {
if let Some(file) = super::STATIC_DIR.get_file(static_path) {
(StatusCode::OK, file.contents()).into_response()
} else {
(StatusCode::NOT_FOUND, "").into_response()
}
}
#[derive(Clone)]
pub enum ResponseFuture {
OpenFileFuture(&'static [u8], String),
NotFoundFuture,
}
impl Future for ResponseFuture {
type Output = Result<Response, Infallible>;
fn poll(
self: std::pin::Pin<&mut Self>,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.clone() {
ResponseFuture::OpenFileFuture(content, mime_type) => {
Poll::Ready(Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", mime_type.clone())
.body(axum::body::boxed(Full::new(Bytes::copy_from_slice(
content,
))))
.unwrap()))
}
ResponseFuture::NotFoundFuture => {
Poll::Ready(Ok(StatusCode::NOT_FOUND.into_response()))
}
}
}
}
#[derive(Clone, Debug)]
pub struct StaticFiles(Dir<'static>, String);
impl<B> tower::Service<Request<B>> for StaticFiles
where
B: Send + 'static,
{
type Response = Response;
type Error = Infallible;
type Future = ResponseFuture;
fn poll_ready(
&mut self,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let path = if let Some(path) = req.uri().path().strip_prefix(&self.1) {
path
} else {
return ResponseFuture::NotFoundFuture;
};
let mime_type = mime_guess::from_path(path)
.first()
.map_or("application/octet-stream".to_string(), |m| {
m.essence_str().to_string()
});
tracing::info!(path, "trying to get static");
if let Some(file) = self.0.get_file(path) {
ResponseFuture::OpenFileFuture(file.contents(), mime_type.to_string())
} else {
ResponseFuture::NotFoundFuture
}
}
}
impl StaticFiles {
pub fn new() -> Self {
StaticFiles(STATIC_DIR.clone(), "/".to_string())
}
pub fn strip<S: ToString>(prefix: S) -> Self {
StaticFiles(STATIC_DIR.clone(), prefix.to_string())
}
}
pub fn router() -> MethodRouter {
MethodRouter::new()
.on(MethodFilter::HEAD, head)
.on(MethodFilter::GET, get)
}
}

30
src/web/templates.rs Normal file
View File

@ -0,0 +1,30 @@
#[cfg(feature = "embed-templates")]
static TEMPLATES_DIR: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/templates");
pub fn load_templates() -> Result<tera::Tera, Box<dyn std::error::Error>> {
let mut templates = tera::Tera::parse("templates/**/*.html")?;
#[cfg(feature = "embed-templates")]
{
let template_names: std::collections::HashSet<_> = templates
.get_template_names()
.map(|s| s.to_string())
.collect();
for entry in TEMPLATES_DIR.find("**/*.html")? {
if let Some(file) = entry.as_file() {
let path = file.path();
let path = path.to_string_lossy().to_string();
if template_names.contains(&path) {
continue;
}
if let Some(content) = file.contents_utf8() {
templates.add_raw_template(&path, content)?;
}
}
}
}
templates.build_inheritance_chains()?;
Ok(templates)
}