Skip to main content

turn_server_sdk/
lib.rs

1//! # Turn Server SDK
2//!
3//! A Rust client SDK for interacting with the `turn-server` gRPC API exposed by the `turn-rs` project.
4//! This crate provides both client and server utilities for TURN server integration.
5//!
6//! ## Features
7//!
8//! - **TurnService Client**: Query server information, session details, and manage TURN sessions
9//! - **TurnHooksServer**: Implement custom authentication and event handling for TURN server hooks
10//! - **Password Generation**: Generate STUN/TURN authentication passwords using MD5 or SHA256
11//!
12//! ## Client Usage
13//!
14//! The `TurnService` client allows you to interact with a running TURN server's gRPC API:
15//!
16//! ```no_run
17//! use tonic::transport::Channel;
18//! use turn_server_sdk::TurnService;
19//!
20//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21//! // Connect to the TURN server gRPC endpoint
22//! let channel = Channel::from_static("http://127.0.0.1:3000")
23//!     .connect()
24//!     .await?;
25//!
26//! // Create a client
27//! let mut client = TurnService::new(channel);
28//!
29//! // Get server information
30//! let info = client.get_info().await?;
31//! println!("Server software: {}", info.software);
32//!
33//! // Query a session by ID
34//! let session = client.get_session("session-id".to_string()).await?;
35//! println!("Session username: {}", session.username);
36//!
37//! // Get session statistics
38//! let stats = client.get_session_statistics("session-id".to_string()).await?;
39//! println!("Bytes sent: {}", stats.send_bytes);
40//!
41//! // Destroy a session
42//! client.destroy_session("session-id".to_string()).await?;
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ## Server Usage (Hooks Implementation)
48//!
49//! Implement the `TurnHooksServer` trait to provide custom authentication and handle TURN events:
50//!
51//! ```no_run
52//! use tonic::transport::Server;
53//! use turn_server_sdk::{
54//!     TurnHooksServer, Credential, protos::PasswordAlgorithm,
55//! };
56//!
57//! use std::net::SocketAddr;
58//!
59//! struct MyHooksServer;
60//!
61//! #[tonic::async_trait]
62//! impl TurnHooksServer for MyHooksServer {
63//!     async fn get_password(
64//!         &self,
65//!         realm: &str,
66//!         username: &str,
67//!         algorithm: PasswordAlgorithm,
68//!     ) -> Result<Credential, tonic::Status> {
69//!         // Implement your authentication logic here
70//!         // For example, look up the user in a database
71//!         Ok(Credential {
72//!             password: "user-password".to_string(),
73//!             realm: realm.to_string(),
74//!         })
75//!     }
76//!
77//!     async fn on_allocated(&self, id: String, username: String, port: u16) {
78//!         println!("Session allocated: id={}, username={}, port={}", id, username, port);
79//!         // Handle allocation event (e.g., log to database, update metrics)
80//!     }
81//!
82//!     async fn on_destroy(&self, id: String, username: String) {
83//!         println!("Session destroyed: id={}, username={}", id, username);
84//!         // Handle session destruction (e.g., cleanup resources)
85//!     }
86//! }
87//!
88//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
89//! // Start the hooks server
90//! let mut server = Server::builder();
91//! let hooks = MyHooksServer;
92//!
93//! hooks.start_with_server(
94//!     &mut server,
95//!     "127.0.0.1:8080".parse()?,
96//! ).await?;
97//! # Ok(())
98//! # }
99//! ```
100//!
101//! ## Password Generation
102//!
103//! Generate STUN/TURN authentication passwords for long-term credentials:
104//!
105//! ```no_run
106//! use turn_server_sdk::{generate_password, protos::PasswordAlgorithm};
107//!
108//! // Generate MD5 password (RFC 5389)
109//! let md5_password = generate_password(
110//!     "username",
111//!     "password",
112//!     "realm",
113//!     PasswordAlgorithm::Md5,
114//! );
115//!
116//! // Generate SHA256 password (RFC 8489)
117//! let sha256_password = generate_password(
118//!     "username",
119//!     "password",
120//!     "realm",
121//!     PasswordAlgorithm::Sha256,
122//! );
123//!
124//! // Access the password bytes
125//! match md5_password {
126//!     turn_server_sdk::Password::Md5(bytes) => {
127//!         println!("MD5 password: {:?}", bytes);
128//!     }
129//!     turn_server_sdk::Password::Sha256(bytes) => {
130//!         println!("SHA256 password: {:?}", bytes);
131//!     }
132//! }
133//! ```
134//!
135//! ## Event Handling
136//!
137//! The `TurnHooksServer` trait provides hooks for various TURN server events:
138//!
139//! - `on_allocated`: Called when a client allocates a relay port
140//! - `on_channel_bind`: Called when a channel is bound to a peer
141//! - `on_create_permission`: Called when permissions are created for peers
142//! - `on_refresh`: Called when a session is refreshed
143//! - `on_destroy`: Called when a session is destroyed
144//!
145//! All event handlers are optional and have default no-op implementations.
146//!
147//! ## Error Handling
148//!
149//! Most operations return `Result<T, Status>` where `Status` is a gRPC status code.
150//! Common error scenarios:
151//!
152//! - `Status::not_found`: Session or resource not found
153//! - `Status::unavailable`: Server is not available
154//! - `Status::unauthenticated`: Authentication failed
155//!
156//! ## Re-exports
157//!
158//! This crate re-exports:
159//! - `tonic`: The gRPC framework used for communication
160//! - `protos`: The generated protobuf bindings for TURN server messages
161//!
162//! ## See Also
163//!
164//! - [TURN Server Documentation](../README.md)
165//! - [RFC 8489](https://tools.ietf.org/html/rfc8489) - Session Traversal Utilities for NAT (STUN)
166//! - [RFC 8656](https://tools.ietf.org/html/rfc8656) - Traversal Using Relays around NAT (TURN)
167
168pub use protos;
169use sha2::Sha256;
170
171use std::{net::SocketAddr, ops::Deref};
172
173use md5::{Digest, Md5};
174use tonic::{
175    Request, Response, Status,
176    transport::{Channel, Server},
177};
178
179use protos::{
180    GetTurnPasswordRequest, GetTurnPasswordResponse, PasswordAlgorithm, SessionQueryParams,
181    TurnAllocatedEvent, TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent,
182    TurnRefreshEvent, TurnServerInfo, TurnSession, TurnSessionStatistics,
183    turn_hooks_service_server::{TurnHooksService, TurnHooksServiceServer},
184    turn_service_client::TurnServiceClient,
185};
186
187/// turn service client
188///
189/// This struct is used to interact with the turn service.
190pub struct TurnService(TurnServiceClient<Channel>);
191
192impl TurnService {
193    /// create a new turn service client
194    pub fn new(channel: Channel) -> Self {
195        Self(TurnServiceClient::new(channel))
196    }
197
198    /// get the server info
199    pub async fn get_info(&mut self) -> Result<TurnServerInfo, Status> {
200        Ok(self.0.get_info(Request::new(())).await?.into_inner())
201    }
202
203    /// get the session
204    pub async fn get_session(&mut self, id: String) -> Result<TurnSession, Status> {
205        Ok(self
206            .0
207            .get_session(Request::new(SessionQueryParams { id }))
208            .await?
209            .into_inner())
210    }
211
212    /// get the session statistics
213    pub async fn get_session_statistics(
214        &mut self,
215        id: String,
216    ) -> Result<TurnSessionStatistics, Status> {
217        Ok(self
218            .0
219            .get_session_statistics(Request::new(SessionQueryParams { id }))
220            .await?
221            .into_inner())
222    }
223
224    /// destroy the session
225    pub async fn destroy_session(&mut self, id: String) -> Result<(), Status> {
226        Ok(self
227            .0
228            .destroy_session(Request::new(SessionQueryParams { id }))
229            .await?
230            .into_inner())
231    }
232}
233
234/// credential
235///
236/// This struct is used to store the credential for the turn hooks server.
237pub struct Credential {
238    pub password: String,
239    pub realm: String,
240}
241
242struct TurnHooksServerInner<T>(T);
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum Password {
246    Md5([u8; 16]),
247    Sha256([u8; 32]),
248}
249
250impl Deref for Password {
251    type Target = [u8];
252
253    fn deref(&self) -> &Self::Target {
254        match self {
255            Password::Md5(it) => it,
256            Password::Sha256(it) => it,
257        }
258    }
259}
260
261pub fn generate_password(
262    username: &str,
263    password: &str,
264    realm: &str,
265    algorithm: PasswordAlgorithm,
266) -> Password {
267    match algorithm {
268        PasswordAlgorithm::Md5 => {
269            let mut hasher = Md5::new();
270
271            hasher.update([username, realm, password].join(":"));
272
273            Password::Md5(hasher.finalize().into())
274        }
275        PasswordAlgorithm::Sha256 => {
276            let mut hasher = Sha256::new();
277
278            hasher.update([username, realm, password].join(":").as_bytes());
279
280            let mut result = [0u8; 32];
281            result.copy_from_slice(&hasher.finalize());
282            Password::Sha256(result)
283        }
284        PasswordAlgorithm::Unspecified => {
285            panic!("Invalid password algorithm");
286        }
287    }
288}
289
290#[tonic::async_trait]
291impl<T: TurnHooksServer + 'static> TurnHooksService for TurnHooksServerInner<T> {
292    async fn get_password(
293        &self,
294        request: Request<GetTurnPasswordRequest>,
295    ) -> Result<Response<GetTurnPasswordResponse>, Status> {
296        let request = request.into_inner();
297        let algorithm = request.algorithm();
298
299        if let Ok(credential) = self
300            .0
301            .get_password(&request.realm, &request.username, algorithm)
302            .await
303        {
304            Ok(Response::new(GetTurnPasswordResponse {
305                password: generate_password(
306                    &request.username,
307                    &credential.password,
308                    &credential.realm,
309                    algorithm,
310                )
311                .to_vec(),
312            }))
313        } else {
314            Err(Status::not_found("Message integrity not found"))
315        }
316    }
317
318    async fn on_allocated_event(
319        &self,
320        request: Request<TurnAllocatedEvent>,
321    ) -> Result<Response<()>, Status> {
322        let request = request.into_inner();
323        self.0
324            .on_allocated(request.id, request.username, request.port as u16)
325            .await;
326
327        Ok(Response::new(()))
328    }
329
330    async fn on_channel_bind_event(
331        &self,
332        request: Request<TurnChannelBindEvent>,
333    ) -> Result<Response<()>, Status> {
334        let request = request.into_inner();
335        self.0
336            .on_channel_bind(request.id, request.username, request.channel as u16)
337            .await;
338
339        Ok(Response::new(()))
340    }
341
342    async fn on_create_permission_event(
343        &self,
344        request: Request<TurnCreatePermissionEvent>,
345    ) -> Result<Response<()>, Status> {
346        let request = request.into_inner();
347        self.0
348            .on_create_permission(
349                request.id,
350                request.username,
351                request.ports.iter().map(|p| *p as u16).collect(),
352            )
353            .await;
354
355        Ok(Response::new(()))
356    }
357
358    async fn on_refresh_event(
359        &self,
360        request: Request<TurnRefreshEvent>,
361    ) -> Result<Response<()>, Status> {
362        let request = request.into_inner();
363        self.0
364            .on_refresh(request.id, request.username, request.lifetime as u32)
365            .await;
366
367        Ok(Response::new(()))
368    }
369
370    async fn on_destroy_event(
371        &self,
372        request: Request<TurnDestroyEvent>,
373    ) -> Result<Response<()>, Status> {
374        let request = request.into_inner();
375        self.0.on_destroy(request.id, request.username).await;
376
377        Ok(Response::new(()))
378    }
379}
380
381#[tonic::async_trait]
382pub trait TurnHooksServer: Send + Sync {
383    #[allow(unused_variables)]
384    async fn get_password(
385        &self,
386        realm: &str,
387        username: &str,
388        algorithm: PasswordAlgorithm,
389    ) -> Result<Credential, Status> {
390        Err(Status::unimplemented("get_password is not implemented"))
391    }
392
393    /// allocate request
394    ///
395    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
396    ///
397    /// In all cases, the server SHOULD only allocate ports from the range
398    /// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]),
399    /// unless the TURN server application knows, through some means not
400    /// specified here, that other applications running on the same host as
401    /// the TURN server application will not be impacted by allocating ports
402    /// outside this range.  This condition can often be satisfied by running
403    /// the TURN server application on a dedicated machine and/or by
404    /// arranging that any other applications on the machine allocate ports
405    /// before the TURN server application starts.  In any case, the TURN
406    /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well-
407    /// Known Port range) to discourage clients from using TURN to run
408    /// standard services.
409    #[allow(unused_variables)]
410    async fn on_allocated(&self, id: String, username: String, port: u16) {}
411
412    /// channel bind request
413    ///
414    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
415    ///
416    /// If the request is valid, but the server is unable to fulfill the
417    /// request due to some capacity limit or similar, the server replies
418    /// with a 508 (Insufficient Capacity) error.
419    ///
420    /// Otherwise, the server replies with a ChannelBind success response.
421    /// There are no required attributes in a successful ChannelBind
422    /// response.
423    #[allow(unused_variables)]
424    async fn on_channel_bind(&self, id: String, username: String, channel: u16) {}
425
426    /// create permission request
427    ///
428    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
429    ///
430    /// If the request is valid, but the server is unable to fulfill the
431    /// request due to some capacity limit or similar, the server replies
432    /// with a 508 (Insufficient Capacity) error.
433    ///
434    /// Otherwise, the server replies with a ChannelBind success response.
435    /// There are no required attributes in a successful ChannelBind
436    /// response.
437    #[allow(unused_variables)]
438    async fn on_create_permission(&self, id: String, username: String, ports: Vec<u16>) {}
439
440    /// refresh request
441    ///
442    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
443    ///
444    /// If the request is valid, but the server is unable to fulfill the
445    /// request due to some capacity limit or similar, the server replies
446    /// with a 508 (Insufficient Capacity) error.
447    ///
448    /// Otherwise, the server replies with a ChannelBind success response.
449    /// There are no required attributes in a successful ChannelBind
450    /// response.
451    #[allow(unused_variables)]
452    async fn on_refresh(&self, id: String, username: String, lifetime: u32) {}
453
454    /// session closed
455    ///
456    /// Triggered when the session leaves from the turn. Possible reasons: the
457    /// session life cycle has expired, external active deletion, or active
458    /// exit of the session.
459    #[allow(unused_variables)]
460    async fn on_destroy(&self, id: String, username: String) {}
461
462    /// start the turn hooks server
463    ///
464    /// This function will start the turn hooks server on the given server and listen address.
465    async fn start_with_server(
466        self,
467        server: &mut Server,
468        listen: SocketAddr,
469    ) -> Result<(), tonic::transport::Error>
470    where
471        Self: Sized + 'static,
472    {
473        server
474            .add_service(TurnHooksServiceServer::<TurnHooksServerInner<Self>>::new(
475                TurnHooksServerInner(self),
476            ))
477            .serve(listen)
478            .await?;
479
480        Ok(())
481    }
482}