turn-server-sdk 0.3.0

Client SDK for interacting with the turn-server gRPC API.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
//! # Turn Server SDK
//!
//! A Rust client SDK for interacting with the `turn-server` gRPC API exposed by the `turn-rs` project.
//! This crate provides both client and server utilities for TURN server integration.
//!
//! ## Features
//!
//! - **TurnService Client**: Query server information, session details, and manage TURN sessions
//! - **TurnHooksServer**: Implement custom authentication and event handling for TURN server hooks
//! - **Password Generation**: Generate STUN/TURN authentication passwords using MD5 or SHA256
//!
//! ## Client Usage
//!
//! The `TurnService` client allows you to interact with a running TURN server's gRPC API:
//!
//! ```no_run
//! use tonic::transport::Channel;
//! use turn_server_sdk::{TurnService, protos::{Identifier, Transport}};
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // Connect to the TURN server gRPC endpoint
//! let channel = Channel::from_static("http://127.0.0.1:3000")
//!     .connect()
//!     .await?;
//!
//! // Create a client
//! let mut client = TurnService::new(channel);
//!
//! // Get server information
//! let info = client.get_info().await?;
//! println!("Server software: {}", info.software);
//!
//! let id = Identifier {
//!     source: "127.0.0.1".to_string(),
//!     external: "127.0.0.1".to_string(),
//!     interface: "127.0.0.1".to_string(),
//!     transport: Transport::Udp as i32,
//! };
//!
//! // Query a session by ID
//! let session = client.get_session(id.clone()).await?;
//! println!("Session username: {}", session.username);
//!
//! // Get session statistics
//! let stats = client.get_session_statistics(id.clone()).await?;
//! println!("Bytes sent: {}", stats.send_bytes);
//!
//! // Destroy a session
//! client.destroy_session(id).await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Server Usage (Hooks Implementation)
//!
//! Implement the `TurnHooksServer` trait to provide custom authentication and handle TURN events:
//!
//! ```no_run
//! use tonic::transport::Server;
//! use turn_server_sdk::{
//!     TurnHooksServer, Credential, protos::{PasswordAlgorithm, Identifier},
//! };
//!
//! use std::net::SocketAddr;
//!
//! struct MyHooksServer;
//!
//! #[tonic::async_trait]
//! impl TurnHooksServer for MyHooksServer {
//!     async fn get_password(
//!         &self,
//!         _id: Identifier,
//!         realm: &str,
//!         username: &str,
//!         algorithm: PasswordAlgorithm,
//!     ) -> Result<Credential, tonic::Status> {
//!         // Implement your authentication logic here
//!         // For example, look up the user in a database
//!         Ok(Credential {
//!             password: "user-password".to_string(),
//!             realm: realm.to_string(),
//!         })
//!     }
//!
//!     async fn on_allocated(&self, id: Identifier, username: String, port: u16) {
//!         println!("Session allocated: id={:?}, username={}, port={}", id, username, port);
//!         // Handle allocation event (e.g., log to database, update metrics)
//!     }
//!
//!     async fn on_destroy(&self, id: Identifier, username: String) {
//!         println!("Session destroyed: id={:?}, username={}", id, username);
//!         // Handle session destruction (e.g., cleanup resources)
//!     }
//! }
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // Start the hooks server
//! let mut server = Server::builder();
//! let hooks = MyHooksServer;
//!
//! hooks.start_with_server(
//!     &mut server,
//!     "127.0.0.1:8080".parse()?,
//! ).await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Password Generation
//!
//! Generate STUN/TURN authentication passwords for long-term credentials:
//!
//! ```no_run
//! use turn_server_sdk::{generate_password, protos::PasswordAlgorithm};
//!
//! // Generate MD5 password (RFC 5389)
//! let md5_password = generate_password(
//!     "username",
//!     "password",
//!     "realm",
//!     PasswordAlgorithm::Md5,
//! );
//!
//! // Generate SHA256 password (RFC 8489)
//! let sha256_password = generate_password(
//!     "username",
//!     "password",
//!     "realm",
//!     PasswordAlgorithm::Sha256,
//! );
//!
//! // Access the password bytes
//! match md5_password {
//!     turn_server_sdk::Password::Md5(bytes) => {
//!         println!("MD5 password: {:?}", bytes);
//!     }
//!     turn_server_sdk::Password::Sha256(bytes) => {
//!         println!("SHA256 password: {:?}", bytes);
//!     }
//! }
//! ```
//!
//! ## Event Handling
//!
//! The `TurnHooksServer` trait provides hooks for various TURN server events:
//!
//! - `on_allocated`: Called when a client allocates a relay port
//! - `on_channel_bind`: Called when a channel is bound to a peer
//! - `on_create_permission`: Called when permissions are created for peers
//! - `on_refresh`: Called when a session is refreshed
//! - `on_destroy`: Called when a session is destroyed
//!
//! All event handlers are optional and have default no-op implementations.
//!
//! ## Error Handling
//!
//! Most operations return `Result<T, Status>` where `Status` is a gRPC status code.
//! Common error scenarios:
//!
//! - `Status::not_found`: Session or resource not found
//! - `Status::unavailable`: Server is not available
//! - `Status::unauthenticated`: Authentication failed
//!
//! ## Re-exports
//!
//! This crate re-exports:
//! - `tonic`: The gRPC framework used for communication
//! - `protos`: The generated protobuf bindings for TURN server messages
//!
//! ## See Also
//!
//! - [TURN Server Documentation](../README.md)
//! - [RFC 8489](https://tools.ietf.org/html/rfc8489) - Session Traversal Utilities for NAT (STUN)
//! - [RFC 8656](https://tools.ietf.org/html/rfc8656) - Traversal Using Relays around NAT (TURN)

pub mod protos {
    tonic::include_proto!("turn.server");
}

use std::{net::SocketAddr, ops::Deref};

use md5::{Digest, Md5};
use sha2::Sha256;
use tonic::{
    Request, Response, Status,
    transport::{Channel, Server},
};

use self::protos::{
    GetTurnPasswordRequest, GetTurnPasswordResponse, Identifier, PasswordAlgorithm,
    TurnAllocatedEvent, TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent,
    TurnRefreshEvent, TurnServerInfo, TurnSession, TurnSessionStatistics,
    turn_hooks_service_server::{TurnHooksService, TurnHooksServiceServer},
    turn_service_client::TurnServiceClient,
};

/// turn service client
///
/// This struct is used to interact with the turn service.
pub struct TurnService(TurnServiceClient<Channel>);

impl TurnService {
    /// create a new turn service client
    pub fn new(channel: Channel) -> Self {
        Self(TurnServiceClient::new(channel))
    }

    /// get the server info
    pub async fn get_info(&mut self) -> Result<TurnServerInfo, Status> {
        Ok(self.0.get_info(Request::new(())).await?.into_inner())
    }

    /// get the session
    pub async fn get_session(&mut self, id: Identifier) -> Result<TurnSession, Status> {
        Ok(self.0.get_session(Request::new(id)).await?.into_inner())
    }

    /// get the session statistics
    pub async fn get_session_statistics(
        &mut self,
        id: Identifier,
    ) -> Result<TurnSessionStatistics, Status> {
        Ok(self
            .0
            .get_session_statistics(Request::new(id))
            .await?
            .into_inner())
    }

    /// destroy the session
    pub async fn destroy_session(&mut self, id: Identifier) -> Result<(), Status> {
        Ok(self.0.destroy_session(Request::new(id)).await?.into_inner())
    }
}

/// credential
///
/// This struct is used to store the credential for the turn hooks server.
pub struct Credential {
    pub password: String,
    pub realm: String,
}

struct TurnHooksServerInner<T>(T);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Password {
    Md5([u8; 16]),
    Sha256([u8; 32]),
}

impl Deref for Password {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        match self {
            Password::Md5(it) => it,
            Password::Sha256(it) => it,
        }
    }
}

pub fn generate_password(
    username: &str,
    password: &str,
    realm: &str,
    algorithm: PasswordAlgorithm,
) -> Password {
    match algorithm {
        PasswordAlgorithm::Md5 => {
            let mut hasher = Md5::new();

            hasher.update([username, realm, password].join(":"));

            Password::Md5(hasher.finalize().into())
        }
        PasswordAlgorithm::Sha256 => {
            let mut hasher = Sha256::new();

            hasher.update([username, realm, password].join(":").as_bytes());

            let mut result = [0u8; 32];
            result.copy_from_slice(&hasher.finalize());
            Password::Sha256(result)
        }
        PasswordAlgorithm::Unspecified => {
            panic!("Invalid password algorithm");
        }
    }
}

#[tonic::async_trait]
impl<T: TurnHooksServer + 'static> TurnHooksService for TurnHooksServerInner<T> {
    async fn get_password(
        &self,
        request: Request<GetTurnPasswordRequest>,
    ) -> Result<Response<GetTurnPasswordResponse>, Status> {
        let request = request.into_inner();
        let algorithm = request.algorithm();

        if let Ok(credential) = self
            .0
            .get_password(
                request
                    .id
                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
                &request.realm,
                &request.username,
                algorithm,
            )
            .await
        {
            Ok(Response::new(GetTurnPasswordResponse {
                password: generate_password(
                    &request.username,
                    &credential.password,
                    &credential.realm,
                    algorithm,
                )
                .to_vec(),
            }))
        } else {
            Err(Status::not_found("Message integrity not found"))
        }
    }

    async fn on_allocated_event(
        &self,
        request: Request<TurnAllocatedEvent>,
    ) -> Result<Response<()>, Status> {
        let request = request.into_inner();
        self.0
            .on_allocated(
                request
                    .id
                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
                request.username,
                request.port as u16,
            )
            .await;

        Ok(Response::new(()))
    }

    async fn on_channel_bind_event(
        &self,
        request: Request<TurnChannelBindEvent>,
    ) -> Result<Response<()>, Status> {
        let request = request.into_inner();
        self.0
            .on_channel_bind(
                request
                    .id
                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
                request.username,
                request.channel as u16,
            )
            .await;

        Ok(Response::new(()))
    }

    async fn on_create_permission_event(
        &self,
        request: Request<TurnCreatePermissionEvent>,
    ) -> Result<Response<()>, Status> {
        let request = request.into_inner();
        self.0
            .on_create_permission(
                request
                    .id
                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
                request.username,
                request.ports.iter().map(|p| *p as u16).collect(),
            )
            .await;

        Ok(Response::new(()))
    }

    async fn on_refresh_event(
        &self,
        request: Request<TurnRefreshEvent>,
    ) -> Result<Response<()>, Status> {
        let request = request.into_inner();
        self.0
            .on_refresh(
                request
                    .id
                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
                request.username,
                request.lifetime as u32,
            )
            .await;

        Ok(Response::new(()))
    }

    async fn on_destroy_event(
        &self,
        request: Request<TurnDestroyEvent>,
    ) -> Result<Response<()>, Status> {
        let request = request.into_inner();
        self.0
            .on_destroy(
                request
                    .id
                    .ok_or_else(|| Status::invalid_argument("identifier is None"))?,
                request.username,
            )
            .await;

        Ok(Response::new(()))
    }
}

#[tonic::async_trait]
pub trait TurnHooksServer: Send + Sync {
    #[allow(unused_variables)]
    async fn get_password(
        &self,
        id: Identifier,
        realm: &str,
        username: &str,
        algorithm: PasswordAlgorithm,
    ) -> Result<Credential, Status> {
        Err(Status::unimplemented("get_password is not implemented"))
    }

    /// allocate request
    ///
    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
    ///
    /// In all cases, the server SHOULD only allocate ports from the range
    /// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]),
    /// unless the TURN server application knows, through some means not
    /// specified here, that other applications running on the same host as
    /// the TURN server application will not be impacted by allocating ports
    /// outside this range.  This condition can often be satisfied by running
    /// the TURN server application on a dedicated machine and/or by
    /// arranging that any other applications on the machine allocate ports
    /// before the TURN server application starts.  In any case, the TURN
    /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well-
    /// Known Port range) to discourage clients from using TURN to run
    /// standard services.
    #[allow(unused_variables)]
    async fn on_allocated(&self, id: Identifier, username: String, port: u16) {}

    /// channel bind request
    ///
    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
    ///
    /// If the request is valid, but the server is unable to fulfill the
    /// request due to some capacity limit or similar, the server replies
    /// with a 508 (Insufficient Capacity) error.
    ///
    /// Otherwise, the server replies with a ChannelBind success response.
    /// There are no required attributes in a successful ChannelBind
    /// response.
    #[allow(unused_variables)]
    async fn on_channel_bind(&self, id: Identifier, username: String, channel: u16) {}

    /// create permission request
    ///
    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
    ///
    /// If the request is valid, but the server is unable to fulfill the
    /// request due to some capacity limit or similar, the server replies
    /// with a 508 (Insufficient Capacity) error.
    ///
    /// Otherwise, the server replies with a ChannelBind success response.
    /// There are no required attributes in a successful ChannelBind
    /// response.
    #[allow(unused_variables)]
    async fn on_create_permission(&self, id: Identifier, username: String, ports: Vec<u16>) {}

    /// refresh request
    ///
    /// [rfc8489](https://tools.ietf.org/html/rfc8489)
    ///
    /// If the request is valid, but the server is unable to fulfill the
    /// request due to some capacity limit or similar, the server replies
    /// with a 508 (Insufficient Capacity) error.
    ///
    /// Otherwise, the server replies with a ChannelBind success response.
    /// There are no required attributes in a successful ChannelBind
    /// response.
    #[allow(unused_variables)]
    async fn on_refresh(&self, id: Identifier, username: String, lifetime: u32) {}

    /// session closed
    ///
    /// Triggered when the session leaves from the turn. Possible reasons: the
    /// session life cycle has expired, external active deletion, or active
    /// exit of the session.
    #[allow(unused_variables)]
    async fn on_destroy(&self, id: Identifier, username: String) {}

    /// start the turn hooks server
    ///
    /// This function will start the turn hooks server on the given server and listen address.
    async fn start_with_server(
        self,
        server: &mut Server,
        listen: SocketAddr,
    ) -> Result<(), tonic::transport::Error>
    where
        Self: Sized + 'static,
    {
        server
            .add_service(TurnHooksServiceServer::<TurnHooksServerInner<Self>>::new(
                TurnHooksServerInner(self),
            ))
            .serve(listen)
            .await?;

        Ok(())
    }
}