From ce80f47237d2f22092e5987596c441ed4c7cce21 Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Wed, 11 Sep 2024 22:19:16 +0800 Subject: [PATCH 1/7] feat: use turn rest api; --- docs/configure.md | 48 +++++++++++++++++++++++------ docs/rest-api.md | 1 + docs/web-hooks.md | 31 ++++++++++--------- tests/src/lib.rs | 4 +-- turn-server.toml | 19 ++++++++++-- turn-server/src/api.rs | 65 ++++++++++++++++++++++++++++++++++----- turn-server/src/config.rs | 20 +++++++++--- turn-server/src/lib.rs | 2 +- turn-server/src/main.rs | 2 +- 9 files changed, 148 insertions(+), 44 deletions(-) diff --git a/docs/configure.md b/docs/configure.md index 384f52e..4ead824 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -45,7 +45,7 @@ external = "127.0.0.1:3478" # environment. bind = "127.0.0.1:3000" -# web hooks url +# hooks url # # This option is used to specify the http address of the hooks service. # @@ -56,6 +56,19 @@ bind = "127.0.0.1:3000" # # hooks = "http://127.0.0.1:8080" +# Credentials used by the http interface, credentials are carried in the +# http request and are used to authenticate the request. +# +# credential = "" + +# Choose whether the hooks api follows the +# RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), +# if the use follows this RFC, then the hooks api will only keep the +# authentication functionality, other things like event push will be +# disabled. +# +# use_turn_rest_api = false + [log] # log level # @@ -70,8 +83,9 @@ level = "info" # The server will try to use static authentication first, and then use # external control service authentication. [auth] -user1 = "test" -user2 = "test" +# user1 = "test" +# user2 = "test" + ``` ## Configuration keys @@ -80,7 +94,7 @@ user2 = "test" ### `turn.realm` -* Type: strings +* Type: string * Default: "localhost" This option describes the realm of the turn service. For the definition of realm, please refer to [RFC](https://datatracker.ietf.org/doc/html/rfc5766#section-3). @@ -98,7 +112,7 @@ This option describes the interface to which the turn service is bound. A turn s ### `[turn.interfaces.transport]` -* Type: enum of strings +* Type: enum of string Describes the transport protocol used by the interface. The value can be `udp` or `tcp`, which correspond to udp turn and tcp turn respectively, and choose whether to bind the turn service to a udp socket or a tcp socket. @@ -106,7 +120,7 @@ Describes the transport protocol used by the interface. The value can be `udp` o ### `[turn.interfaces.bind]` -* Type: strings +* Type: string The IP address and port number bound to the interface. This is the address to which the internal socket is bound. @@ -114,7 +128,7 @@ The IP address and port number bound to the interface. This is the address to wh ### `[turn.interfaces.external]` -* Type: strings +* Type: string bind is used to bind to the address of your local NIC, for example, you have two NICs A and B on your server, the IP address of NIC A is 192.168.1.2, and the address of NIC B is 192.168.1.3, if you bind to NIC A, you should bind to the address of 192.168.1.2, and bind to 0.0.0.0 means that it listens to all of them at the same time. @@ -126,7 +140,7 @@ As for why bind and external are needed, this is because for the stun protocol, ### `api.bind` -* Type: strings +* Type: string * Default: "127.0.0.1:3000" Describes the address to which the turn api server is bound. @@ -139,7 +153,7 @@ The turn service provides an external REST API. External parties can control the ### `api.hooks` -* Type: strings +* Type: string * Default: None Describes the address of external Web Hooks. The default value is empty. The purpose of Web Hooks is to allow the turn service to push to external services when authentication is required and event updates occur. @@ -148,11 +162,25 @@ The turn service provides an external REST API. External parties can control the > Warning: The REST API does not provide any authentication or encryption measures. You need to run the turn service in a trusted network environment or add a proxy to increase authentication and encryption measures. +### `api.credential` + +* Type: string +* Default: None + +Credentials used by the http interface, credentials are carried in the http request and are used to authenticate the request. + +### `api.use_turn_rest_api` + +* Type: boolean +* Default: false + +Choose whether the hooks api follows the RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), if the use follows this RFC, then the hooks api will only keep the authentication functionality, other things like event push will be disabled. + *** ### `log.level` -* Type: enum of strings +* Type: enum of string * Default: "info" Describes the log level of the turn service. Possible values ​​are `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`. diff --git a/docs/rest-api.md b/docs/rest-api.md index b93d481..500fa0c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -3,6 +3,7 @@ #### Global Response Headers * `realm` - string - turn server realm +* `credential` - string - credentials used by the http interface * `rid` - string - The runtime ID of the turn server rid: A new ID is generated each time the server is started. This is a random string. Its main function is to determine whether the turn server has been restarted. diff --git a/docs/web-hooks.md b/docs/web-hooks.md index 6ec3ea6..bf07f10 100644 --- a/docs/web-hooks.md +++ b/docs/web-hooks.md @@ -3,6 +3,7 @@ #### Global Request Headers * `realm` - string - turn server realm +* `credential` - string - credentials used by the http interface * `rid` - string - The runtime ID of the turn server rid: A new ID is generated each time the server is started. This is a random string. Its main function is to determine whether the turn server has been restarted. @@ -20,38 +21,38 @@ Get the current user's password, which is mainly used to provide authentication binding request: * `kind` - string - "binding" -* `addr` - string +* `addr` - string - The IP address and port number of the UDP or TCP connection used by the client. allocate request: * `kind` - string - "allocated" -* `name` - string -* `addr` - string -* `port` - uint16 +* `name` - string - The username used for the turn session. +* `addr` - string - The IP address and port number of the UDP or TCP connection used by the client. +* `port` - uint16 - The port to which the request is assigned. channel binding request: * `kind` - string - "channel_bind" -* `name` - string -* `addr` - string -* `channel` - uint16 +* `name` - string - The username used for the turn session. +* `addr` - string - The IP address and port number of the UDP or TCP connection used by the client. +* `channel` - uint16 - The channel to which the request is binding. create permission request: * `kind` - string - "create_permission" -* `name` - string -* `addr` - string -* `channel` - uint16 +* `name` - string - The username used for the turn session. +* `addr` - string - The IP address and port number of the UDP or TCP connection used by the client. +* `relay` - uint16 - The port number of the other side specified when the privilege was created. refresh request: * `kind` - string - "refresh" -* `name` - string -* `addr` - string -* `expiration` - uint32 +* `name` - string - The username used for the turn session. +* `addr` - string - The IP address and port number of the UDP or TCP connection used by the client. +* `expiration` - uint32 - Time to expiration in seconds. session closed: * `kind` - string - "abort" -* `name` - string -* `addr` - string +* `name` - string - The username used for the turn session. +* `addr` - string - The IP address and port number of the UDP or TCP connection used by the client. diff --git a/tests/src/lib.rs b/tests/src/lib.rs index c9cedf5..280df0c 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -12,7 +12,7 @@ use stun::{Decoder, Kind, MessageReader, MessageWriter, Method, Payload}; use tokio::net::UdpSocket; use turn_server::{ config::{self, *}, - server_main, + startup, }; use std::collections::HashMap; @@ -58,7 +58,7 @@ pub async fn create_turn() { // pass, so turn-server is used as a library here, and the server is // started with a custom configuration. tokio::spawn(async move { - server_main(Arc::new(Config { + startup(Arc::new(Config { auth, api: Api::default(), log: Log::default(), diff --git a/turn-server.toml b/turn-server.toml index e96fa1b..7f874d2 100644 --- a/turn-server.toml +++ b/turn-server.toml @@ -40,7 +40,7 @@ external = "127.0.0.1:3478" # environment. bind = "127.0.0.1:3000" -# web hooks url +# hooks url # # This option is used to specify the http address of the hooks service. # @@ -51,6 +51,19 @@ bind = "127.0.0.1:3000" # # hooks = "http://127.0.0.1:8080" +# Credentials used by the http interface, credentials are carried in the +# http request and are used to authenticate the request. +# +# credential = "" + +# Choose whether the hooks api follows the +# RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), +# if the use follows this RFC, then the hooks api will only keep the +# authentication functionality, other things like event push will be +# disabled. +# +# use_turn_rest_api = false + [log] # log level # @@ -65,5 +78,5 @@ level = "info" # The server will try to use static authentication first, and then use # external control service authentication. [auth] -user1 = "test" -user2 = "test" +# user1 = "test" +# user2 = "test" diff --git a/turn-server/src/api.rs b/turn-server/src/api.rs index dc30d59..cb4e603 100644 --- a/turn-server/src/api.rs +++ b/turn-server/src/api.rs @@ -175,6 +175,10 @@ pub async fn start_server( HeaderValue::from_str(&state.config.turn.realm).unwrap(), ); + if let Some(credential) = &state.config.api.credential { + headers.insert("Credential", HeaderValue::from_str(credential).unwrap()); + } + res }, )) @@ -198,6 +202,10 @@ impl HooksService { headers.insert("Realm", HeaderValue::from_str(&cfg.turn.realm)?); headers.insert("Rid", HeaderValue::from_str(&RID)?); + if let Some(credential) = &cfg.api.credential { + headers.insert("Credential", HeaderValue::from_str(credential)?); + } + let client = Arc::new( ClientBuilder::new() .default_headers(headers) @@ -229,14 +237,28 @@ impl HooksService { } if let Some(server) = &self.cfg.api.hooks { - if let Ok(res) = self - .client - .get(format!("{}/password?addr={}&name={}", server, addr, name)) - .send() - .await - { - if let Ok(password) = res.text().await { - return Some(password); + let url = if self.cfg.api.use_turn_rest_api { + if let Some(credential) = &self.cfg.api.credential { + format!( + "{}/?service=turn&username={}&key={}", + server, name, credential + ) + } else { + format!("{}/?service=turn&username={}", server, name) + } + } else { + format!("{}/password?addr={}&name={}", server, addr, name) + }; + + if let Ok(res) = self.client.get(url).send().await { + if self.cfg.api.use_turn_rest_api { + if let Ok(response) = res.json::().await { + return Some(response.password); + } + } else { + if let Ok(password) = res.text().await { + return Some(password); + } } } } @@ -245,6 +267,10 @@ impl HooksService { } pub fn send_event(&self, event: Value) { + if self.cfg.api.use_turn_rest_api { + return; + } + if self.cfg.api.hooks.is_some() { if let Err(e) = self.tx.send(event) { log::error!("failed to send event, err={}", e) @@ -252,3 +278,26 @@ impl HooksService { } } } + +#[allow(unused)] +#[derive(Deserialize, Debug)] +struct TurnRestApiResponse { + // the TURN username to use, which is a colon-delimited combination of the expiration timestamp + // and the username parameter from the request (if specified). The timestamp is intended to be + // opaque to the web application, so its format is arbitrary, but for simplicity, use of UNIX + // timestamps is recommended. + username: String, + // the TURN password to use; this value is computed from the a secret key shared with the TURN + // server and the returned username value, by performing base64(hmac(secret key, returned + // username)). HMAC-SHA1 is one HMAC algorithm that can be used, but any algorithm that + // incorporates a shared secret is acceptable, as long as both the web server and TURN server + // use the same algorithm and secret. + password: String, + // the duration for which the username and password are valid, in seconds. A value of one day + // (86400 seconds) is recommended. + ttl: u32, + // This is used to indicate the different addresses and/or protocols that can be used to reach + // the TURN server. These URIs SHOULD specify a hostname, IPv4, or IPv6 address for the TURN + // server, as well as the port and transport to use; + uris: Vec, +} diff --git a/turn-server/src/config.rs b/turn-server/src/config.rs index 3053536..53df0c9 100644 --- a/turn-server/src/config.rs +++ b/turn-server/src/config.rs @@ -72,7 +72,7 @@ impl Default for Turn { #[derive(Deserialize, Debug)] pub struct Api { - /// Api bind + /// api bind /// /// This option specifies the http server binding address used to control /// the turn server. @@ -92,6 +92,16 @@ pub struct Api { /// through this service, please do not expose it directly to an unsafe /// environment. pub hooks: Option, + /// Credentials used by the http interface, credentials are carried in the + /// http request and are used to authenticate the request. + pub credential: Option, + /// Choose whether the hooks api follows the + /// RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), + /// if the use follows this RFC, then the hooks api will only keep the + /// authentication functionality, other things like event push will be + /// disabled. + #[serde(default = "Api::use_turn_rest_api")] + pub use_turn_rest_api: bool, } impl Api { @@ -99,16 +109,18 @@ impl Api { "127.0.0.1:3000".parse().unwrap() } - fn hooks() -> Option { - None + fn use_turn_rest_api() -> bool { + false } } impl Default for Api { fn default() -> Self { Self { + hooks: None, + credential: None, bind: Self::bind(), - hooks: Self::hooks(), + use_turn_rest_api: Self::use_turn_rest_api(), } } } diff --git a/turn-server/src/lib.rs b/turn-server/src/lib.rs index 3e3a4af..789ae3f 100644 --- a/turn-server/src/lib.rs +++ b/turn-server/src/lib.rs @@ -14,7 +14,7 @@ use self::{config::Config, observer::Observer, statistics::Statistics}; /// In order to let the integration test directly use the turn-server crate and /// start the server, a function is opened to replace the main function to /// directly start the server. -pub async fn server_main(config: Arc) -> anyhow::Result<()> { +pub async fn startup(config: Arc) -> anyhow::Result<()> { let statistics = Statistics::default(); let observer = Observer::new(config.clone(), statistics.clone()).await?; let externals = config.turn.get_externals(); diff --git a/turn-server/src/main.rs b/turn-server/src/main.rs index 13a1404..c75a7cf 100644 --- a/turn-server/src/main.rs +++ b/turn-server/src/main.rs @@ -8,5 +8,5 @@ use turn_server::config::Config; async fn main() -> anyhow::Result<()> { let config = Arc::new(Config::load()?); simple_logger::init_with_level(config.log.level.as_level())?; - turn_server::server_main(config).await + turn_server::startup(config).await } From 007e46219bd55392ca1c4fb9253b581edd6aaa23 Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Thu, 12 Sep 2024 00:59:28 +0800 Subject: [PATCH 2/7] fix: turn rest api; --- turn-server/src/config.rs | 12 -- turn-server/src/credentials.rs | 81 ++++++++++++ turn-server/src/lib.rs | 21 ++- turn-server/src/main.rs | 1 + turn-server/src/observer.rs | 12 +- turn-server/src/{api.rs => publicly.rs} | 162 ++++++++++++++---------- turn-server/src/statistics.rs | 6 +- turn/src/router/nonces.rs | 6 +- 8 files changed, 207 insertions(+), 94 deletions(-) create mode 100644 turn-server/src/credentials.rs rename turn-server/src/{api.rs => publicly.rs} (69%) diff --git a/turn-server/src/config.rs b/turn-server/src/config.rs index 53df0c9..197a1bc 100644 --- a/turn-server/src/config.rs +++ b/turn-server/src/config.rs @@ -95,23 +95,12 @@ pub struct Api { /// Credentials used by the http interface, credentials are carried in the /// http request and are used to authenticate the request. pub credential: Option, - /// Choose whether the hooks api follows the - /// RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), - /// if the use follows this RFC, then the hooks api will only keep the - /// authentication functionality, other things like event push will be - /// disabled. - #[serde(default = "Api::use_turn_rest_api")] - pub use_turn_rest_api: bool, } impl Api { fn bind() -> SocketAddr { "127.0.0.1:3000".parse().unwrap() } - - fn use_turn_rest_api() -> bool { - false - } } impl Default for Api { @@ -120,7 +109,6 @@ impl Default for Api { hooks: None, credential: None, bind: Self::bind(), - use_turn_rest_api: Self::use_turn_rest_api(), } } } diff --git a/turn-server/src/credentials.rs b/turn-server/src/credentials.rs new file mode 100644 index 0000000..aa5862b --- /dev/null +++ b/turn-server/src/credentials.rs @@ -0,0 +1,81 @@ +use std::{ + sync::{Arc, RwLock}, + thread::{self, sleep}, + time::{Duration, Instant}, +}; + +use ahash::HashMap; + +pub struct StaticPassword { + pub value: String, + lifetime: Option, +} + +#[derive(Clone)] +pub struct StaticCredentials(Arc>>); + +impl From> for StaticCredentials { + fn from(value: std::collections::HashMap) -> Self { + let this = Self::new(); + + for (k, v) in value { + this.set(k, v, true); + } + + this + } +} + +impl AsRef>>> for StaticCredentials { + fn as_ref(&self) -> &Arc>> { + &self.0 + } +} + +impl StaticCredentials { + pub fn new() -> Self { + let map: Arc>> = Default::default(); + + let map_ = Arc::downgrade(&map); + thread::spawn(move || { + let mut keys_ = Vec::new(); + + while let Some(map) = map_.upgrade() { + keys_.clear(); + + { + for (key, value) in map.read().unwrap().iter() { + if let Some(lifetime) = value.lifetime { + if lifetime.elapsed().as_secs() >= 86400 { + keys_.push(key.clone()); + } + } + } + + let mut map_ = map.write().unwrap(); + for key in &keys_ { + map_.remove(key); + } + } + + sleep(Duration::from_secs(60)); + } + }); + + Self(map) + } + + pub fn set(&self, username: String, password: String, permanent: bool) { + self.0.write().unwrap().insert( + username, + StaticPassword { + value: password, + lifetime: if !permanent { + Some(Instant::now()) + } else { + None + }, + }, + ); + } +} diff --git a/turn-server/src/lib.rs b/turn-server/src/lib.rs index 789ae3f..d30c97b 100644 --- a/turn-server/src/lib.rs +++ b/turn-server/src/lib.rs @@ -1,6 +1,7 @@ -pub mod api; pub mod config; +pub mod credentials; pub mod observer; +pub mod publicly; pub mod router; pub mod server; pub mod statistics; @@ -9,17 +10,25 @@ use std::sync::Arc; use turn::Service; -use self::{config::Config, observer::Observer, statistics::Statistics}; +use self::{ + config::Config, credentials::StaticCredentials, observer::Observer, statistics::Statistics, +}; /// In order to let the integration test directly use the turn-server crate and /// start the server, a function is opened to replace the main function to /// directly start the server. pub async fn startup(config: Arc) -> anyhow::Result<()> { let statistics = Statistics::default(); - let observer = Observer::new(config.clone(), statistics.clone()).await?; - let externals = config.turn.get_externals(); - let service = Service::new(config.turn.realm.clone(), externals, observer); + let credentials = StaticCredentials::from(config.auth.clone()); + + let service = Service::new( + config.turn.realm.clone(), + config.turn.get_externals(), + Observer::new(config.clone(), statistics.clone(), credentials.clone()).await?, + ); + server::run(config.clone(), statistics.clone(), &service).await?; - api::start_server(config, service, statistics).await?; + publicly::start_server(config, service, statistics, credentials.clone()).await?; + Ok(()) } diff --git a/turn-server/src/main.rs b/turn-server/src/main.rs index c75a7cf..861c098 100644 --- a/turn-server/src/main.rs +++ b/turn-server/src/main.rs @@ -2,6 +2,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; use std::sync::Arc; + use turn_server::config::Config; #[tokio::main] diff --git a/turn-server/src/observer.rs b/turn-server/src/observer.rs index 69ea9a0..4485aad 100644 --- a/turn-server/src/observer.rs +++ b/turn-server/src/observer.rs @@ -1,6 +1,8 @@ use std::{net::SocketAddr, sync::Arc}; -use crate::{api::HooksService, config::Config, statistics::Statistics}; +use crate::{ + config::Config, credentials::StaticCredentials, publicly::HooksService, statistics::Statistics, +}; use anyhow::Result; use async_trait::async_trait; @@ -12,9 +14,13 @@ pub struct Observer { } impl Observer { - pub async fn new(cfg: Arc, statistics: Statistics) -> Result { + pub async fn new( + config: Arc, + statistics: Statistics, + credentials: StaticCredentials, + ) -> Result { Ok(Self { - hooks: HooksService::new(cfg)?, + hooks: HooksService::new(config, credentials)?, statistics, }) } diff --git a/turn-server/src/api.rs b/turn-server/src/publicly.rs similarity index 69% rename from turn-server/src/api.rs rename to turn-server/src/publicly.rs index cb4e603..a0161b5 100644 --- a/turn-server/src/api.rs +++ b/turn-server/src/publicly.rs @@ -4,7 +4,11 @@ use std::{ time::{Duration, Instant}, }; -use crate::{config::Config, statistics::Statistics}; +use crate::{ + config::{Config, Transport}, + credentials::StaticCredentials, + statistics::Statistics, +}; use axum::{ extract::{Query, State}, @@ -31,19 +35,13 @@ use tokio::{ use turn::Service; -static RID: Lazy = Lazy::new(|| { - let mut rng = thread_rng(); - std::iter::repeat(()) - .map(|_| rng.sample(Alphanumeric) as char) - .take(16) - .collect::() - .to_lowercase() -}); +static RID: Lazy = Lazy::new(|| random_string(16)); struct AppState { config: Arc, service: Service, statistics: Statistics, + credentials: StaticCredentials, uptime: Instant, } @@ -53,6 +51,13 @@ struct QueryFilter { username: Option, } +#[derive(Deserialize)] +struct TurnRestRequest { + service: String, + username: Option, + key: Option, +} + /// start http server /// /// Create an http server and start it, and you can access the controller @@ -66,15 +71,64 @@ pub async fn start_server( config: Arc, service: Service, statistics: Statistics, + credentials: StaticCredentials, ) -> anyhow::Result<()> { let state = Arc::new(AppState { config: config.clone(), uptime: Instant::now(), service, statistics, + credentials, }); let app = Router::new() + .route( + "/", + get( + |Query(TurnRestRequest { + service, + username, + key, + }): Query, + State(state): State>| async move { + if service != "turn" || state.config.api.credential != key { + return StatusCode::NOT_FOUND.into_response(); + } + + let username = username.unwrap_or_else(|| random_string(20)); + let password = random_string(50); + + // Cache the user credentials automatically generated by the turn server. + state.credentials.set(username.clone(), password.clone(), false); + + let uris = state + .config + .turn + .interfaces + .iter() + .map(|it| { + format!( + "turn:{}:{}?transport={}", + it.external, + it.bind.port(), + match it.transport { + Transport::TCP => "tcp", + Transport::UDP => "udp", + } + ) + }) + .collect::>(); + + Json(json!({ + "password": stun::util::long_key(&username, &password, &state.config.turn.realm), + "username": format!(":{}", username), + "ttl": 86400, + "uris": uris, + })) + .into_response() + }, + ), + ) .route( "/info", get(|State(state): State>| async move { @@ -193,16 +247,17 @@ pub async fn start_server( pub struct HooksService { client: Arc, tx: UnboundedSender, - cfg: Arc, + credentials: StaticCredentials, + config: Arc, } impl HooksService { - pub fn new(cfg: Arc) -> anyhow::Result { + pub fn new(config: Arc, credentials: StaticCredentials) -> anyhow::Result { let mut headers = HeaderMap::new(); - headers.insert("Realm", HeaderValue::from_str(&cfg.turn.realm)?); + headers.insert("Realm", HeaderValue::from_str(&config.turn.realm)?); headers.insert("Rid", HeaderValue::from_str(&RID)?); - if let Some(credential) = &cfg.api.credential { + if let Some(credential) = &config.api.credential { headers.insert("Credential", HeaderValue::from_str(credential)?); } @@ -213,11 +268,11 @@ impl HooksService { .build()?, ); - let cfg_ = cfg.clone(); + let config_ = config.clone(); let client_ = client.clone(); let (tx, mut rx) = unbounded_channel::(); tokio::spawn(async move { - if let Some(server) = &cfg_.api.hooks { + if let Some(server) = &config_.api.hooks { let uri = format!("{}/events", server); while let Some(signal) = rx.recv().await { @@ -228,37 +283,28 @@ impl HooksService { } }); - Ok(Self { client, cfg, tx }) + Ok(Self { + client, + config, + credentials, + tx, + }) } pub async fn get_password(&self, addr: &SocketAddr, name: &str) -> Option { - if let Some(pwd) = self.cfg.auth.get(name) { - return Some(pwd.clone()); + if let Some(pwd) = self.credentials.as_ref().read().unwrap().get(name) { + return Some(pwd.value.clone()); } - if let Some(server) = &self.cfg.api.hooks { - let url = if self.cfg.api.use_turn_rest_api { - if let Some(credential) = &self.cfg.api.credential { - format!( - "{}/?service=turn&username={}&key={}", - server, name, credential - ) - } else { - format!("{}/?service=turn&username={}", server, name) - } - } else { - format!("{}/password?addr={}&name={}", server, addr, name) - }; - - if let Ok(res) = self.client.get(url).send().await { - if self.cfg.api.use_turn_rest_api { - if let Ok(response) = res.json::().await { - return Some(response.password); - } - } else { - if let Ok(password) = res.text().await { - return Some(password); - } + if let Some(server) = &self.config.api.hooks { + if let Ok(res) = self + .client + .get(format!("{}/password?addr={}&name={}", server, addr, name)) + .send() + .await + { + if let Ok(password) = res.text().await { + return Some(password); } } } @@ -267,11 +313,7 @@ impl HooksService { } pub fn send_event(&self, event: Value) { - if self.cfg.api.use_turn_rest_api { - return; - } - - if self.cfg.api.hooks.is_some() { + if self.config.api.hooks.is_some() { if let Err(e) = self.tx.send(event) { log::error!("failed to send event, err={}", e) } @@ -279,25 +321,11 @@ impl HooksService { } } -#[allow(unused)] -#[derive(Deserialize, Debug)] -struct TurnRestApiResponse { - // the TURN username to use, which is a colon-delimited combination of the expiration timestamp - // and the username parameter from the request (if specified). The timestamp is intended to be - // opaque to the web application, so its format is arbitrary, but for simplicity, use of UNIX - // timestamps is recommended. - username: String, - // the TURN password to use; this value is computed from the a secret key shared with the TURN - // server and the returned username value, by performing base64(hmac(secret key, returned - // username)). HMAC-SHA1 is one HMAC algorithm that can be used, but any algorithm that - // incorporates a shared secret is acceptable, as long as both the web server and TURN server - // use the same algorithm and secret. - password: String, - // the duration for which the username and password are valid, in seconds. A value of one day - // (86400 seconds) is recommended. - ttl: u32, - // This is used to indicate the different addresses and/or protocols that can be used to reach - // the TURN server. These URIs SHOULD specify a hostname, IPv4, or IPv6 address for the TURN - // server, as well as the port and transport to use; - uris: Vec, +fn random_string(len: usize) -> String { + let mut rng = thread_rng(); + std::iter::repeat(()) + .map(|_| rng.sample(Alphanumeric) as char) + .take(len) + .collect::() + .to_lowercase() } diff --git a/turn-server/src/statistics.rs b/turn-server/src/statistics.rs index 2f36022..7d0e8a3 100644 --- a/turn-server/src/statistics.rs +++ b/turn-server/src/statistics.rs @@ -4,11 +4,11 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, RwLock, }, + thread::{self, sleep}, time::Duration, }; use ahash::AHashMap; -use tokio::time::sleep; #[derive(Debug, Clone, Copy)] pub struct NodeCounts { @@ -79,10 +79,10 @@ impl Default for Statistics { fn default() -> Self { let map: Arc>> = Default::default(); let map_ = Arc::downgrade(&map); - tokio::spawn(async move { + thread::spawn(move || { while let Some(map) = map_.upgrade() { let _ = map.read().unwrap().iter().for_each(|(_, it)| it.clear()); - sleep(Duration::from_secs(1)).await; + sleep(Duration::from_secs(1)); } }); diff --git a/turn/src/router/nonces.rs b/turn/src/router/nonces.rs index 36e7f1b..c0cb0f6 100644 --- a/turn/src/router/nonces.rs +++ b/turn/src/router/nonces.rs @@ -22,7 +22,7 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; /// for guidance on selection of nonce values in a server. pub struct Nonce { raw: Arc, - timer: Instant, + lifetime: Instant, } impl Default for Nonce { @@ -35,7 +35,7 @@ impl Nonce { pub fn new() -> Self { Self { raw: Arc::new(Self::create_nonce()), - timer: Instant::now(), + lifetime: Instant::now(), } } @@ -50,7 +50,7 @@ impl Nonce { /// assert!(!nonce.is_death()); /// ``` pub fn is_death(&self) -> bool { - self.timer.elapsed().as_secs() >= 3600 + self.lifetime.elapsed().as_secs() >= 3600 } /// unwind nonce random string. From d39ba6257a8453ba719cbac83c871048e86e0e5d Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Thu, 12 Sep 2024 10:45:33 +0800 Subject: [PATCH 3/7] update docs; --- docs/configure.md | 15 --------------- turn-server.toml | 8 -------- 2 files changed, 23 deletions(-) diff --git a/docs/configure.md b/docs/configure.md index 4ead824..d41c9f3 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -61,14 +61,6 @@ bind = "127.0.0.1:3000" # # credential = "" -# Choose whether the hooks api follows the -# RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), -# if the use follows this RFC, then the hooks api will only keep the -# authentication functionality, other things like event push will be -# disabled. -# -# use_turn_rest_api = false - [log] # log level # @@ -169,13 +161,6 @@ The turn service provides an external REST API. External parties can control the Credentials used by the http interface, credentials are carried in the http request and are used to authenticate the request. -### `api.use_turn_rest_api` - -* Type: boolean -* Default: false - -Choose whether the hooks api follows the RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), if the use follows this RFC, then the hooks api will only keep the authentication functionality, other things like event push will be disabled. - *** ### `log.level` diff --git a/turn-server.toml b/turn-server.toml index 7f874d2..b289643 100644 --- a/turn-server.toml +++ b/turn-server.toml @@ -56,14 +56,6 @@ bind = "127.0.0.1:3000" # # credential = "" -# Choose whether the hooks api follows the -# RFC [turn rest api](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00), -# if the use follows this RFC, then the hooks api will only keep the -# authentication functionality, other things like event push will be -# disabled. -# -# use_turn_rest_api = false - [log] # log level # From e677f62724c39eb897a3559ce12fa33f6499a62e Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Fri, 13 Sep 2024 13:58:31 +0800 Subject: [PATCH 4/7] update --- Cargo.lock | 2 + README.md | 2 +- docs/README.md | 2 +- docs/configure.md | 37 +- docs/{web-hooks.md => http-hooks.md} | 1 - docs/rest-api.md | 1 - stun/src/message.rs | 6 +- stun/src/util.rs | 4 +- tests/Cargo.toml | 1 + tests/benches/benchmark.rs | 40 +-- tests/src/lib.rs | 505 ++++++++++++++++----------- turn-server.toml | 16 +- turn-server/Cargo.toml | 1 + turn-server/src/config.rs | 33 +- turn-server/src/credentials.rs | 81 ----- turn-server/src/lib.rs | 11 +- turn-server/src/observer.rs | 12 +- turn-server/src/publicly.rs | 103 ++---- turn/src/router/nodes.rs | 4 +- 19 files changed, 403 insertions(+), 459 deletions(-) rename docs/{web-hooks.md => http-hooks.md} (96%) delete mode 100644 turn-server/src/credentials.rs diff --git a/Cargo.lock b/Cargo.lock index 1e855e8..e5b9fc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1704,6 +1704,7 @@ dependencies = [ name = "tests" version = "0.1.0" dependencies = [ + "base64", "bytes", "criterion", "once_cell", @@ -1967,6 +1968,7 @@ dependencies = [ "anyhow", "async-trait", "axum", + "base64", "bytes", "clap", "log", diff --git a/README.md b/README.md index 8147247..407be9f 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If you have extensive standard support requirements for turn servers and need mo * [start the server](./docs/start-the-server.md) * [configure](./docs/configure.md) * [rest api](./docs/rest-api.md) - * [web hooks](./docs/web-hooks.md) + * [http hooks](./docs/http-hooks.md) * [driver](./drivers) ## Features diff --git a/docs/README.md b/docs/README.md index 25334d1..f4719e8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,4 +5,4 @@ * [Start the server](start-the-server.md) * [Configure](configure.md) * [REST API](rest-api.md) -* [Web Hooks](web-hooks.md) +* [HTTP Hooks](http-hooks.md) diff --git a/docs/configure.md b/docs/configure.md index d41c9f3..853e705 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -56,17 +56,21 @@ bind = "127.0.0.1:3000" # # hooks = "http://127.0.0.1:8080" -# Credentials used by the http interface, credentials are carried in the -# http request and are used to authenticate the request. -# -# credential = "" - [log] # log level # # An enum representing the available verbosity levels of the logger. level = "info" +[auth] +# Static authentication key value (string) that applies only to the TURN +# REST API. +# +# If set, the turn server will not request external services via the HTTP +# Hooks API to obtain the key. +# +# static_auth_secret = "" + # static user password # # This option can be used to specify the @@ -74,10 +78,9 @@ level = "info" # verification. Note: this is a high-priority authentication method, turn # The server will try to use static authentication first, and then use # external control service authentication. -[auth] +[auth.static_credentials] # user1 = "test" # user2 = "test" - ``` ## Configuration keys @@ -154,13 +157,6 @@ The turn service provides an external REST API. External parties can control the > Warning: The REST API does not provide any authentication or encryption measures. You need to run the turn service in a trusted network environment or add a proxy to increase authentication and encryption measures. -### `api.credential` - -* Type: string -* Default: None - -Credentials used by the http interface, credentials are carried in the http request and are used to authenticate the request. - *** ### `log.level` @@ -172,8 +168,19 @@ Describes the log level of the turn service. Possible values ​​are `"error"` *** -### `auth` +### `auth.static_credentials` * Type: key values Describes static authentication information, with username and password as key pair. Static identity authentication is authentication information provided to the turn service in advance. The turn service will first look for this table when it needs to authenticate the turn session. If it cannot find it, it will use Web Hooks for external authentication. + +*** + +### `auth.static_auth_secret` + +* Type: string +* Default: None + +Static authentication key value (string) that applies only to the TURN REST API. + +If set, the turn server will not request external services via the HTTP Hooks API to obtain the key. diff --git a/docs/web-hooks.md b/docs/http-hooks.md similarity index 96% rename from docs/web-hooks.md rename to docs/http-hooks.md index bf07f10..7075dc7 100644 --- a/docs/web-hooks.md +++ b/docs/http-hooks.md @@ -3,7 +3,6 @@ #### Global Request Headers * `realm` - string - turn server realm -* `credential` - string - credentials used by the http interface * `rid` - string - The runtime ID of the turn server rid: A new ID is generated each time the server is started. This is a random string. Its main function is to determine whether the turn server has been restarted. diff --git a/docs/rest-api.md b/docs/rest-api.md index 500fa0c..b93d481 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -3,7 +3,6 @@ #### Global Response Headers * `realm` - string - turn server realm -* `credential` - string - credentials used by the http interface * `rid` - string - The runtime ID of the turn server rid: A new ID is generated each time the server is started. This is a random string. Its main function is to determine whether the turn server has been restarted. diff --git a/stun/src/message.rs b/stun/src/message.rs index 704668d..7dcda98 100644 --- a/stun/src/message.rs +++ b/stun/src/message.rs @@ -213,7 +213,7 @@ impl<'a, 'b> MessageWriter<'a> { // long key, // digest the message buffer, // create the new MessageIntegrity attribute. - let hmac_output = util::hmac_sha1(auth, vec![self.raw])?.into_bytes(); + let hmac_output = util::hmac_sha1(auth, &[self.raw])?.into_bytes(); let property_buf = hmac_output.as_slice(); // write MessageIntegrity attribute. @@ -325,14 +325,14 @@ impl<'a, 'b> MessageReader<'a, 'b> { // create multiple submit. let size_buf = (self.valid_offset + 4).to_be_bytes(); - let body = vec![ + let body = [ &self.buf[0..2], &size_buf, &self.buf[4..self.valid_offset as usize], ]; // digest the message buffer. - let hmac_output = util::hmac_sha1(auth, body)?.into_bytes(); + let hmac_output = util::hmac_sha1(auth, &body)?.into_bytes(); let property_buf = hmac_output.as_slice(); // Compare local and original attribute. diff --git a/stun/src/util.rs b/stun/src/util.rs index 73cc852..5f6b157 100644 --- a/stun/src/util.rs +++ b/stun/src/util.rs @@ -70,12 +70,12 @@ pub fn long_key(username: &str, key: &str, realm: &str) -> [u8; 16] { /// 0x74, 0xe2, 0x3c, 0x26, 0xc5, 0xb1, 0x03, 0xb2, 0x6d, /// ]; /// -/// let hmac_output = stun::util::hmac_sha1(&key, vec![&buffer]) +/// let hmac_output = stun::util::hmac_sha1(&key, &[&buffer]) /// .unwrap() /// .into_bytes(); /// assert_eq!(hmac_output.as_slice(), &sign); /// ``` -pub fn hmac_sha1(key: &[u8], source: Vec<&[u8]>) -> Result>, StunError> { +pub fn hmac_sha1(key: &[u8], source: &[&[u8]]) -> Result>, StunError> { match Hmac::::new_from_slice(key) { Err(_) => Err(StunError::ShaFailed), Ok(mut mac) => { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 6461b02..844e597 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" publish = false [dependencies] +base64 = "0.22.1" once_cell = "1.18.0" tokio = { version = "1", features = ["full"] } stun = { path = "../stun", version = "1" } diff --git a/tests/benches/benchmark.rs b/tests/benches/benchmark.rs index 41c0355..181e69e 100644 --- a/tests/benches/benchmark.rs +++ b/tests/benches/benchmark.rs @@ -1,43 +1,25 @@ use criterion::*; -use tests::{allocate_request, create_client, create_permission_request, create_turn, indication}; -use tokio::{net::UdpSocket, runtime::Runtime}; +use tests::{create_turn_server, AuthMethod, TurnClient}; -fn create_turn_block(rt: &Runtime) { - rt.block_on(async { create_turn().await }) -} - -fn create_client_block(rt: &Runtime) -> UdpSocket { - rt.block_on(async { create_client().await }) -} - -fn allocate_request_block(rt: &Runtime, socket: &UdpSocket) -> u16 { - rt.block_on(async { allocate_request(&socket).await }) -} +fn criterion_benchmark(c: &mut Criterion) { + create_turn_server(&AuthMethod::Static); -fn create_permission_request_block(rt: &Runtime, socket: &UdpSocket, port: u16) { - rt.block_on(async { create_permission_request(&socket, port).await }) -} + let mut local = TurnClient::new(&AuthMethod::Static); + let mut peer = TurnClient::new(&AuthMethod::Static); -fn criterion_benchmark(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); + let local_port = local.allocate_request(); + let peer_port = peer.allocate_request(); - create_turn_block(&rt); - let local = create_client_block(&rt); - let peer = create_client_block(&rt); - let local_port = allocate_request_block(&rt, &local); - let peer_port = allocate_request_block(&rt, &peer); - create_permission_request_block(&rt, &local, peer_port); - create_permission_request_block(&rt, &peer, local_port); + local.create_permission_request(peer_port); + peer.create_permission_request(local_port); let mut turn_relay = c.benchmark_group("turn_relay"); turn_relay.bench_function("send_indication_local_to_peer", |b| { - b.to_async(&rt) - .iter(|| indication(&local, &peer, peer_port)) + b.iter(|| local.indication(&peer, peer_port)) }); turn_relay.bench_function("send_indication_peer_to_local", |b| { - b.to_async(&rt) - .iter(|| indication(&peer, &local, local_port)) + b.iter(|| peer.indication(&local, local_port)) }); turn_relay.finish(); diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 280df0c..34e62aa 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,15 +1,14 @@ -#![allow(static_mut_refs)] - +use base64::prelude::*; use bytes::BytesMut; +use once_cell::sync::Lazy; use stun::attribute::{ ChannelNumber, Data, ErrKind, ErrorCode, Lifetime, MappedAddress, Realm, ReqeestedTransport, ResponseOrigin, Transport, UserName, XorMappedAddress, XorPeerAddress, XorRelayedAddress, }; -use once_cell::sync::Lazy; use rand::seq::SliceRandom; use stun::{Decoder, Kind, MessageReader, MessageWriter, Method, Payload}; -use tokio::net::UdpSocket; +use tokio::{net::UdpSocket, runtime::Runtime}; use turn_server::{ config::{self, *}, startup, @@ -19,47 +18,44 @@ use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; -// global static var - -pub const BIND_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); -pub const BIND_ADDR: SocketAddr = SocketAddr::new(BIND_IP, 3478); -pub const USERNAME: &str = "user1"; -pub const PASSWORD: &str = "test"; -pub const REALM: &str = "localhost"; - -static mut RECV_BUF: [u8; 1500] = [0u8; 1500]; -static mut SEND_BUF: Lazy = Lazy::new(|| BytesMut::with_capacity(2048)); -static TOKEN_BUF: Lazy<[u8; 12]> = Lazy::new(|| { - let mut rng = rand::thread_rng(); - let mut token = [0u8; 12]; - token.shuffle(&mut rng); - token -}); - -static KEY_BUF: Lazy<[u8; 16]> = Lazy::new(|| stun::util::long_key(USERNAME, PASSWORD, REALM)); -static mut DECODER: Lazy = Lazy::new(Decoder::new); - -// global static var end - -fn get_message_from_payload<'a, 'b>(payload: Payload<'a, 'b>) -> MessageReader<'a, 'b> { - if let Payload::Message(m) = payload { - m - } else { - panic!("get message from payload failed!") - } -} +static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); -pub async fn create_turn() { - let mut auth = HashMap::new(); - auth.insert(USERNAME.to_string(), PASSWORD.to_string()); +static BIND_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); +static BIND_ADDR: SocketAddr = SocketAddr::new(BIND_IP, 3478); +static USERNAME: &str = "user1"; +static PASSWORD: &str = "test"; +static REALM: &str = "localhost"; - // Because it is testing, it is not reasonable to start a separate process - // to start turn-server, and the configuration file is not convenient to - // pass, so turn-server is used as a library here, and the server is - // started with a custom configuration. - tokio::spawn(async move { +#[derive(Debug, Clone)] +pub enum AuthMethod { + Static, + Secret(String), + Hooks(String), +} + +pub fn create_turn_server(auth_method: &AuthMethod) { + let mut static_credentials = HashMap::new(); + let mut static_auth_secret = None; + let mut api = Api::default(); + + match auth_method { + AuthMethod::Static => { + static_credentials.insert(USERNAME.to_string(), PASSWORD.to_string()); + } + AuthMethod::Secret(secret) => { + static_auth_secret = Some(secret.clone()); + } + AuthMethod::Hooks(uri) => { + api.hooks = Some(uri.clone()); + } + }; + + RUNTIME.spawn(async move { startup(Arc::new(Config { - auth, + auth: Auth { + static_credentials, + static_auth_secret, + }, api: Api::default(), log: Log::default(), turn: Turn { @@ -76,207 +72,302 @@ pub async fn create_turn() { }); } -// Create a udp connection and connect to the turn-server, and then start -// the corresponding session process checks in sequence. It should be noted -// that the order of request responses is relatively strict, and should not -// be changed under normal circumstances. -pub async fn create_client() -> UdpSocket { - let socket = UdpSocket::bind(SocketAddr::new(BIND_IP, 0)).await.unwrap(); - socket.connect(BIND_ADDR).await.unwrap(); - socket +pub struct TurnClient { + key_buf: [u8; 16], + decoder: Decoder, + client: UdpSocket, + token_buf: [u8; 12], + recv_buf: [u8; 1500], + send_buf: BytesMut, + bind_request_buf: BytesMut, + base_allocate_request_buf: BytesMut, + allocate_request_buf: BytesMut, } -static BIND_REQUEST_BUF: Lazy = Lazy::new(|| { - let mut buf = BytesMut::with_capacity(1500); - let mut msg = MessageWriter::new(Method::Binding(Kind::Request), &TOKEN_BUF, &mut buf); - - msg.flush(None).unwrap(); - buf -}); - -pub async fn binding_request(socket: &UdpSocket) { - socket.send(&BIND_REQUEST_BUF).await.unwrap(); - let size = socket.recv(unsafe { &mut RECV_BUF }).await.unwrap(); +impl TurnClient { + pub fn new(auth_method: &AuthMethod) -> Self { + let client = RUNTIME + .block_on(UdpSocket::bind(SocketAddr::new(BIND_IP, 0))) + .unwrap(); + + RUNTIME.block_on(client.connect(BIND_ADDR)).unwrap(); + + let key = match &auth_method { + AuthMethod::Static => PASSWORD.to_string(), + AuthMethod::Secret(secret) => Self::encode_password(secret, USERNAME).unwrap(), + AuthMethod::Hooks(_) => PASSWORD.to_string(), + }; + + let key_buf = stun::util::long_key(USERNAME, &key, REALM); + + let token_buf = { + let mut rng = rand::thread_rng(); + let mut token = [0u8; 12]; + token.shuffle(&mut rng); + token + }; + + let bind_request_buf = { + let mut buf = BytesMut::with_capacity(1500); + let mut msg = MessageWriter::new(Method::Binding(Kind::Request), &token_buf, &mut buf); + + msg.flush(None).unwrap(); + buf + }; + + let base_allocate_request_buf = { + let mut buf = BytesMut::with_capacity(1500); + let mut msg = MessageWriter::new(Method::Allocate(Kind::Request), &token_buf, &mut buf); + + msg.append::(Transport::UDP); + msg.flush(None).unwrap(); + buf + }; + + let allocate_request_buf = { + let mut buf = BytesMut::with_capacity(1500); + let mut msg = MessageWriter::new(Method::Allocate(Kind::Request), &token_buf, &mut buf); + + msg.append::(Transport::UDP); + msg.append::(USERNAME); + msg.append::(REALM); + msg.flush(Some(&key_buf)).unwrap(); + buf + }; + + Self { + key_buf, + bind_request_buf, + base_allocate_request_buf, + allocate_request_buf, + send_buf: BytesMut::with_capacity(2048), + recv_buf: [0u8; 1500], + decoder: Decoder::new(), + token_buf, + client, + } + } - let decoder = unsafe { &mut DECODER }; - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); + pub fn binding_request(&mut self) { + RUNTIME + .block_on(self.client.send(&self.bind_request_buf)) + .unwrap(); + + let size = RUNTIME + .block_on(self.client.recv(&mut self.recv_buf)) + .unwrap(); - assert_eq!(ret.method, Method::Binding(Kind::Response)); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); - let value = ret.get::().unwrap(); - assert_eq!(value, socket.local_addr().unwrap()); + assert_eq!(ret.method, Method::Binding(Kind::Response)); + assert_eq!(ret.token, self.token_buf.as_slice()); - let value = ret.get::().unwrap(); - assert_eq!(value, socket.local_addr().unwrap()); + let value = ret.get::().unwrap(); + assert_eq!(value, self.client.local_addr().unwrap()); - let value = ret.get::().unwrap(); - assert_eq!(value, BIND_ADDR); -} + let value = ret.get::().unwrap(); + assert_eq!(value, self.client.local_addr().unwrap()); -static BASE_ALLOCATE_REQUEST_BUF: Lazy = Lazy::new(|| { - let mut buf = BytesMut::with_capacity(1500); - let mut msg = MessageWriter::new(Method::Allocate(Kind::Request), &TOKEN_BUF, &mut buf); - - msg.append::(Transport::UDP); - msg.flush(None).unwrap(); - buf -}); + let value = ret.get::().unwrap(); + assert_eq!(value, BIND_ADDR); + } -pub async fn base_allocate_request(socket: &UdpSocket) { - socket.send(&BASE_ALLOCATE_REQUEST_BUF).await.unwrap(); + pub fn base_allocate_request(&mut self) { + RUNTIME + .block_on(self.client.send(&self.base_allocate_request_buf)) + .unwrap(); - let decoder = unsafe { &mut DECODER }; - let size = socket.recv(unsafe { &mut RECV_BUF }).await.unwrap(); - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); + let size = RUNTIME + .block_on(self.client.recv(&mut self.recv_buf)) + .unwrap(); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); - assert_eq!(ret.method, Method::Allocate(Kind::Error)); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); + assert_eq!(ret.method, Method::Allocate(Kind::Error)); + assert_eq!(ret.token, self.token_buf.as_slice()); - let value = ret.get::().unwrap(); - assert_eq!(value.code, ErrKind::Unauthorized as u16); + let value = ret.get::().unwrap(); + assert_eq!(value.code, ErrKind::Unauthorized as u16); - let value = ret.get::().unwrap(); - assert_eq!(value, REALM); -} + let value = ret.get::().unwrap(); + assert_eq!(value, REALM); + } -static ALLOCATE_REQUEST_BUF: Lazy = Lazy::new(|| { - let mut buf = BytesMut::with_capacity(1500); - let mut msg = MessageWriter::new(Method::Allocate(Kind::Request), &TOKEN_BUF, &mut buf); + pub fn allocate_request(&mut self) -> u16 { + RUNTIME + .block_on(self.client.send(&self.allocate_request_buf)) + .unwrap(); - msg.append::(Transport::UDP); - msg.append::(USERNAME); - msg.append::(REALM); - msg.flush(Some(&KEY_BUF)).unwrap(); - buf -}); + let size = RUNTIME + .block_on(self.client.recv(&mut self.recv_buf)) + .unwrap(); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); -pub async fn allocate_request(socket: &UdpSocket) -> u16 { - socket.send(&ALLOCATE_REQUEST_BUF).await.unwrap(); + assert_eq!(ret.method, Method::Allocate(Kind::Response)); + assert_eq!(ret.token, self.token_buf.as_slice()); + ret.integrity(&self.key_buf).unwrap(); - let decoder = unsafe { &mut DECODER }; - let size = socket.recv(unsafe { &mut RECV_BUF }).await.unwrap(); - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); + let relay = ret.get::().unwrap(); + assert_eq!(relay.ip(), BIND_IP); - assert_eq!(ret.method, Method::Allocate(Kind::Response)); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); - ret.integrity(&KEY_BUF).unwrap(); + let value = ret.get::().unwrap(); + assert_eq!(value, self.client.local_addr().unwrap()); - let relay = ret.get::().unwrap(); - assert_eq!(relay.ip(), BIND_IP); + let value = ret.get::().unwrap(); + assert_eq!(value, 600); - let value = ret.get::().unwrap(); - assert_eq!(value, socket.local_addr().unwrap()); + relay.port() + } - let value = ret.get::().unwrap(); - assert_eq!(value, 600); + pub fn create_permission_request(&mut self, port: u16) { + let mut msg = MessageWriter::new( + Method::CreatePermission(Kind::Request), + &self.token_buf, + &mut self.send_buf, + ); + + msg.append::(SocketAddr::new(BIND_IP, port)); + msg.append::(USERNAME); + msg.append::(REALM); + msg.flush(Some(&self.key_buf)).unwrap(); + RUNTIME.block_on(self.client.send(&self.send_buf)).unwrap(); + + let size = RUNTIME + .block_on(self.client.recv(&mut self.recv_buf)) + .unwrap(); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); + + assert_eq!(ret.method, Method::CreatePermission(Kind::Response)); + assert_eq!(ret.token, self.token_buf.as_slice()); + ret.integrity(&self.key_buf).unwrap(); + } - relay.port() -} + pub fn channel_bind_request(&mut self, port: u16) { + let mut msg = MessageWriter::new( + Method::ChannelBind(Kind::Request), + &self.token_buf, + &mut self.send_buf, + ); + + msg.append::(0x4000); + msg.append::(SocketAddr::new(BIND_IP, port)); + msg.append::(USERNAME); + msg.append::(REALM); + msg.flush(Some(&self.key_buf)).unwrap(); + RUNTIME.block_on(self.client.send(&self.send_buf)).unwrap(); + + let size = RUNTIME + .block_on(self.client.recv(&mut self.recv_buf)) + .unwrap(); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); + + assert_eq!(ret.method, Method::ChannelBind(Kind::Response)); + assert_eq!(ret.token, self.token_buf.as_slice()); + ret.integrity(&self.key_buf).unwrap(); + } -pub async fn create_permission_request(socket: &UdpSocket, port: u16) { - let mut msg = MessageWriter::new( - Method::CreatePermission(Kind::Request), - &TOKEN_BUF, - unsafe { &mut SEND_BUF }, - ); - - msg.append::(SocketAddr::new(BIND_IP, port)); - msg.append::(USERNAME); - msg.append::(REALM); - msg.flush(Some(&KEY_BUF)).unwrap(); - socket.send(unsafe { &SEND_BUF }).await.unwrap(); - - let decoder = unsafe { &mut DECODER }; - let size = socket.recv(unsafe { &mut RECV_BUF }).await.unwrap(); - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); - - assert_eq!(ret.method, Method::CreatePermission(Kind::Response)); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); - ret.integrity(&KEY_BUF).unwrap(); -} + pub fn refresh_request(&mut self) { + let mut msg = MessageWriter::new( + Method::Refresh(Kind::Request), + &self.token_buf, + &mut self.send_buf, + ); + + msg.append::(0); + msg.append::(USERNAME); + msg.append::(REALM); + msg.flush(Some(&self.key_buf)).unwrap(); + RUNTIME.block_on(self.client.send(&self.send_buf)).unwrap(); + + let size = RUNTIME + .block_on(self.client.recv(&mut self.recv_buf)) + .unwrap(); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); + + assert_eq!(ret.method, Method::Refresh(Kind::Response)); + assert_eq!(ret.token, self.token_buf.as_slice()); + ret.integrity(&self.key_buf).unwrap(); + + let value = ret.get::().unwrap(); + assert_eq!(value, 0); + } -pub async fn channel_bind_request(socket: &UdpSocket, port: u16) { - let mut msg = MessageWriter::new(Method::ChannelBind(Kind::Request), &TOKEN_BUF, unsafe { - &mut SEND_BUF - }); + pub fn indication(&mut self, peer: &Self, port: u16) { + let mut msg = + MessageWriter::new(Method::SendIndication, &self.token_buf, &mut self.send_buf); - msg.append::(0x4000); - msg.append::(SocketAddr::new(BIND_IP, port)); - msg.append::(USERNAME); - msg.append::(REALM); - msg.flush(Some(&KEY_BUF)).unwrap(); - socket.send(unsafe { &SEND_BUF }).await.unwrap(); - - let decoder = unsafe { &mut DECODER }; - let size = socket.recv(unsafe { &mut RECV_BUF }).await.unwrap(); - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); - - assert_eq!(ret.method, Method::ChannelBind(Kind::Response)); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); - ret.integrity(&KEY_BUF).unwrap(); -} + msg.append::(SocketAddr::new(BIND_IP, port)); + msg.append::(self.token_buf.as_slice()); + msg.flush(None).unwrap(); + RUNTIME.block_on(self.client.send(&self.send_buf)).unwrap(); -pub async fn refresh_request(socket: &UdpSocket) { - let mut msg = MessageWriter::new(Method::Refresh(Kind::Request), &TOKEN_BUF, unsafe { - &mut SEND_BUF - }); + let size = RUNTIME + .block_on(peer.client.recv(&mut self.recv_buf)) + .unwrap(); + let ret = self.decoder.decode(&mut self.recv_buf[..size]).unwrap(); + let ret = Self::get_message_from_payload(ret); - msg.append::(0); - msg.append::(USERNAME); - msg.append::(REALM); - msg.flush(Some(&KEY_BUF)).unwrap(); - socket.send(unsafe { &SEND_BUF }).await.unwrap(); + assert_eq!(ret.method, Method::DataIndication); + assert_eq!(ret.token, self.token_buf.as_slice()); - let decoder = unsafe { &mut DECODER }; - let size = socket.recv(unsafe { &mut RECV_BUF }).await.unwrap(); - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); + let value = ret.get::().unwrap(); + assert_eq!(value, self.token_buf.as_slice()); + } - assert_eq!(ret.method, Method::Refresh(Kind::Response)); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); - ret.integrity(&KEY_BUF).unwrap(); + fn get_message_from_payload<'a, 'b>(payload: Payload<'a, 'b>) -> MessageReader<'a, 'b> { + if let Payload::Message(m) = payload { + m + } else { + panic!("get message from payload failed!") + } + } - let value = ret.get::().unwrap(); - assert_eq!(value, 0); + fn encode_password(key: &str, username: &str) -> Option { + Some( + BASE64_STANDARD.encode( + stun::util::hmac_sha1(key.as_bytes(), &[username.as_bytes()]) + .ok()? + .into_bytes() + .as_slice(), + ), + ) + } } -pub async fn indication(local: &UdpSocket, peer: &UdpSocket, port: u16) { - let mut msg = MessageWriter::new(Method::SendIndication, &TOKEN_BUF, unsafe { &mut SEND_BUF }); +#[cfg(test)] +mod tests { + use crate::{create_turn_server, AuthMethod, TurnClient, PASSWORD}; + + #[test] + fn integration_testing() { + create_turn_server(&AuthMethod::Static); - msg.append::(SocketAddr::new(BIND_IP, port)); - msg.append::(TOKEN_BUF.as_slice()); - msg.flush(None).unwrap(); - local.send(unsafe { &SEND_BUF }).await.unwrap(); + let mut local = TurnClient::new(&AuthMethod::Static); + local.binding_request(); + local.base_allocate_request(); - let decoder = unsafe { &mut DECODER }; - let size = peer.recv(unsafe { &mut RECV_BUF }).await.unwrap(); - let ret = decoder.decode(unsafe { &RECV_BUF[..size] }).unwrap(); - let ret = get_message_from_payload(ret); + let port = local.allocate_request(); + local.create_permission_request(port); + local.channel_bind_request(port); + local.refresh_request(); + } - assert_eq!(ret.method, Method::DataIndication); - assert_eq!(ret.token, TOKEN_BUF.as_slice()); + #[test] + fn turn_rest_testing() { + create_turn_server(&AuthMethod::Secret(PASSWORD.to_string())); - let value = ret.get::().unwrap(); - assert_eq!(value, TOKEN_BUF.as_slice()); -} + let mut local = TurnClient::new(&AuthMethod::Secret(PASSWORD.to_string())); + local.binding_request(); + local.base_allocate_request(); -#[cfg(test)] -mod tests { - #[tokio::test] - async fn integration_testing() { - crate::create_turn().await; - let socket = crate::create_client().await; - crate::binding_request(&socket).await; - crate::base_allocate_request(&socket).await; - let port = crate::allocate_request(&socket).await; - crate::create_permission_request(&socket, port).await; - crate::channel_bind_request(&socket, port).await; - crate::refresh_request(&socket).await; + let port = local.allocate_request(); + local.create_permission_request(port); + local.channel_bind_request(port); + local.refresh_request(); } } diff --git a/turn-server.toml b/turn-server.toml index b289643..fb26df6 100644 --- a/turn-server.toml +++ b/turn-server.toml @@ -51,17 +51,21 @@ bind = "127.0.0.1:3000" # # hooks = "http://127.0.0.1:8080" -# Credentials used by the http interface, credentials are carried in the -# http request and are used to authenticate the request. -# -# credential = "" - [log] # log level # # An enum representing the available verbosity levels of the logger. level = "info" +[auth] +# Static authentication key value (string) that applies only to the TURN +# REST API. +# +# If set, the turn server will not request external services via the HTTP +# Hooks API to obtain the key. +# +# static_auth_secret = "" + # static user password # # This option can be used to specify the @@ -69,6 +73,6 @@ level = "info" # verification. Note: this is a high-priority authentication method, turn # The server will try to use static authentication first, and then use # external control service authentication. -[auth] +[auth.static_credentials] # user1 = "test" # user2 = "test" diff --git a/turn-server/Cargo.toml b/turn-server/Cargo.toml index 408423d..b34b9fd 100644 --- a/turn-server/Cargo.toml +++ b/turn-server/Cargo.toml @@ -25,6 +25,7 @@ ahash = "0.8.3" async-trait = "0.1" anyhow = "1.0" axum = "0.7.5" +base64 = "0.22.1" bytes = "1.4.0" clap = { version = "4", features = ["derive"] } log = "0.4" diff --git a/turn-server/src/config.rs b/turn-server/src/config.rs index 197a1bc..87dcf4b 100644 --- a/turn-server/src/config.rs +++ b/turn-server/src/config.rs @@ -92,9 +92,6 @@ pub struct Api { /// through this service, please do not expose it directly to an unsafe /// environment. pub hooks: Option, - /// Credentials used by the http interface, credentials are carried in the - /// http request and are used to authenticate the request. - pub credential: Option, } impl Api { @@ -107,7 +104,6 @@ impl Default for Api { fn default() -> Self { Self { hooks: None, - credential: None, bind: Self::bind(), } } @@ -150,6 +146,25 @@ pub struct Log { pub level: LogLevel, } +#[derive(Deserialize, Debug, Default)] +pub struct Auth { + /// static user password + /// + /// This option can be used to specify the + /// static identity authentication information used by the turn server for + /// verification. Note: this is a high-priority authentication method, turn + /// The server will try to use static authentication first, and then use + /// external control service authentication. + #[serde(default)] + pub static_credentials: HashMap, + /// Static authentication key value (string) that applies only to the TURN + /// REST API. + /// + /// If set, the turn server will not request external services via the HTTP + /// Hooks API to obtain the key. + pub static_auth_secret: Option, +} + #[derive(Deserialize, Debug)] pub struct Config { #[serde(default)] @@ -158,16 +173,8 @@ pub struct Config { pub api: Api, #[serde(default)] pub log: Log, - - /// static user password - /// - /// This option can be used to specify the - /// static identity authentication information used by the turn server for - /// verification. Note: this is a high-priority authentication method, turn - /// The server will try to use static authentication first, and then use - /// external control service authentication. #[serde(default)] - pub auth: HashMap, + pub auth: Auth, } #[derive(Parser)] diff --git a/turn-server/src/credentials.rs b/turn-server/src/credentials.rs deleted file mode 100644 index aa5862b..0000000 --- a/turn-server/src/credentials.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::{ - sync::{Arc, RwLock}, - thread::{self, sleep}, - time::{Duration, Instant}, -}; - -use ahash::HashMap; - -pub struct StaticPassword { - pub value: String, - lifetime: Option, -} - -#[derive(Clone)] -pub struct StaticCredentials(Arc>>); - -impl From> for StaticCredentials { - fn from(value: std::collections::HashMap) -> Self { - let this = Self::new(); - - for (k, v) in value { - this.set(k, v, true); - } - - this - } -} - -impl AsRef>>> for StaticCredentials { - fn as_ref(&self) -> &Arc>> { - &self.0 - } -} - -impl StaticCredentials { - pub fn new() -> Self { - let map: Arc>> = Default::default(); - - let map_ = Arc::downgrade(&map); - thread::spawn(move || { - let mut keys_ = Vec::new(); - - while let Some(map) = map_.upgrade() { - keys_.clear(); - - { - for (key, value) in map.read().unwrap().iter() { - if let Some(lifetime) = value.lifetime { - if lifetime.elapsed().as_secs() >= 86400 { - keys_.push(key.clone()); - } - } - } - - let mut map_ = map.write().unwrap(); - for key in &keys_ { - map_.remove(key); - } - } - - sleep(Duration::from_secs(60)); - } - }); - - Self(map) - } - - pub fn set(&self, username: String, password: String, permanent: bool) { - self.0.write().unwrap().insert( - username, - StaticPassword { - value: password, - lifetime: if !permanent { - Some(Instant::now()) - } else { - None - }, - }, - ); - } -} diff --git a/turn-server/src/lib.rs b/turn-server/src/lib.rs index d30c97b..3f69025 100644 --- a/turn-server/src/lib.rs +++ b/turn-server/src/lib.rs @@ -1,5 +1,4 @@ pub mod config; -pub mod credentials; pub mod observer; pub mod publicly; pub mod router; @@ -10,25 +9,21 @@ use std::sync::Arc; use turn::Service; -use self::{ - config::Config, credentials::StaticCredentials, observer::Observer, statistics::Statistics, -}; +use self::{config::Config, observer::Observer, statistics::Statistics}; /// In order to let the integration test directly use the turn-server crate and /// start the server, a function is opened to replace the main function to /// directly start the server. pub async fn startup(config: Arc) -> anyhow::Result<()> { let statistics = Statistics::default(); - let credentials = StaticCredentials::from(config.auth.clone()); - let service = Service::new( config.turn.realm.clone(), config.turn.get_externals(), - Observer::new(config.clone(), statistics.clone(), credentials.clone()).await?, + Observer::new(config.clone(), statistics.clone()).await?, ); server::run(config.clone(), statistics.clone(), &service).await?; - publicly::start_server(config, service, statistics, credentials.clone()).await?; + publicly::start_server(config, service, statistics).await?; Ok(()) } diff --git a/turn-server/src/observer.rs b/turn-server/src/observer.rs index 4485aad..9e71756 100644 --- a/turn-server/src/observer.rs +++ b/turn-server/src/observer.rs @@ -1,8 +1,6 @@ use std::{net::SocketAddr, sync::Arc}; -use crate::{ - config::Config, credentials::StaticCredentials, publicly::HooksService, statistics::Statistics, -}; +use crate::{config::Config, publicly::HooksService, statistics::Statistics}; use anyhow::Result; use async_trait::async_trait; @@ -14,13 +12,9 @@ pub struct Observer { } impl Observer { - pub async fn new( - config: Arc, - statistics: Statistics, - credentials: StaticCredentials, - ) -> Result { + pub async fn new(config: Arc, statistics: Statistics) -> Result { Ok(Self { - hooks: HooksService::new(config, credentials)?, + hooks: HooksService::new(config)?, statistics, }) } diff --git a/turn-server/src/publicly.rs b/turn-server/src/publicly.rs index a0161b5..b5566ce 100644 --- a/turn-server/src/publicly.rs +++ b/turn-server/src/publicly.rs @@ -1,14 +1,11 @@ +use core::str; use std::{ net::SocketAddr, sync::Arc, time::{Duration, Instant}, }; -use crate::{ - config::{Config, Transport}, - credentials::StaticCredentials, - statistics::Statistics, -}; +use crate::{config::Config, statistics::Statistics}; use axum::{ extract::{Query, State}, @@ -19,6 +16,7 @@ use axum::{ Json, Router, }; +use base64::prelude::*; use once_cell::sync::Lazy; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use reqwest::{ @@ -41,7 +39,6 @@ struct AppState { config: Arc, service: Service, statistics: Statistics, - credentials: StaticCredentials, uptime: Instant, } @@ -51,13 +48,6 @@ struct QueryFilter { username: Option, } -#[derive(Deserialize)] -struct TurnRestRequest { - service: String, - username: Option, - key: Option, -} - /// start http server /// /// Create an http server and start it, and you can access the controller @@ -71,64 +61,15 @@ pub async fn start_server( config: Arc, service: Service, statistics: Statistics, - credentials: StaticCredentials, ) -> anyhow::Result<()> { let state = Arc::new(AppState { config: config.clone(), uptime: Instant::now(), service, statistics, - credentials, }); let app = Router::new() - .route( - "/", - get( - |Query(TurnRestRequest { - service, - username, - key, - }): Query, - State(state): State>| async move { - if service != "turn" || state.config.api.credential != key { - return StatusCode::NOT_FOUND.into_response(); - } - - let username = username.unwrap_or_else(|| random_string(20)); - let password = random_string(50); - - // Cache the user credentials automatically generated by the turn server. - state.credentials.set(username.clone(), password.clone(), false); - - let uris = state - .config - .turn - .interfaces - .iter() - .map(|it| { - format!( - "turn:{}:{}?transport={}", - it.external, - it.bind.port(), - match it.transport { - Transport::TCP => "tcp", - Transport::UDP => "udp", - } - ) - }) - .collect::>(); - - Json(json!({ - "password": stun::util::long_key(&username, &password, &state.config.turn.realm), - "username": format!(":{}", username), - "ttl": 86400, - "uris": uris, - })) - .into_response() - }, - ), - ) .route( "/info", get(|State(state): State>| async move { @@ -229,10 +170,6 @@ pub async fn start_server( HeaderValue::from_str(&state.config.turn.realm).unwrap(), ); - if let Some(credential) = &state.config.api.credential { - headers.insert("Credential", HeaderValue::from_str(credential).unwrap()); - } - res }, )) @@ -247,20 +184,15 @@ pub async fn start_server( pub struct HooksService { client: Arc, tx: UnboundedSender, - credentials: StaticCredentials, config: Arc, } impl HooksService { - pub fn new(config: Arc, credentials: StaticCredentials) -> anyhow::Result { + pub fn new(config: Arc) -> anyhow::Result { let mut headers = HeaderMap::new(); headers.insert("Realm", HeaderValue::from_str(&config.turn.realm)?); headers.insert("Rid", HeaderValue::from_str(&RID)?); - if let Some(credential) = &config.api.credential { - headers.insert("Credential", HeaderValue::from_str(credential)?); - } - let client = Arc::new( ClientBuilder::new() .default_headers(headers) @@ -283,17 +215,16 @@ impl HooksService { } }); - Ok(Self { - client, - config, - credentials, - tx, - }) + Ok(Self { client, config, tx }) } pub async fn get_password(&self, addr: &SocketAddr, name: &str) -> Option { - if let Some(pwd) = self.credentials.as_ref().read().unwrap().get(name) { - return Some(pwd.value.clone()); + if let Some(pwd) = self.config.auth.static_credentials.get(name) { + return Some(pwd.clone()); + } + + if let Some(secret) = &self.config.auth.static_auth_secret { + return encode_password(secret, name); } if let Some(server) = &self.config.api.hooks { @@ -329,3 +260,15 @@ fn random_string(len: usize) -> String { .collect::() .to_lowercase() } + +// https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00#section-2.2 +fn encode_password(key: &str, username: &str) -> Option { + Some( + BASE64_STANDARD.encode( + stun::util::hmac_sha1(key.as_bytes(), &[username.as_bytes()]) + .ok()? + .into_bytes() + .as_slice(), + ), + ) +} diff --git a/turn/src/router/nodes.rs b/turn/src/router/nodes.rs index 26ae37e..49998e1 100644 --- a/turn/src/router/nodes.rs +++ b/turn/src/router/nodes.rs @@ -269,7 +269,7 @@ impl Nodes { password: &str, ) -> Option> { let node = Node::new(realm, username, password); - let pwd = node.get_secret(); + let secret = node.get_secret(); let mut addrs = self.addrs.write().unwrap(); self.map.write().unwrap().insert(*addr, node); @@ -277,7 +277,7 @@ impl Nodes { .entry(username.to_string()) .or_insert_with(|| AHashSet::with_capacity(5)) .insert(*addr); - Some(pwd) + Some(secret) } /// push port to node. From 8124f0c98d091d6373d60d6c7d7beac220eba551 Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Fri, 13 Sep 2024 15:36:08 +0800 Subject: [PATCH 5/7] fix tests; --- tests/benches/benchmark.rs | 7 ++++--- tests/src/lib.rs | 41 ++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/tests/benches/benchmark.rs b/tests/benches/benchmark.rs index 181e69e..be9cac1 100644 --- a/tests/benches/benchmark.rs +++ b/tests/benches/benchmark.rs @@ -2,10 +2,11 @@ use criterion::*; use tests::{create_turn_server, AuthMethod, TurnClient}; fn criterion_benchmark(c: &mut Criterion) { - create_turn_server(&AuthMethod::Static); + let bind = "127.0.0.1:3578".parse().unwrap(); + create_turn_server(&AuthMethod::Static, bind); - let mut local = TurnClient::new(&AuthMethod::Static); - let mut peer = TurnClient::new(&AuthMethod::Static); + let mut local = TurnClient::new(&AuthMethod::Static, bind); + let mut peer = TurnClient::new(&AuthMethod::Static, bind); let local_port = local.allocate_request(); let peer_port = peer.allocate_request(); diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 34e62aa..2ca5fbc 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -14,14 +14,13 @@ use turn_server::{ startup, }; -use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; +use std::{collections::HashMap, thread::sleep, time::Duration}; static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); static BIND_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); -static BIND_ADDR: SocketAddr = SocketAddr::new(BIND_IP, 3478); static USERNAME: &str = "user1"; static PASSWORD: &str = "test"; static REALM: &str = "localhost"; @@ -33,7 +32,7 @@ pub enum AuthMethod { Hooks(String), } -pub fn create_turn_server(auth_method: &AuthMethod) { +pub fn create_turn_server(auth_method: &AuthMethod, bind: SocketAddr) { let mut static_credentials = HashMap::new(); let mut static_auth_secret = None; let mut api = Api::default(); @@ -62,14 +61,16 @@ pub fn create_turn_server(auth_method: &AuthMethod) { realm: REALM.to_string(), interfaces: vec![Interface { transport: config::Transport::UDP, - bind: BIND_ADDR, - external: BIND_ADDR, + external: bind, + bind, }], }, })) .await .unwrap(); }); + + sleep(Duration::from_secs(2)); } pub struct TurnClient { @@ -82,15 +83,16 @@ pub struct TurnClient { bind_request_buf: BytesMut, base_allocate_request_buf: BytesMut, allocate_request_buf: BytesMut, + bind: SocketAddr, } impl TurnClient { - pub fn new(auth_method: &AuthMethod) -> Self { + pub fn new(auth_method: &AuthMethod, bind: SocketAddr) -> Self { let client = RUNTIME .block_on(UdpSocket::bind(SocketAddr::new(BIND_IP, 0))) .unwrap(); - RUNTIME.block_on(client.connect(BIND_ADDR)).unwrap(); + RUNTIME.block_on(client.connect(bind)).unwrap(); let key = match &auth_method { AuthMethod::Static => PASSWORD.to_string(), @@ -145,6 +147,7 @@ impl TurnClient { decoder: Decoder::new(), token_buf, client, + bind, } } @@ -152,7 +155,7 @@ impl TurnClient { RUNTIME .block_on(self.client.send(&self.bind_request_buf)) .unwrap(); - + let size = RUNTIME .block_on(self.client.recv(&mut self.recv_buf)) .unwrap(); @@ -170,7 +173,7 @@ impl TurnClient { assert_eq!(value, self.client.local_addr().unwrap()); let value = ret.get::().unwrap(); - assert_eq!(value, BIND_ADDR); + assert_eq!(value, self.bind); } pub fn base_allocate_request(&mut self) { @@ -341,13 +344,18 @@ impl TurnClient { #[cfg(test)] mod tests { - use crate::{create_turn_server, AuthMethod, TurnClient, PASSWORD}; + use std::net::SocketAddr; + + use crate::{create_turn_server, AuthMethod, TurnClient, BIND_IP, PASSWORD}; #[test] - fn integration_testing() { - create_turn_server(&AuthMethod::Static); + fn static_auth_testing() { + let bind = SocketAddr::new(BIND_IP, 4478); + let auth = AuthMethod::Static; - let mut local = TurnClient::new(&AuthMethod::Static); + create_turn_server(&auth, bind); + + let mut local = TurnClient::new(&auth, bind); local.binding_request(); local.base_allocate_request(); @@ -359,9 +367,12 @@ mod tests { #[test] fn turn_rest_testing() { - create_turn_server(&AuthMethod::Secret(PASSWORD.to_string())); + let bind = SocketAddr::new(BIND_IP, 4479); + let auth = AuthMethod::Secret(PASSWORD.to_string()); + + create_turn_server(&auth, bind); - let mut local = TurnClient::new(&AuthMethod::Secret(PASSWORD.to_string())); + let mut local = TurnClient::new(&auth, bind); local.binding_request(); local.base_allocate_request(); From 675fbde56b0b3c61e15b91c9bd70fe6fa586aa0b Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Fri, 13 Sep 2024 16:55:37 +0800 Subject: [PATCH 6/7] fix workflow; --- .github/workflows/release.yml | 10 +++++----- tests/src/lib.rs | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03fe974..c056451 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.cargo/bin/ @@ -70,7 +70,7 @@ jobs: - name: Upload artifact (Linux) if: runner.os == 'Linux' && matrix.arch == 'x86_64' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linux-x86_64 path: | @@ -78,7 +78,7 @@ jobs: - name: Upload artifact (Linux) if: runner.os == 'Linux' && matrix.arch == 'aarch64' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linux-x86_64 path: | @@ -86,7 +86,7 @@ jobs: - name: Upload artifact (Windows) if: runner.os == 'Windows' && matrix.arch == 'x86_64' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: windows-x86_64 path: | @@ -100,7 +100,7 @@ jobs: uses: actions/checkout@v3 - name: Download All Artifacts - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4 with: path: artifacts - diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 2ca5fbc..35f93e1 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,22 +1,29 @@ use base64::prelude::*; use bytes::BytesMut; use once_cell::sync::Lazy; -use stun::attribute::{ - ChannelNumber, Data, ErrKind, ErrorCode, Lifetime, MappedAddress, Realm, ReqeestedTransport, - ResponseOrigin, Transport, UserName, XorMappedAddress, XorPeerAddress, XorRelayedAddress, +use stun::{ + attribute::{ + ChannelNumber, Data, ErrKind, ErrorCode, Lifetime, MappedAddress, Realm, + ReqeestedTransport, ResponseOrigin, Transport, UserName, XorMappedAddress, XorPeerAddress, + XorRelayedAddress, + }, + Decoder, Kind, MessageReader, MessageWriter, Method, Payload, }; use rand::seq::SliceRandom; -use stun::{Decoder, Kind, MessageReader, MessageWriter, Method, Payload}; use tokio::{net::UdpSocket, runtime::Runtime}; use turn_server::{ config::{self, *}, startup, }; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::sync::Arc; -use std::{collections::HashMap, thread::sleep, time::Duration}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, + thread::sleep, + time::Duration, +}; static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); From 800b9a8c391902c4cfea5254b9cff29b7847263e Mon Sep 17 00:00:00 2001 From: Lazy Panda Date: Fri, 13 Sep 2024 17:04:35 +0800 Subject: [PATCH 7/7] Update release.yml --- .github/workflows/release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c056451..807038e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,7 @@ jobs: if: runner.os == 'Linux' && matrix.arch == 'aarch64' uses: actions/upload-artifact@v4 with: - name: linux-x86_64 + name: linux-aarch64 path: | ./target/aarch64-unknown-linux-gnu/release/turn-server-linux-aarch64 - @@ -95,9 +95,6 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - - name: Checkout code - uses: actions/checkout@v3 - name: Download All Artifacts uses: actions/download-artifact@v4