0.1.1
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-Clink-arg=-fuse-ld=/usr/local/bin/mold", "-Clink-arg=-Wl,--no-rosegment"]
|
||||
|
||||
+37
-10
@@ -22,7 +22,7 @@ use tokio::{
|
||||
use tower_http::cors::CorsLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use tracing::{debug, info};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
@@ -30,12 +30,20 @@ use crate::{
|
||||
webrtc_ingest::handle_whip_injest,
|
||||
};
|
||||
|
||||
const MAX_USERNAME_LEN: usize = 32;
|
||||
const MAX_LABEL_LEN: usize = 64;
|
||||
|
||||
pub struct HttpServerConfig {
|
||||
pub signup_code: String,
|
||||
}
|
||||
|
||||
pub struct HttpServer {
|
||||
pub offer_tx: Sender<(i32, i32, String)>,
|
||||
pub accept_rx: async_broadcast::InactiveReceiver<(i32, Option<String>)>,
|
||||
pub appstate: Arc<Mutex<AppState>>,
|
||||
pub request_count: Mutex<i32>,
|
||||
pub db: DatabaseConnection,
|
||||
pub config: HttpServerConfig,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
@@ -73,7 +81,7 @@ impl HttpServer {
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
||||
let listener = TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
@@ -175,12 +183,18 @@ async fn create_stream_key_handler(
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
if payload.label.is_empty() || payload.label.len() > MAX_LABEL_LEN {
|
||||
warn!(user_id = auth.0.id, label_len = payload.label.len(), max = MAX_LABEL_LEN, "stream key creation rejected: label length invalid");
|
||||
return StatusCode::UNPROCESSABLE_ENTITY;
|
||||
}
|
||||
if key_amount >= auth.0.stream_key_limit.try_into().unwrap() {
|
||||
warn!(user_id = auth.0.id, limit = auth.0.stream_key_limit, "stream key limit reached");
|
||||
return StatusCode::NOT_ACCEPTABLE;
|
||||
}
|
||||
let key = stream_key::Entity::create(&state.db, auth.0.id, value, payload.label, false).await;
|
||||
|
||||
if let Ok(_key) = key {
|
||||
if let Ok(ref k) = key {
|
||||
info!(user_id = auth.0.id, stream_key_id = k.id, label = %k.label, "stream key created");
|
||||
StatusCode::CREATED
|
||||
} else {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
@@ -226,15 +240,17 @@ async fn login_handler(
|
||||
State(state): State<Arc<HttpServer>>,
|
||||
Json(payload): Json<LoginForm>,
|
||||
) -> impl IntoResponse {
|
||||
if let Ok(x) = users::Entity::find_by_username(&state.db, payload.username).await {
|
||||
if let Ok(x) = users::Entity::find_by_username(&state.db, payload.username.clone()).await {
|
||||
if let Some(x) = x {
|
||||
let pass = verify_password(&payload.password, &x.hashed_password);
|
||||
if !pass {
|
||||
warn!(username = %payload.username, "login failed: wrong password");
|
||||
let mut meow = Response::new("".to_string());
|
||||
*meow.status_mut() = StatusCode::UNAUTHORIZED;
|
||||
return meow;
|
||||
};
|
||||
let auth = auth_session::Entity::create(&state.db, x.id).await.unwrap(); // This should be ok (hopefully)
|
||||
info!(user_id = x.id, username = %x.username, "login successful");
|
||||
let auth = auth_session::Entity::create(&state.db, x.id).await.unwrap();
|
||||
let token = auth.value;
|
||||
let mut meow = Response::new("".to_string());
|
||||
meow.headers_mut().insert(
|
||||
@@ -246,11 +262,13 @@ async fn login_handler(
|
||||
*meow.status_mut() = StatusCode::OK;
|
||||
return meow;
|
||||
} else {
|
||||
warn!(username = %payload.username, "login failed: user not found");
|
||||
let mut meow = Response::new("".to_string());
|
||||
*meow.status_mut() = StatusCode::UNAUTHORIZED;
|
||||
return meow;
|
||||
}
|
||||
} else {
|
||||
warn!(username = %payload.username, "login failed: DB error");
|
||||
let mut meow = Response::new("".to_string());
|
||||
*meow.status_mut() = StatusCode::UNAUTHORIZED;
|
||||
return meow;
|
||||
@@ -268,23 +286,30 @@ async fn create_user_handler(
|
||||
State(state): State<Arc<HttpServer>>,
|
||||
Json(payload): Json<CreateUserForm>,
|
||||
) -> (HeaderMap, StatusCode) {
|
||||
if payload.ref_token != "TEST" {
|
||||
if state.config.signup_code.is_empty() || payload.ref_token != state.config.signup_code {
|
||||
warn!(username = %payload.username, "signup rejected: invalid signup code");
|
||||
return (HeaderMap::new(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
if payload.username.is_empty() || payload.username.len() > MAX_USERNAME_LEN {
|
||||
warn!(username = %payload.username, max = MAX_USERNAME_LEN, "signup rejected: username length invalid");
|
||||
return (HeaderMap::new(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
let meow = users::Entity::create(
|
||||
&state.db,
|
||||
payload.username,
|
||||
payload.username.clone(),
|
||||
hash_password(&payload.password).unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create session
|
||||
let session: auth_session::Model = if let Ok(meow) = meow {
|
||||
auth_session::Entity::create(&state.db, meow.id)
|
||||
let session: auth_session::Model = if let Ok(ref user) = meow {
|
||||
info!(user_id = user.id, username = %user.username, "user created");
|
||||
auth_session::Entity::create(&state.db, user.id)
|
||||
.await
|
||||
.unwrap() // This should be ok (hopefully)
|
||||
.unwrap()
|
||||
} else {
|
||||
warn!(username = %payload.username, "user creation failed (likely username conflict)");
|
||||
return (HeaderMap::new(), StatusCode::CONFLICT);
|
||||
};
|
||||
let token = session.value;
|
||||
@@ -319,6 +344,7 @@ async fn stream_handler(
|
||||
let stream_key_id = if let Some(id) = stream_key_id {
|
||||
id
|
||||
} else {
|
||||
warn!(slug = %slug, "WHEP request for unknown or inactive stream");
|
||||
return Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.header("content-type", "application/text")
|
||||
@@ -326,6 +352,7 @@ async fn stream_handler(
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
info!(request_id = request_id_clone, slug = %slug, stream_key_id, "WHEP offer received");
|
||||
let mut accept_rx = state.accept_rx.activate_cloned();
|
||||
let _ = state
|
||||
.offer_tx
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{error::Error, sync::Arc};
|
||||
use tracing::{level_filters::LevelFilter, warn};
|
||||
use std::{env, error::Error, sync::Arc};
|
||||
use tracing::{info, level_filters::LevelFilter, warn};
|
||||
|
||||
use async_broadcast::broadcast;
|
||||
use dashmap::DashMap;
|
||||
@@ -23,8 +23,9 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::{
|
||||
audio::OpusAudioFrame,
|
||||
http::HttpServer,
|
||||
http::{HttpServer, HttpServerConfig},
|
||||
media::{H264Parser, VideoFrame},
|
||||
webrtc_proxy::WebRtcProxyConfig,
|
||||
};
|
||||
|
||||
mod audio;
|
||||
@@ -34,6 +35,7 @@ mod media;
|
||||
mod rtmp;
|
||||
mod webrtc;
|
||||
mod webrtc_ingest;
|
||||
mod webrtc_proxy;
|
||||
|
||||
// #[derive(Debug)]
|
||||
pub struct AppState {
|
||||
@@ -51,17 +53,22 @@ pub struct StreamSession {
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::DEBUG.into())
|
||||
.parse("")
|
||||
.from_env()
|
||||
// .parse("")
|
||||
.unwrap();
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let listener = TcpListener::bind("0.0.0.0:1935").await?;
|
||||
info!("RTMP listening on 0.0.0.0:1935");
|
||||
info!("HTTP API listening on 0.0.0.0:3000");
|
||||
|
||||
let db = Database::connect("sqlite://./db.sqlite?mode=rwc")
|
||||
let db = Database::connect("sqlite://./db/db.sqlite?mode=rwc")
|
||||
.await
|
||||
.unwrap();
|
||||
info!("database connected");
|
||||
Migrator::up(&db, None).await.unwrap();
|
||||
info!("migrations complete");
|
||||
|
||||
stream_session::Model::clean_unended_streams(&db)
|
||||
.await
|
||||
@@ -85,15 +92,31 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
appstate: appstate.clone(),
|
||||
request_count: Mutex::new(0),
|
||||
db: db.clone(),
|
||||
config: HttpServerConfig {
|
||||
signup_code: env::var("SIGNUP_CODE").unwrap_or_else(|_| {
|
||||
warn!("SIGNUP_CODE not set; signup will be disabled");
|
||||
String::new()
|
||||
}),
|
||||
},
|
||||
};
|
||||
http.start()?;
|
||||
|
||||
let proxyconfig = WebRtcProxyConfig {
|
||||
proxy_port: env::var("RTC_PORT")
|
||||
.unwrap_or("6969".into())
|
||||
.parse()
|
||||
.expect("RTC_PORT needs to be a number (i32)"),
|
||||
};
|
||||
let proxy = webrtc_proxy::WebrtcProxy::new(proxyconfig).await.unwrap();
|
||||
proxy.start().unwrap();
|
||||
|
||||
let app = appstate.lock().await;
|
||||
let webrtc = webrtc::Webrtc {
|
||||
offer_rx,
|
||||
accept_tx: answer_tx,
|
||||
sessions_ref: app.stream_sessions.clone(),
|
||||
db: db.clone(),
|
||||
proxy: proxy.into(),
|
||||
};
|
||||
webrtc.start()?;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{TcpListener, TcpStream},
|
||||
};
|
||||
use tracing::warn;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
StreamSession,
|
||||
@@ -72,8 +72,10 @@ impl Rtmp {
|
||||
} = self;
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let (socket, peer_addr) = listener.accept().await.unwrap();
|
||||
info!(%peer_addr, "RTMP connection accepted");
|
||||
let (mut session, mut socket) = Rtmp::handshake(socket).await.unwrap();
|
||||
info!(%peer_addr, "RTMP handshake complete");
|
||||
let (mut video_tx, mut video_rx) = broadcast::<Arc<VideoFrame>>(32);
|
||||
let (mut audio_tx, mut audio_rx) = broadcast::<Arc<OpusAudioFrame>>(32);
|
||||
// video_rx.cycle
|
||||
@@ -87,10 +89,13 @@ impl Rtmp {
|
||||
let stream_sessions = stream_sessions.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// let video_rx = video_rx;
|
||||
loop {
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = socket.read(&mut buf).await.unwrap();
|
||||
if n == 0 {
|
||||
debug!("RTMP connection closed by peer");
|
||||
return;
|
||||
}
|
||||
let events = session.handle_input(&buf[..n]).unwrap();
|
||||
// Blankly using it, so it doesnt drop
|
||||
video_rx.is_closed();
|
||||
@@ -105,6 +110,7 @@ impl Rtmp {
|
||||
ServerSessionEvent::ConnectionRequested {
|
||||
request_id, ..
|
||||
} => {
|
||||
debug!("RTMP ConnectionRequested, accepting");
|
||||
let reply = session.accept_request(request_id).unwrap();
|
||||
write_outbound(&mut socket, reply).await;
|
||||
}
|
||||
@@ -113,6 +119,7 @@ impl Rtmp {
|
||||
stream_key,
|
||||
..
|
||||
} => {
|
||||
info!(stream_key = %stream_key, "publish stream requested");
|
||||
let key = entity::stream_key::Entity::find_by_key(
|
||||
&db,
|
||||
&stream_key,
|
||||
@@ -122,6 +129,7 @@ impl Rtmp {
|
||||
let key = if let Ok(Some(key)) = key {
|
||||
key
|
||||
} else {
|
||||
warn!(stream_key = %stream_key, "stream key not found, rejecting");
|
||||
let reply = session
|
||||
.reject_request(
|
||||
request_id,
|
||||
@@ -140,6 +148,7 @@ impl Rtmp {
|
||||
.await
|
||||
.unwrap();
|
||||
if already_live.is_some() {
|
||||
warn!(stream_key_id = key.id, label = %key.label, "stream key already live, rejecting duplicate publish");
|
||||
let reply = session
|
||||
.reject_request(
|
||||
request_id,
|
||||
@@ -151,6 +160,7 @@ impl Rtmp {
|
||||
break;
|
||||
}
|
||||
|
||||
info!(stream_key_id = key.id, label = %key.label, "stream started");
|
||||
stream_sessions.insert(
|
||||
key.id,
|
||||
StreamSession {
|
||||
@@ -175,6 +185,7 @@ impl Rtmp {
|
||||
stream_key,
|
||||
..
|
||||
} => {
|
||||
info!(stream_key = %stream_key, "publish stream finished");
|
||||
let key = entity::stream_key::Entity::find_by_key(
|
||||
&db,
|
||||
&stream_key,
|
||||
@@ -214,12 +225,10 @@ impl Rtmp {
|
||||
..
|
||||
} => {
|
||||
// Consume the non-Send error before any await point.
|
||||
let opus_frames: Vec<_> = aac_parser
|
||||
.parse(&data, timestamp.value)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|f| audio_proc.encode(f))
|
||||
.unwrap_or_default();
|
||||
let opus_frames: Vec<_> = match aac_parser.parse(&data, timestamp.value) {
|
||||
Err(e) => { warn!("AAC parse error: {}", e); vec![] }
|
||||
Ok(frame) => frame.map(|f| audio_proc.encode(f)).unwrap_or_default(),
|
||||
};
|
||||
for frame in opus_frames {
|
||||
audio_tx.broadcast(Arc::new(frame)).await.ok();
|
||||
}
|
||||
|
||||
+26
-14
@@ -1,8 +1,10 @@
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use entity::stream_key;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait};
|
||||
use std::{
|
||||
error::Error,
|
||||
net::SocketAddr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -19,12 +21,13 @@ use str0m::{
|
||||
net::{Protocol, Receive},
|
||||
};
|
||||
|
||||
use crate::{StreamSession, audio::OpusAudioFrame, media::VideoFrame};
|
||||
use crate::{StreamSession, audio::OpusAudioFrame, media::VideoFrame, webrtc_proxy::WebrtcProxy};
|
||||
|
||||
pub struct Webrtc {
|
||||
pub offer_rx: Receiver<(i32, i32, String)>,
|
||||
pub accept_tx: async_broadcast::Sender<(i32, Option<String>)>,
|
||||
pub sessions_ref: Arc<DashMap<i32, StreamSession>>,
|
||||
pub proxy: Arc<WebrtcProxy>,
|
||||
pub db: DatabaseConnection,
|
||||
}
|
||||
|
||||
@@ -42,12 +45,20 @@ impl Webrtc {
|
||||
.await;
|
||||
|
||||
if let Ok(None) = stream_key {
|
||||
warn!(request_id, stream_id, "stream key not found in DB, rejecting offer");
|
||||
self.accept_tx.broadcast((request_id, None)).await.unwrap();
|
||||
continue;
|
||||
}
|
||||
if let Err(ref e) = stream_key {
|
||||
warn!(request_id, stream_id, "DB error looking up stream key: {:?}", e);
|
||||
self.accept_tx.broadcast((request_id, None)).await.unwrap();
|
||||
continue;
|
||||
}
|
||||
|
||||
let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let local_addr = socket.local_addr().unwrap();
|
||||
// let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
// let local_addr = socket.local_addr().unwrap();
|
||||
|
||||
let local_addr = self.proxy.public_addr();
|
||||
|
||||
let mut builder = Rtc::builder();
|
||||
{
|
||||
@@ -78,6 +89,7 @@ impl Webrtc {
|
||||
Some("0".to_string()),
|
||||
None,
|
||||
);
|
||||
changes.add_channel("meow".into());
|
||||
let offer_answer = match changes.accept_offer(offer_sdp) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
@@ -92,6 +104,7 @@ impl Webrtc {
|
||||
.and_then(|l| l.strip_prefix("a=ice-ufrag:"))
|
||||
.map(|s| s.trim().to_string());
|
||||
info!("Serving webrtc ufrag: {:?}", ufrag);
|
||||
let (socket, rx) = self.proxy.add_client(ufrag.unwrap());
|
||||
|
||||
debug!(request_id, "sending answer back");
|
||||
self.accept_tx
|
||||
@@ -100,8 +113,9 @@ impl Webrtc {
|
||||
.unwrap();
|
||||
|
||||
let sessions_ref = self.sessions_ref.clone();
|
||||
let public_addr = self.proxy.public_addr();
|
||||
tokio::spawn(async move {
|
||||
Webrtc::detach_connection(socket, rtc, sessions_ref, stream_id, mid).await;
|
||||
Webrtc::detach_connection(socket, rx, rtc, sessions_ref, stream_id, mid, public_addr).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -109,11 +123,13 @@ impl Webrtc {
|
||||
}
|
||||
|
||||
async fn detach_connection(
|
||||
socket: UdpSocket,
|
||||
socket: Arc<UdpSocket>,
|
||||
mut rx: Receiver<(Bytes, SocketAddr)>,
|
||||
mut rtc: Rtc,
|
||||
sessions_ref: Arc<DashMap<i32, StreamSession>>,
|
||||
stream_id: i32,
|
||||
_hint_mid: Mid,
|
||||
local_addr: SocketAddr,
|
||||
) {
|
||||
let mut video_mid: Option<Mid> = None;
|
||||
let mut video_pt = None;
|
||||
@@ -124,9 +140,6 @@ impl Webrtc {
|
||||
let mut audio_stream: Option<async_broadcast::Receiver<Arc<OpusAudioFrame>>> = None;
|
||||
let mut saw_keyframe = false;
|
||||
|
||||
let mut recv_buf = vec![0u8; PER_CLIENT_CONNECTION_BUF];
|
||||
let local_addr = socket.local_addr().unwrap();
|
||||
|
||||
loop {
|
||||
let deadline = loop {
|
||||
match rtc.poll_output() {
|
||||
@@ -251,11 +264,11 @@ impl Webrtc {
|
||||
}
|
||||
Err(async_broadcast::TryRecvError::Empty) => break,
|
||||
Err(async_broadcast::TryRecvError::Closed) => {
|
||||
info!("video channel closed, stream ended");
|
||||
warn!("video channel closed, stream ended");
|
||||
break;
|
||||
}
|
||||
Err(async_broadcast::TryRecvError::Overflowed(_)) => {
|
||||
saw_keyframe = false;
|
||||
// saw_keyframe = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -317,10 +330,9 @@ impl Webrtc {
|
||||
return;
|
||||
}
|
||||
}
|
||||
result = socket.recv_from(&mut recv_buf) => {
|
||||
if let Ok((n, from)) = result {
|
||||
let data = recv_buf[..n].to_vec();
|
||||
if let Ok(contents) = data.as_slice().try_into() {
|
||||
result = rx.recv() => {
|
||||
if let Some((data, from)) = result {
|
||||
if let Ok(contents) = (&data[..]).try_into() {
|
||||
if let Err(e) = rtc.handle_input(Input::Receive(
|
||||
Instant::now(),
|
||||
Receive {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use std::{env, error::Error, net::SocketAddr, sync::Arc};
|
||||
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use str0m::net::DatagramRecv;
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
sync::mpsc::{self, Receiver},
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
pub struct WebRtcProxyConfig {
|
||||
pub proxy_port: i32,
|
||||
}
|
||||
|
||||
pub struct WebrtcProxy {
|
||||
clients_ufrag: Arc<DashMap<String, tokio::sync::mpsc::Sender<(Bytes, SocketAddr)>>>,
|
||||
clients_addr: Arc<DashMap<SocketAddr, tokio::sync::mpsc::Sender<(Bytes, SocketAddr)>>>,
|
||||
socket: Arc<UdpSocket>,
|
||||
public_addr: SocketAddr,
|
||||
}
|
||||
|
||||
const STUN_MAGIC: u32 = 0x2112A442;
|
||||
|
||||
impl WebrtcProxy {
|
||||
pub async fn new(config: WebRtcProxyConfig) -> Result<Self, Box<dyn Error>> {
|
||||
let sock = UdpSocket::bind(format!("0.0.0.0:{}", config.proxy_port)).await?;
|
||||
let port = sock.local_addr()?.port();
|
||||
|
||||
let public_ip = match env::var("PUBLIC_DOMAIN") {
|
||||
Ok(domain) => {
|
||||
let ip = resolve_domain(&domain).await?;
|
||||
info!(%domain, %ip, "resolved PUBLIC_DOMAIN for WebRTC candidates");
|
||||
ip
|
||||
}
|
||||
Err(_) => {
|
||||
let ip = stun_public_ip().await?;
|
||||
info!(%ip, "discovered public IP via STUN for WebRTC candidates");
|
||||
ip
|
||||
}
|
||||
};
|
||||
let public_addr = SocketAddr::new(public_ip, port);
|
||||
info!(%public_addr, "WebRTC UDP proxy listening");
|
||||
|
||||
Ok(WebrtcProxy {
|
||||
socket: Arc::new(sock),
|
||||
clients_ufrag: Arc::new(DashMap::new()),
|
||||
clients_addr: Arc::new(DashMap::new()),
|
||||
public_addr,
|
||||
})
|
||||
}
|
||||
pub fn start(&self) -> Result<(), Box<dyn Error>> {
|
||||
// let self_arc = Arc::new(self);
|
||||
let by_ufrag = self.clients_ufrag.clone();
|
||||
let by_addr = self.clients_addr.clone();
|
||||
let socket = self.socket.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut buf = vec![0u8; 65535];
|
||||
loop {
|
||||
let (b, from) = match socket.recv_from(&mut buf).await {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
warn!("proxy couldnt eat data from socket, (({}))", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let data = Bytes::copy_from_slice(&buf[..b]);
|
||||
|
||||
// By addr
|
||||
if let Some(tx) = by_addr.get(&from) {
|
||||
match tx.try_send((data, from)) {
|
||||
Ok(_) => continue,
|
||||
Err(e) => {
|
||||
match e {
|
||||
mpsc::error::TrySendError::Full(_) => continue,
|
||||
mpsc::error::TrySendError::Closed(_) => {
|
||||
// the rv is ded
|
||||
drop(tx);
|
||||
by_addr.remove(&from);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let Some(ufrag) = self::WebrtcProxy::ufrag(&data) else {
|
||||
debug!("huh, packet isnt stun or added as client.");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some((_, tx)) = by_ufrag.remove(&ufrag) else {
|
||||
warn!("STUN packet ({}), isnt registored", ufrag);
|
||||
continue;
|
||||
};
|
||||
|
||||
by_addr.insert(from, tx.clone());
|
||||
debug!("got ufrag {}", ufrag);
|
||||
debug!("sending data");
|
||||
if let Err(e) = tx.try_send((data, from)) {
|
||||
match e {
|
||||
mpsc::error::TrySendError::Full(_) => {
|
||||
error!("Channel full")
|
||||
}
|
||||
mpsc::error::TrySendError::Closed(_) => {
|
||||
error!("Channel is closed");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
pub fn add_client(&self, ufrag: String) -> (Arc<UdpSocket>, Receiver<(Bytes, SocketAddr)>) {
|
||||
debug!("Added client {}", ufrag);
|
||||
let (tx, rx) = mpsc::channel(256);
|
||||
self.clients_ufrag.insert(ufrag, tx);
|
||||
(self.socket.clone(), rx)
|
||||
}
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.socket.local_addr().unwrap()
|
||||
}
|
||||
|
||||
pub fn public_addr(&self) -> SocketAddr {
|
||||
self.public_addr
|
||||
}
|
||||
pub fn ufrag(b: &Bytes) -> Option<String> {
|
||||
if b.len() <= 20 {
|
||||
return None;
|
||||
}
|
||||
let magic = u32::from_be_bytes(b[4..8].try_into().ok()?);
|
||||
if magic != STUN_MAGIC {
|
||||
return None;
|
||||
}
|
||||
// attribies start at 20
|
||||
let mut pos = 20usize;
|
||||
while (pos + 4) <= b.len() {
|
||||
let attr_type: u16 = u16::from_be_bytes(b[pos..pos + 2].try_into().ok()?);
|
||||
let attr_len: u16 = u16::from_be_bytes(b[pos + 2..pos + 4].try_into().ok()?);
|
||||
pos = pos + 4;
|
||||
if attr_type == 0x0006 {
|
||||
let value = std::str::from_utf8(b[pos..pos + (attr_len as usize)].try_into().ok()?);
|
||||
let local = value.unwrap().split(":").next();
|
||||
return Some(local.unwrap().to_string());
|
||||
}
|
||||
pos += (attr_len as usize + 3) & !3;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_domain(domain: &str) -> Result<std::net::IpAddr, Box<dyn Error>> {
|
||||
let addr = tokio::net::lookup_host(format!("{}:0", domain))
|
||||
.await?
|
||||
.find(|a| a.is_ipv4())
|
||||
.ok_or_else(|| format!("no IPv4 address found for {}", domain))?;
|
||||
Ok(addr.ip())
|
||||
}
|
||||
|
||||
// Send a STUN Binding Request to a public STUN server and extract our public IP
|
||||
// from the XOR-MAPPED-ADDRESS attribute in the response.
|
||||
async fn stun_public_ip() -> Result<std::net::IpAddr, Box<dyn Error>> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
sock.connect("stun.l.google.com:19302").await?;
|
||||
|
||||
// Build a minimal STUN Binding Request (RFC 5389).
|
||||
// Header: type(2) | length(2) | magic(4) | transaction-id(12)
|
||||
let mut req = [0u8; 20];
|
||||
req[0..2].copy_from_slice(&0x0001u16.to_be_bytes()); // Binding Request
|
||||
req[2..4].copy_from_slice(&0u16.to_be_bytes()); // no attributes
|
||||
req[4..8].copy_from_slice(&STUN_MAGIC.to_be_bytes());
|
||||
req[8..20].copy_from_slice(b"rtmp2whip_tx"); // transaction ID (12 bytes)
|
||||
|
||||
sock.send(&req).await?;
|
||||
|
||||
let mut buf = [0u8; 512];
|
||||
let n = tokio::time::timeout(std::time::Duration::from_secs(5), sock.recv(&mut buf)).await??;
|
||||
|
||||
let data = &buf[..n];
|
||||
parse_xor_mapped_address(data).ok_or("no XOR-MAPPED-ADDRESS in STUN response".into())
|
||||
}
|
||||
|
||||
// Parse XOR-MAPPED-ADDRESS (0x0020) from a STUN response.
|
||||
// The IP is XOR'd with the magic cookie (IPv4) or magic+transaction-id (IPv6).
|
||||
fn parse_xor_mapped_address(data: &[u8]) -> Option<std::net::IpAddr> {
|
||||
if data.len() < 20 {
|
||||
return None;
|
||||
}
|
||||
let magic = u32::from_be_bytes(data[4..8].try_into().ok()?);
|
||||
if magic != STUN_MAGIC {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut pos = 20usize;
|
||||
while pos + 4 <= data.len() {
|
||||
let attr_type = u16::from_be_bytes(data[pos..pos + 2].try_into().ok()?);
|
||||
let attr_len = u16::from_be_bytes(data[pos + 2..pos + 4].try_into().ok()?) as usize;
|
||||
pos += 4;
|
||||
if pos + attr_len > data.len() {
|
||||
break;
|
||||
}
|
||||
if attr_type == 0x0020 && attr_len >= 8 {
|
||||
// byte 0: reserved, byte 1: family (0x01=IPv4, 0x02=IPv6)
|
||||
let family = data[pos + 1];
|
||||
let x_port = u16::from_be_bytes(data[pos + 2..pos + 4].try_into().ok()?);
|
||||
let _ = x_port ^ (STUN_MAGIC >> 16) as u16; // port (unused here)
|
||||
|
||||
if family == 0x01 {
|
||||
let x_addr = u32::from_be_bytes(data[pos + 4..pos + 8].try_into().ok()?);
|
||||
let addr = x_addr ^ STUN_MAGIC;
|
||||
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(addr)));
|
||||
}
|
||||
}
|
||||
pos += (attr_len + 3) & !3;
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user