mirror of
https://github.com/coral/sipcord-bridge.git
synced 2026-04-12 20:42:33 -06:00
ye
This commit is contained in:
parent
4abaef773b
commit
7ff46977f5
26
README.md
26
README.md
|
|
@ -2,19 +2,31 @@
|
||||||
|
|
||||||
This is a slice of the code that powers [SIPcord](https://sipcord.net/) that you can use to self host something similar. It's not the full SIPcord package but rather the core functionality used in SIPcord with ways to build your own backend adapter. SIPcord itself uses this as a component of the full build so the code is the same that runs on the public bridges.
|
This is a slice of the code that powers [SIPcord](https://sipcord.net/) that you can use to self host something similar. It's not the full SIPcord package but rather the core functionality used in SIPcord with ways to build your own backend adapter. SIPcord itself uses this as a component of the full build so the code is the same that runs on the public bridges.
|
||||||
|
|
||||||
## Help!
|
This means you have to build the call routing backend yourself. I am including a `static-router` backend which you can use to map extensions in a TOML file like this
|
||||||
|
```toml
|
||||||
|
[extensions]
|
||||||
|
1000 = { guild = 123456789012345620, channel = 987654321012345620 }
|
||||||
|
2000 = { guild = 123456789012345620, channel = 111222333444555620 }
|
||||||
|
```
|
||||||
|
but if you want more fancy routing you have to build it. You can easily use sipcord-bridge as a library and provide your own routers by implementing the `Backend` trait.
|
||||||
|
|
||||||
I am providing 0 support for this, my goal is to run [sipcord.net](https://sipcord.net/), not support self hosting. If you want to run this self hosted, feel free to use this code but do not ask me for support.
|
This was written a mix between myself and claude, sure, some of it's big slop but the parts I care about are not.
|
||||||
|
|
||||||
## I have a feature request!
|
### Can you help me set this up?
|
||||||
|
|
||||||
|
**No.** I am not providing support for this as my goal is to run [sipcord.net](https://sipcord.net/), not support self hosting. If you want to run this self hosted, feel free to use this code but you are on your own here.
|
||||||
|
|
||||||
|
### I have a feature request!
|
||||||
|
|
||||||
**PR's welcome**. No really, feel free to implement it and contribute.
|
**PR's welcome**. No really, feel free to implement it and contribute.
|
||||||
|
|
||||||
## Acknowledgements
|
### Acknowledgements
|
||||||
|
|
||||||
- Thanks to [dusthillguy](https://www.youtube.com/watch?v=IK1ydvw3xkU) for letting me use the song *"Joona Kouvolalainen buttermilk"* as hold music and distribute it.
|
- Thanks to [dusthillguy](https://www.youtube.com/watch?v=IK1ydvw3xkU) for letting me use the song *"Joona Kouvolalainen buttermilk"* as hold music and distribute it.
|
||||||
- Thanks to chrischrome for hosting bridge-use1
|
- Thanks to [wberg](https://wberg.com/) for hosting `bridge-eu1`
|
||||||
|
- Thanks to [chrischrome](https://litenet.tel/) for hosting `bridge-use1`
|
||||||
|
|
||||||
## License
|
### License
|
||||||
|
|
||||||
GPLv3
|
Code is AGPLv3
|
||||||
|
Dusthillguy track is whatever dusthillguy wishe
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//! cmake build (used in Docker to separate the slow C build into its own layer).
|
//! cmake build (used in Docker to separate the slow C build into its own layer).
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -214,7 +214,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build pjproject from source and return include paths.
|
/// Build pjproject from source and return include paths.
|
||||||
fn build_from_source(out_dir: &PathBuf) -> Vec<PathBuf> {
|
fn build_from_source(out_dir: &Path) -> Vec<PathBuf> {
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let pjproject_src = manifest_dir.join("pjproject");
|
let pjproject_src = manifest_dir.join("pjproject");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
use udptl::AsyncUdptlSocket;
|
use udptl::AsyncUdptlSocket;
|
||||||
|
|
||||||
|
/// Type alias for fax session entries stored in the DashMap.
|
||||||
|
type FaxSessionEntry = (Arc<tokio::sync::Mutex<FaxSession>>, CancellationToken);
|
||||||
|
|
||||||
/// Ring buffer capacity for Discord→SIP audio (i16 mono @ 16kHz).
|
/// Ring buffer capacity for Discord→SIP audio (i16 mono @ 16kHz).
|
||||||
/// 3200 samples = 200ms of audio, enough for timing jitter.
|
/// 3200 samples = 200ms of audio, enough for timing jitter.
|
||||||
const DISCORD_TO_SIP_RING_BUFFER_SIZE: usize = 3200;
|
const DISCORD_TO_SIP_RING_BUFFER_SIZE: usize = 3200;
|
||||||
|
|
@ -107,7 +110,7 @@ struct BridgeContext {
|
||||||
sip_calls: Arc<DashMap<CallId, SipCallInfo>>,
|
sip_calls: Arc<DashMap<CallId, SipCallInfo>>,
|
||||||
/// Active fax sessions keyed by SIP call ID.
|
/// Active fax sessions keyed by SIP call ID.
|
||||||
/// Each entry holds the session and a cancellation token for the T.38 processing task.
|
/// Each entry holds the session and a cancellation token for the T.38 processing task.
|
||||||
fax_sessions: Arc<DashMap<CallId, (Arc<tokio::sync::Mutex<FaxSession>>, CancellationToken)>>,
|
fax_sessions: Arc<DashMap<CallId, FaxSessionEntry>>,
|
||||||
discord_event_tx: Sender<DiscordEvent>,
|
discord_event_tx: Sender<DiscordEvent>,
|
||||||
sip_cmd_tx: Sender<SipCommand>,
|
sip_cmd_tx: Sender<SipCommand>,
|
||||||
sound_manager: Arc<SoundManager>,
|
sound_manager: Arc<SoundManager>,
|
||||||
|
|
@ -127,7 +130,7 @@ pub struct BridgeCoordinator {
|
||||||
sip_calls: Arc<DashMap<CallId, SipCallInfo>>,
|
sip_calls: Arc<DashMap<CallId, SipCallInfo>>,
|
||||||
/// Active fax sessions keyed by SIP call ID.
|
/// Active fax sessions keyed by SIP call ID.
|
||||||
/// Each entry holds the session and a cancellation token for the T.38 processing task.
|
/// Each entry holds the session and a cancellation token for the T.38 processing task.
|
||||||
fax_sessions: Arc<DashMap<CallId, (Arc<tokio::sync::Mutex<FaxSession>>, CancellationToken)>>,
|
fax_sessions: Arc<DashMap<CallId, FaxSessionEntry>>,
|
||||||
/// Stores outbound call requests by tracking_id so the answered handler can retrieve them.
|
/// Stores outbound call requests by tracking_id so the answered handler can retrieve them.
|
||||||
/// Entries are cleaned on answer/fail and periodically swept for stale entries.
|
/// Entries are cleaned on answer/fail and periodically swept for stale entries.
|
||||||
outbound_requests: Arc<DashMap<String, OutboundCallRequest>>,
|
outbound_requests: Arc<DashMap<String, OutboundCallRequest>>,
|
||||||
|
|
@ -221,8 +224,9 @@ impl BridgeCoordinator {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for config-based extension sounds (easter eggs)
|
// Check for config-based extension sounds (easter eggs)
|
||||||
if let Ok(ext_num) = extension.parse::<u32>() {
|
if let Ok(ext_num) = extension.parse::<u32>()
|
||||||
if let Some(sound_name) = sound_manager.get_extension_sound(ext_num) {
|
&& let Some(sound_name) = sound_manager.get_extension_sound(ext_num)
|
||||||
|
{
|
||||||
info!(
|
info!(
|
||||||
"Extension {} maps to sound '{}' (call {})",
|
"Extension {} maps to sound '{}' (call {})",
|
||||||
ext_num, sound_name, call_id
|
ext_num, sound_name, call_id
|
||||||
|
|
@ -243,7 +247,6 @@ impl BridgeCoordinator {
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Track this call
|
// Track this call
|
||||||
sip_calls.insert(
|
sip_calls.insert(
|
||||||
|
|
@ -325,8 +328,9 @@ impl BridgeCoordinator {
|
||||||
backend.on_call_ended(&sip_call_id_str).await;
|
backend.on_call_ended(&sip_call_id_str).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((_, call_info)) = sip_calls.remove(&call_id) {
|
if let Some((_, call_info)) = sip_calls.remove(&call_id)
|
||||||
if let Some(channel_id) = call_info.channel_id {
|
&& let Some(channel_id) = call_info.channel_id
|
||||||
|
{
|
||||||
let should_destroy = {
|
let should_destroy = {
|
||||||
if let Some(mut bridge) = bridges.get_mut(&channel_id) {
|
if let Some(mut bridge) = bridges.get_mut(&channel_id) {
|
||||||
bridge.sip_calls.remove(&call_id);
|
bridge.sip_calls.remove(&call_id);
|
||||||
|
|
@ -356,7 +360,6 @@ impl BridgeCoordinator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SipEvent::CallTimeout { call_id, rx_count } => {
|
SipEvent::CallTimeout { call_id, rx_count } => {
|
||||||
warn!(
|
warn!(
|
||||||
|
|
@ -366,17 +369,16 @@ impl BridgeCoordinator {
|
||||||
|
|
||||||
// If no audio was ever received, report no_audio to the coordinator
|
// If no audio was ever received, report no_audio to the coordinator
|
||||||
// so the Discord embed can show a diagnostic message
|
// so the Discord embed can show a diagnostic message
|
||||||
if rx_count == 0 {
|
if rx_count == 0
|
||||||
if let Some(call_info) = sip_calls.get(&call_id) {
|
&& let Some(call_info) = sip_calls.get(&call_id)
|
||||||
if let Some(ref tracking_id) = call_info.tracking_id {
|
&& let Some(ref tracking_id) = call_info.tracking_id
|
||||||
|
{
|
||||||
info!(
|
info!(
|
||||||
"Call {} had zero RTP packets, reporting no_audio (tracking_id={})",
|
"Call {} had zero RTP packets, reporting no_audio (tracking_id={})",
|
||||||
call_id, tracking_id
|
call_id, tracking_id
|
||||||
);
|
);
|
||||||
backend_for_sip.report_call_status(tracking_id, "no_audio");
|
backend_for_sip.report_call_status(tracking_id, "no_audio");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = sip_cmd_tx.send(SipCommand::Hangup { call_id });
|
let _ = sip_cmd_tx.send(SipCommand::Hangup { call_id });
|
||||||
}
|
}
|
||||||
|
|
@ -1621,8 +1623,9 @@ async fn play_extension_sound_and_hangup(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a streaming sound (large file)
|
// Check if this is a streaming sound (large file)
|
||||||
if sound_manager.is_streaming(sound_name) {
|
if sound_manager.is_streaming(sound_name)
|
||||||
if let Some(config) = sound_manager.get_streaming(sound_name) {
|
&& let Some(config) = sound_manager.get_streaming(sound_name)
|
||||||
|
{
|
||||||
info!(
|
info!(
|
||||||
"Starting streaming playback '{}' from {} for call {}",
|
"Starting streaming playback '{}' from {} for call {}",
|
||||||
sound_name,
|
sound_name,
|
||||||
|
|
@ -1641,7 +1644,6 @@ async fn play_extension_sound_and_hangup(
|
||||||
// or when the call ends (detected via CALL_CONF_PORTS check)
|
// or when the call ends (detected via CALL_CONF_PORTS check)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Preloaded sound - play all at once
|
// Preloaded sound - play all at once
|
||||||
if let Some(sound) = sound_manager.get_preloaded(sound_name) {
|
if let Some(sound) = sound_manager.get_preloaded(sound_name) {
|
||||||
|
|
@ -1805,18 +1807,17 @@ async fn process_fax_audio(
|
||||||
}
|
}
|
||||||
let tx_available = tx_producer.slots();
|
let tx_available = tx_producer.slots();
|
||||||
let to_write = tx_generated.min(tx_available);
|
let to_write = tx_generated.min(tx_available);
|
||||||
if to_write > 0 {
|
if to_write > 0
|
||||||
if let Ok(mut chunk) = tx_producer.write_chunk(to_write) {
|
&& let Ok(mut chunk) = tx_producer.write_chunk(to_write)
|
||||||
|
{
|
||||||
let (first, second) = chunk.as_mut_slices();
|
let (first, second) = chunk.as_mut_slices();
|
||||||
let first_len = first.len().min(to_write);
|
let first_len = first.len().min(to_write);
|
||||||
first[..first_len].copy_from_slice(&tx_buf[..first_len]);
|
first[..first_len].copy_from_slice(&tx_buf[..first_len]);
|
||||||
if first_len < to_write {
|
if first_len < to_write {
|
||||||
second[..to_write - first_len]
|
second[..to_write - first_len].copy_from_slice(&tx_buf[first_len..to_write]);
|
||||||
.copy_from_slice(&tx_buf[first_len..to_write]);
|
|
||||||
}
|
}
|
||||||
chunk.commit_all();
|
chunk.commit_all();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tx_silent_frames += 1;
|
tx_silent_frames += 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,44 +256,50 @@ unsafe extern "C" fn fax_port_get_frame(
|
||||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let call_id_ldata = (*this_port).port_data.ldata;
|
let call_id_ldata = unsafe { (*this_port).port_data.ldata };
|
||||||
|
|
||||||
if let Some(consumer_entry) = get_fax_tx_consumers().get(&call_id_ldata) {
|
if let Some(consumer_entry) = get_fax_tx_consumers().get(&call_id_ldata)
|
||||||
if let Some(mut consumer) = consumer_entry.try_lock() {
|
&& let Some(mut consumer) = consumer_entry.try_lock()
|
||||||
|
{
|
||||||
let available = consumer.slots();
|
let available = consumer.slots();
|
||||||
if available >= SAMPLES_PER_FRAME {
|
if available >= SAMPLES_PER_FRAME
|
||||||
if let Ok(chunk) = consumer.read_chunk(SAMPLES_PER_FRAME) {
|
&& let Ok(chunk) = consumer.read_chunk(SAMPLES_PER_FRAME)
|
||||||
|
{
|
||||||
let (first, second) = chunk.as_slices();
|
let (first, second) = chunk.as_slices();
|
||||||
|
let out = unsafe {
|
||||||
let buf = (*frame).buf as *mut i16;
|
let buf = (*frame).buf as *mut i16;
|
||||||
let out = std::slice::from_raw_parts_mut(buf, SAMPLES_PER_FRAME);
|
std::slice::from_raw_parts_mut(buf, SAMPLES_PER_FRAME)
|
||||||
|
};
|
||||||
out[..first.len()].copy_from_slice(first);
|
out[..first.len()].copy_from_slice(first);
|
||||||
if !second.is_empty() {
|
if !second.is_empty() {
|
||||||
out[first.len()..first.len() + second.len()].copy_from_slice(second);
|
out[first.len()..first.len() + second.len()].copy_from_slice(second);
|
||||||
}
|
}
|
||||||
chunk.commit_all();
|
chunk.commit_all();
|
||||||
|
unsafe {
|
||||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||||
(*frame).size = SAMPLES_PER_FRAME * 2;
|
(*frame).size = SAMPLES_PER_FRAME * 2;
|
||||||
|
}
|
||||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No TX audio available — return silence audio frame (not NONE).
|
// No TX audio available — return silence audio frame (not NONE).
|
||||||
// Returning FRAME_TYPE_NONE can cause PJSIP's conference bridge to
|
// Returning FRAME_TYPE_NONE can cause PJSIP's conference bridge to
|
||||||
// exclude this port from the audio mix, breaking the TX path.
|
// exclude this port from the audio mix, breaking the TX path.
|
||||||
|
unsafe {
|
||||||
let buf = (*frame).buf as *mut i16;
|
let buf = (*frame).buf as *mut i16;
|
||||||
let out = std::slice::from_raw_parts_mut(buf, SAMPLES_PER_FRAME);
|
let out = std::slice::from_raw_parts_mut(buf, SAMPLES_PER_FRAME);
|
||||||
out.fill(0);
|
out.fill(0);
|
||||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||||
(*frame).size = SAMPLES_PER_FRAME * 2;
|
(*frame).size = SAMPLES_PER_FRAME * 2;
|
||||||
|
}
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
}
|
}
|
||||||
|
|
||||||
/// on_destroy callback — no-op since cleanup is done in remove_fax_audio_port().
|
/// on_destroy callback — no-op since cleanup is done in remove_fax_audio_port().
|
||||||
/// Required by PJSIP to avoid "on_destroy() not found" warning.
|
/// Required by PJSIP to avoid "on_destroy() not found" warning.
|
||||||
unsafe extern "C" fn fax_port_on_destroy(_this_port: *mut pjmedia_port) -> pj_status_t {
|
unsafe extern "C" fn fax_port_on_destroy(_this_port: *mut pjmedia_port) -> pj_status_t {
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t // no unsafe ops needed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// put_frame callback — captures SIP audio and pushes to RX ring buffer for SpanDSP.
|
/// put_frame callback — captures SIP audio and pushes to RX ring buffer for SpanDSP.
|
||||||
|
|
@ -306,20 +312,25 @@ unsafe extern "C" fn fax_port_put_frame(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process audio frames with data
|
// Only process audio frames with data
|
||||||
if (*frame).type_ != pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO || (*frame).size == 0 {
|
if unsafe {
|
||||||
|
(*frame).type_ != pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO || (*frame).size == 0
|
||||||
|
} {
|
||||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let call_id_ldata = (*this_port).port_data.ldata;
|
let call_id_ldata = unsafe { (*this_port).port_data.ldata };
|
||||||
|
|
||||||
// View frame buffer as i16 slice
|
// View frame buffer as i16 slice
|
||||||
|
let samples = unsafe {
|
||||||
let num_samples = (*frame).size / 2;
|
let num_samples = (*frame).size / 2;
|
||||||
let frame_buf = (*frame).buf as *const i16;
|
let frame_buf = (*frame).buf as *const i16;
|
||||||
let samples = std::slice::from_raw_parts(frame_buf, num_samples);
|
std::slice::from_raw_parts(frame_buf, num_samples)
|
||||||
|
};
|
||||||
|
|
||||||
// Push to RX ring buffer
|
// Push to RX ring buffer
|
||||||
if let Some(producer_entry) = get_fax_rx_producers().get(&call_id_ldata) {
|
if let Some(producer_entry) = get_fax_rx_producers().get(&call_id_ldata)
|
||||||
if let Some(mut producer) = producer_entry.try_lock() {
|
&& let Some(mut producer) = producer_entry.try_lock()
|
||||||
|
{
|
||||||
let available = producer.slots();
|
let available = producer.slots();
|
||||||
if available >= samples.len() {
|
if available >= samples.len() {
|
||||||
if let Ok(mut chunk) = producer.write_chunk(samples.len()) {
|
if let Ok(mut chunk) = producer.write_chunk(samples.len()) {
|
||||||
|
|
@ -338,7 +349,6 @@ unsafe extern "C" fn fax_port_put_frame(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,12 +115,14 @@ unsafe fn configure_log_state(log_state: *mut spandsp_sys::logging_state_t) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let log_level = LogLevel::Flow as i32 | LogShowFlags::TAG.bits();
|
let log_level = LogLevel::Flow as i32 | LogShowFlags::TAG.bits();
|
||||||
|
unsafe {
|
||||||
spandsp_sys::span_log_set_level(log_state, log_level);
|
spandsp_sys::span_log_set_level(log_state, log_level);
|
||||||
spandsp_sys::span_log_set_message_handler(
|
spandsp_sys::span_log_set_message_handler(
|
||||||
log_state,
|
log_state,
|
||||||
Some(spandsp_log_handler),
|
Some(spandsp_log_handler),
|
||||||
std::ptr::null_mut(),
|
std::ptr::null_mut(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check fax reception completion status from callback state.
|
/// Check fax reception completion status from callback state.
|
||||||
|
|
@ -467,8 +469,11 @@ unsafe extern "C" fn tx_packet_handler(
|
||||||
if user_data.is_null() || buf.is_null() || len <= 0 {
|
if user_data.is_null() || buf.is_null() || len <= 0 {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
let (state, data) = unsafe {
|
||||||
let state = &*(user_data as *const TxCallbackState);
|
let state = &*(user_data as *const TxCallbackState);
|
||||||
let data = std::slice::from_raw_parts(buf, len as usize);
|
let data = std::slice::from_raw_parts(buf, len as usize);
|
||||||
|
(state, data)
|
||||||
|
};
|
||||||
debug!("SpanDSP TX IFP: {}B (count={})", len, count);
|
debug!("SpanDSP TX IFP: {}B (count={})", len, count);
|
||||||
// Send the packet `count` times as SpanDSP requests.
|
// Send the packet `count` times as SpanDSP requests.
|
||||||
// For indicator packets (CNG, CED, DIS), count is typically 3 — these
|
// For indicator packets (CNG, CED, DIS), count is typically 3 — these
|
||||||
|
|
@ -491,7 +496,7 @@ unsafe extern "C" fn tx_packet_handler(
|
||||||
/// Phase B handler: called when T.30 negotiation starts.
|
/// Phase B handler: called when T.30 negotiation starts.
|
||||||
unsafe extern "C" fn phase_b_handler(user_data: *mut std::ffi::c_void, result: i32) -> i32 {
|
unsafe extern "C" fn phase_b_handler(user_data: *mut std::ffi::c_void, result: i32) -> i32 {
|
||||||
if !user_data.is_null() {
|
if !user_data.is_null() {
|
||||||
let state = &mut *(user_data as *mut FaxCallbackState);
|
let state = unsafe { &mut *(user_data as *mut FaxCallbackState) };
|
||||||
state.negotiation_started = true;
|
state.negotiation_started = true;
|
||||||
info!(
|
info!(
|
||||||
"SpanDSP phase B: fax negotiation started (result={})",
|
"SpanDSP phase B: fax negotiation started (result={})",
|
||||||
|
|
@ -504,7 +509,7 @@ unsafe extern "C" fn phase_b_handler(user_data: *mut std::ffi::c_void, result: i
|
||||||
/// Phase D handler: called when a page is received.
|
/// Phase D handler: called when a page is received.
|
||||||
unsafe extern "C" fn phase_d_handler(user_data: *mut std::ffi::c_void, result: i32) -> i32 {
|
unsafe extern "C" fn phase_d_handler(user_data: *mut std::ffi::c_void, result: i32) -> i32 {
|
||||||
if !user_data.is_null() {
|
if !user_data.is_null() {
|
||||||
let state = &mut *(user_data as *mut FaxCallbackState);
|
let state = unsafe { &mut *(user_data as *mut FaxCallbackState) };
|
||||||
state.pages_received += 1;
|
state.pages_received += 1;
|
||||||
info!(
|
info!(
|
||||||
"SpanDSP phase D: page {} received (result={})",
|
"SpanDSP phase D: page {} received (result={})",
|
||||||
|
|
@ -517,7 +522,7 @@ unsafe extern "C" fn phase_d_handler(user_data: *mut std::ffi::c_void, result: i
|
||||||
/// Phase E handler: called when fax reception completes (success or failure).
|
/// Phase E handler: called when fax reception completes (success or failure).
|
||||||
unsafe extern "C" fn phase_e_handler(user_data: *mut std::ffi::c_void, completion_code: i32) {
|
unsafe extern "C" fn phase_e_handler(user_data: *mut std::ffi::c_void, completion_code: i32) {
|
||||||
if !user_data.is_null() {
|
if !user_data.is_null() {
|
||||||
let state = &mut *(user_data as *mut FaxCallbackState);
|
let state = unsafe { &mut *(user_data as *mut FaxCallbackState) };
|
||||||
state.completion_code = completion_code;
|
state.completion_code = completion_code;
|
||||||
state.completed = true;
|
state.completed = true;
|
||||||
|
|
||||||
|
|
@ -550,7 +555,7 @@ unsafe extern "C" fn spandsp_log_handler(
|
||||||
if text.is_null() {
|
if text.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let msg = std::ffi::CStr::from_ptr(text).to_string_lossy();
|
let msg = unsafe { std::ffi::CStr::from_ptr(text) }.to_string_lossy();
|
||||||
let msg = msg.trim_end(); // SpanDSP messages often have trailing newlines
|
let msg = msg.trim_end(); // SpanDSP messages often have trailing newlines
|
||||||
|
|
||||||
match level {
|
match level {
|
||||||
|
|
|
||||||
|
|
@ -941,12 +941,12 @@ fn decode_group4(data: &[u8], width: u32, height: u32) -> Result<Vec<Vec<u16>>>
|
||||||
|
|
||||||
for _ in 0..height {
|
for _ in 0..height {
|
||||||
// Check for EOFB (End Of Facsimile Block): two consecutive EOL codes
|
// Check for EOFB (End Of Facsimile Block): two consecutive EOL codes
|
||||||
if let Some(v) = reader.peek(12) {
|
if let Some(v) = reader.peek(12)
|
||||||
if v == 0x001 {
|
&& v == 0x001
|
||||||
|
{
|
||||||
// Possible EOFB — check for second EOL
|
// Possible EOFB — check for second EOL
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let line = match decode_line_2d(&mut reader, &reference, w) {
|
let line = match decode_line_2d(&mut reader, &reference, w) {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,12 @@ pub struct Registrar {
|
||||||
discord_to_sip: DashMap<String, String>,
|
discord_to_sip: DashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Registrar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Registrar {
|
impl Registrar {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -68,11 +74,11 @@ impl Registrar {
|
||||||
.find(|r| r.source_addr == reg.source_addr && r.contact_uri == reg.contact_uri)
|
.find(|r| r.source_addr == reg.source_addr && r.contact_uri == reg.contact_uri)
|
||||||
{
|
{
|
||||||
// If discord_username changed, remove the old reverse mapping
|
// If discord_username changed, remove the old reverse mapping
|
||||||
if existing.discord_username != reg.discord_username {
|
if existing.discord_username != reg.discord_username
|
||||||
if let Some(ref old_du) = existing.discord_username {
|
&& let Some(ref old_du) = existing.discord_username
|
||||||
|
{
|
||||||
self.discord_to_sip.remove(old_du);
|
self.discord_to_sip.remove(old_du);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
existing.expires_at = reg.expires_at;
|
existing.expires_at = reg.expires_at;
|
||||||
existing.registered_at = reg.registered_at;
|
existing.registered_at = reg.registered_at;
|
||||||
|
|
|
||||||
|
|
@ -1112,9 +1112,10 @@ impl VoiceEventHandler for VoiceReceiver {
|
||||||
// CRITICAL: Skip our own SSRC to prevent feedback loop
|
// CRITICAL: Skip our own SSRC to prevent feedback loop
|
||||||
// When we send audio to Discord, it comes back in VoiceTick.
|
// When we send audio to Discord, it comes back in VoiceTick.
|
||||||
// If we don't filter it out, we get: SIP -> Discord -> SIP -> Discord -> ...
|
// If we don't filter it out, we get: SIP -> Discord -> SIP -> Discord -> ...
|
||||||
if let Some(ref map) = ssrc_map {
|
if let Some(ref map) = ssrc_map
|
||||||
if let Some(&user_id) = map.get(ssrc) {
|
&& let Some(&user_id) = map.get(ssrc)
|
||||||
if user_id == self.bot_user_id {
|
&& user_id == self.bot_user_id
|
||||||
|
{
|
||||||
skipped_self = true;
|
skipped_self = true;
|
||||||
if should_log {
|
if should_log {
|
||||||
trace!(
|
trace!(
|
||||||
|
|
@ -1124,8 +1125,6 @@ impl VoiceEventHandler for VoiceReceiver {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref decoded) = voice_data.decoded_voice {
|
if let Some(ref decoded) = voice_data.decoded_voice {
|
||||||
if decoded.is_empty() {
|
if decoded.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -230,8 +230,9 @@ pub fn stop_audio_thread() {
|
||||||
// If the thread is blocked on a conference bridge lock, we don't want
|
// If the thread is blocked on a conference bridge lock, we don't want
|
||||||
// shutdown to hang indefinitely. The 2s force-exit timer in main.rs
|
// shutdown to hang indefinitely. The 2s force-exit timer in main.rs
|
||||||
// is a final backstop, but this avoids relying on a hard process exit.
|
// is a final backstop, but this avoids relying on a hard process exit.
|
||||||
if let Some(handle_storage) = AUDIO_THREAD_HANDLE.get() {
|
if let Some(handle_storage) = AUDIO_THREAD_HANDLE.get()
|
||||||
if let Some(handle) = handle_storage.lock().take() {
|
&& let Some(handle) = handle_storage.lock().take()
|
||||||
|
{
|
||||||
tracing::debug!("Joining audio thread (2s timeout)...");
|
tracing::debug!("Joining audio thread (2s timeout)...");
|
||||||
let (done_tx, done_rx) = std::sync::mpsc::channel();
|
let (done_tx, done_rx) = std::sync::mpsc::channel();
|
||||||
let join_thread = std::thread::spawn(move || {
|
let join_thread = std::thread::spawn(move || {
|
||||||
|
|
@ -253,7 +254,6 @@ pub fn stop_audio_thread() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process any pending channel registration completions
|
/// Process any pending channel registration completions
|
||||||
|
|
@ -331,8 +331,9 @@ fn process_pending_pjsua_ops() {
|
||||||
PendingPjsuaOp::Hangup { call_id } => Some(*call_id),
|
PendingPjsuaOp::Hangup { call_id } => Some(*call_id),
|
||||||
PendingPjsuaOp::ConnectFaxPort { call_id, .. } => Some(*call_id),
|
PendingPjsuaOp::ConnectFaxPort { call_id, .. } => Some(*call_id),
|
||||||
};
|
};
|
||||||
if let Some(cid) = call_id {
|
if let Some(cid) = call_id
|
||||||
if !is_call_valid(cid) {
|
&& !is_call_valid(cid)
|
||||||
|
{
|
||||||
tracing::warn!("Skipping stale op for dead call {}: {:?}", cid, op);
|
tracing::warn!("Skipping stale op for dead call {}: {:?}", cid, op);
|
||||||
// For ConnectFaxPort, signal failure so the caller doesn't hang
|
// For ConnectFaxPort, signal failure so the caller doesn't hang
|
||||||
if let PendingPjsuaOp::ConnectFaxPort { done_tx, .. } = op {
|
if let PendingPjsuaOp::ConnectFaxPort { done_tx, .. } = op {
|
||||||
|
|
@ -340,7 +341,6 @@ fn process_pending_pjsua_ops() {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
count += 1;
|
count += 1;
|
||||||
match op {
|
match op {
|
||||||
PendingPjsuaOp::PlayDirect { call_id, samples } => {
|
PendingPjsuaOp::PlayDirect { call_id, samples } => {
|
||||||
|
|
@ -559,7 +559,7 @@ unsafe fn process_audio_frame(
|
||||||
timestamp: pj_timestamp { u64_: *timestamp },
|
timestamp: pj_timestamp { u64_: *timestamp },
|
||||||
bit_info: 0,
|
bit_info: 0,
|
||||||
};
|
};
|
||||||
pjmedia_port_get_frame(master_port, &mut clock_frame);
|
unsafe { pjmedia_port_get_frame(master_port, &mut clock_frame) };
|
||||||
|
|
||||||
// Now drain the SIP->Discord buffers that were filled by channel_port_put_frame callbacks
|
// Now drain the SIP->Discord buffers that were filled by channel_port_put_frame callbacks
|
||||||
// during the conference tick above.
|
// during the conference tick above.
|
||||||
|
|
@ -718,14 +718,14 @@ pub fn check_rtp_inactivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sender_lock) = TIMEOUT_EVENT_TX.get() {
|
if let Some(sender_lock) = TIMEOUT_EVENT_TX.get()
|
||||||
if let Some(ref tx) = *sender_lock.lock() {
|
&& let Some(ref tx) = *sender_lock.lock()
|
||||||
|
{
|
||||||
for (call_id, rx_count) in timed_out_calls {
|
for (call_id, rx_count) in timed_out_calls {
|
||||||
let _ = tx.send(super::SipEvent::CallTimeout { call_id, rx_count });
|
let _ = tx.send(super::SipEvent::CallTimeout { call_id, rx_count });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate all entries in COUNTED_CALL_IDS are still valid PJSUA calls
|
/// Validate all entries in COUNTED_CALL_IDS are still valid PJSUA calls
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ pub unsafe fn extract_source_ip(rdata: *const pjsip_rx_data) -> Option<IpAddr> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
// pjsip stores source info in pkt_info.src_name as a C string (null-terminated char array)
|
// pjsip stores source info in pkt_info.src_name as a C string (null-terminated char array)
|
||||||
let src_name = &(*rdata).pkt_info.src_name;
|
let src_name = &(*rdata).pkt_info.src_name;
|
||||||
|
|
||||||
|
|
@ -95,6 +96,7 @@ pub unsafe fn extract_source_ip(rdata: *const pjsip_rx_data) -> Option<IpAddr> {
|
||||||
// pjsip's src_name contains only the IP address (port is in src_port),
|
// pjsip's src_name contains only the IP address (port is in src_port),
|
||||||
// so parse directly as IpAddr. This handles both IPv4 and IPv6.
|
// so parse directly as IpAddr. This handles both IPv4 and IPv6.
|
||||||
ip_str.parse().ok()
|
ip_str.parse().ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract User-Agent header from pjsip_rx_data
|
/// Extract User-Agent header from pjsip_rx_data
|
||||||
|
|
@ -103,6 +105,7 @@ pub unsafe fn extract_user_agent(rdata: *const pjsip_rx_data) -> Option<String>
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
let msg = (*rdata).msg_info.msg;
|
let msg = (*rdata).msg_info.msg;
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -126,6 +129,7 @@ pub unsafe fn extract_user_agent(rdata: *const pjsip_rx_data) -> Option<String>
|
||||||
// Extract the header value
|
// Extract the header value
|
||||||
let value = pj_str_to_string(&(*str_hdr).hvalue);
|
let value = pj_str_to_string(&(*str_hdr).hvalue);
|
||||||
if value.is_empty() { None } else { Some(value) }
|
if value.is_empty() { None } else { Some(value) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if User-Agent indicates a SIPVicious scanner or similar tool
|
/// Check if User-Agent indicates a SIPVicious scanner or similar tool
|
||||||
|
|
@ -144,6 +148,7 @@ pub unsafe fn extract_digest_auth_from_rdata(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
let rdata = &*rdata;
|
let rdata = &*rdata;
|
||||||
let msg = rdata.msg_info.msg;
|
let msg = rdata.msg_info.msg;
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
|
|
@ -225,10 +230,12 @@ pub unsafe fn extract_digest_auth_from_rdata(
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(params)
|
Some(params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send 401 Unauthorized response with WWW-Authenticate header
|
/// Send 401 Unauthorized response with WWW-Authenticate header
|
||||||
pub unsafe fn send_401_challenge(call_id: CallId, www_auth: &str) {
|
pub unsafe fn send_401_challenge(call_id: CallId, www_auth: &str) {
|
||||||
|
unsafe {
|
||||||
// Create the WWW-Authenticate header
|
// Create the WWW-Authenticate header
|
||||||
let hdr_name = CString::new("WWW-Authenticate").unwrap();
|
let hdr_name = CString::new("WWW-Authenticate").unwrap();
|
||||||
let hdr_value = CString::new(www_auth).unwrap();
|
let hdr_value = CString::new(www_auth).unwrap();
|
||||||
|
|
@ -281,11 +288,13 @@ pub unsafe fn send_401_challenge(call_id: CallId, www_auth: &str) {
|
||||||
// pjsua is destroyed. This leaks ~512 bytes per 401 challenge but
|
// pjsua is destroyed. This leaks ~512 bytes per 401 challenge but
|
||||||
// prevents use-after-free crashes.
|
// prevents use-after-free crashes.
|
||||||
// TODO: Track pools per-call and release them in on_call_state when call ends
|
// TODO: Track pools per-call and release them in on_call_state when call ends
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send 302 Moved Temporarily response with Contact header pointing to another bridge
|
/// Send 302 Moved Temporarily response with Contact header pointing to another bridge
|
||||||
/// Used for multi-region channel conflict resolution - redirects caller to the active region
|
/// Used for multi-region channel conflict resolution - redirects caller to the active region
|
||||||
pub unsafe fn send_302_redirect(call_id: CallId, target_domain: &str, extension: &str) {
|
pub unsafe fn send_302_redirect(call_id: CallId, target_domain: &str, extension: &str) {
|
||||||
|
unsafe {
|
||||||
// CRITICAL: Check if call is still valid and in a state that can receive responses
|
// CRITICAL: Check if call is still valid and in a state that can receive responses
|
||||||
// Race condition: caller may hang up during async API auth, causing the call to be
|
// Race condition: caller may hang up during async API auth, causing the call to be
|
||||||
// DISCONNECTED before we get here. Calling pjsua_call_answer on a disconnected call
|
// DISCONNECTED before we get here. Calling pjsua_call_answer on a disconnected call
|
||||||
|
|
@ -373,6 +382,7 @@ pub unsafe fn send_302_redirect(call_id: CallId, target_domain: &str, extension:
|
||||||
|
|
||||||
// DO NOT release the pool here - PJSUA may still need the header data
|
// DO NOT release the pool here - PJSUA may still need the header data
|
||||||
// after pjsua_call_answer returns. Same issue as send_401_challenge.
|
// after pjsua_call_answer returns. Same issue as send_401_challenge.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PJSUA C callbacks
|
// PJSUA C callbacks
|
||||||
|
|
@ -382,6 +392,7 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
raw_call_id: pjsua_call_id,
|
raw_call_id: pjsua_call_id,
|
||||||
rdata: *mut pjsip_rx_data,
|
rdata: *mut pjsip_rx_data,
|
||||||
) {
|
) {
|
||||||
|
unsafe {
|
||||||
let call_id = CallId::new(raw_call_id);
|
let call_id = CallId::new(raw_call_id);
|
||||||
let mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
let mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
||||||
if pjsua_call_get_info(*call_id, ci.as_mut_ptr()) != pj_constants__PJ_SUCCESS as i32 {
|
if pjsua_call_get_info(*call_id, ci.as_mut_ptr()) != pj_constants__PJ_SUCCESS as i32 {
|
||||||
|
|
@ -403,9 +414,11 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
let source_ip = extract_source_ip(rdata);
|
let source_ip = extract_source_ip(rdata);
|
||||||
|
|
||||||
// Check if IP is banned or timed out - silently drop
|
// Check if IP is banned or timed out - silently drop
|
||||||
if let Some(ip) = source_ip {
|
if let Some(ip) = source_ip
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
&& let Some(ban_mgr) = crate::services::ban::global()
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
&& ban_mgr.is_enabled()
|
||||||
|
&& !ban_mgr.is_whitelisted(&ip)
|
||||||
|
{
|
||||||
let result = ban_mgr.check_banned(&ip);
|
let result = ban_mgr.check_banned(&ip);
|
||||||
if result.is_banned {
|
if result.is_banned {
|
||||||
if result.should_log {
|
if result.should_log {
|
||||||
|
|
@ -426,15 +439,16 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check User-Agent for SIPVicious scanners - instant permaban
|
// Check User-Agent for SIPVicious scanners - instant permaban
|
||||||
if let Some(user_agent) = extract_user_agent(rdata) {
|
if let Some(user_agent) = extract_user_agent(rdata)
|
||||||
if is_sipvicious_scanner(&user_agent) {
|
&& is_sipvicious_scanner(&user_agent)
|
||||||
|
{
|
||||||
if let Some(ip) = source_ip {
|
if let Some(ip) = source_ip {
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
if let Some(ban_mgr) = crate::services::ban::global()
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
&& ban_mgr.is_enabled()
|
||||||
|
&& !ban_mgr.is_whitelisted(&ip)
|
||||||
|
{
|
||||||
let result = ban_mgr.record_permanent_ban(ip, "sipvicious_scanner");
|
let result = ban_mgr.record_permanent_ban(ip, "sipvicious_scanner");
|
||||||
if result.should_log {
|
if result.should_log {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
@ -445,7 +459,6 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"SIPVicious scanner detected but no IP available: User-Agent='{}' (call {})",
|
"SIPVicious scanner detected but no IP available: User-Agent='{}' (call {})",
|
||||||
|
|
@ -456,7 +469,6 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
pjsua_call_hangup(*call_id, 403, ptr::null(), ptr::null());
|
pjsua_call_hangup(*call_id, 403, ptr::null(), ptr::null());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Extension-length ban checks use config values
|
// Extension-length ban checks use config values
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||||
|
|
@ -560,12 +572,12 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
|
|
||||||
// Trigger callbacks with Digest auth params
|
// Trigger callbacks with Digest auth params
|
||||||
// The bridge coordinator handles the call flow from here
|
// The bridge coordinator handles the call flow from here
|
||||||
if let Some(callbacks) = CALLBACKS.get() {
|
if let Some(callbacks) = CALLBACKS.get()
|
||||||
if let Some(ref handlers) = *callbacks.lock() {
|
&& let Some(ref handlers) = *callbacks.lock()
|
||||||
|
{
|
||||||
(handlers.on_incoming_call)(call_id, sip_username, extension.clone(), source_ip);
|
(handlers.on_incoming_call)(call_id, sip_username, extension.clone(), source_ip);
|
||||||
(handlers.on_call_authenticated)(call_id, params, extension, source_ip);
|
(handlers.on_call_authenticated)(call_id, params, extension, source_ip);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// No Authorization header - send 401 challenge
|
// No Authorization header - send 401 challenge
|
||||||
tracing::info!("No auth header, sending 401 challenge for call {}", call_id);
|
tracing::info!("No auth header, sending 401 challenge for call {}", call_id);
|
||||||
|
|
@ -589,6 +601,7 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
// Send 401 Unauthorized with WWW-Authenticate header
|
// Send 401 Unauthorized with WWW-Authenticate header
|
||||||
send_401_challenge(call_id, &www_auth);
|
send_401_challenge(call_id, &www_auth);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "C" fn on_dtmf_digit_cb(raw_call_id: pjsua_call_id, digit: c_int) {
|
pub unsafe extern "C" fn on_dtmf_digit_cb(raw_call_id: pjsua_call_id, digit: c_int) {
|
||||||
|
|
@ -596,14 +609,15 @@ pub unsafe extern "C" fn on_dtmf_digit_cb(raw_call_id: pjsua_call_id, digit: c_i
|
||||||
let digit_char = char::from_u32(digit as u32).unwrap_or('?');
|
let digit_char = char::from_u32(digit as u32).unwrap_or('?');
|
||||||
|
|
||||||
// Forward DTMF to callback handler (buffering done in mod.rs)
|
// Forward DTMF to callback handler (buffering done in mod.rs)
|
||||||
if let Some(callbacks) = CALLBACKS.get() {
|
if let Some(callbacks) = CALLBACKS.get()
|
||||||
if let Some(ref handlers) = *callbacks.lock() {
|
&& let Some(ref handlers) = *callbacks.lock()
|
||||||
|
{
|
||||||
(handlers.on_dtmf)(call_id, digit_char);
|
(handlers.on_dtmf)(call_id, digit_char);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "C" fn on_call_state_cb(raw_call_id: pjsua_call_id, _e: *mut pjsip_event) {
|
pub unsafe extern "C" fn on_call_state_cb(raw_call_id: pjsua_call_id, _e: *mut pjsip_event) {
|
||||||
|
unsafe {
|
||||||
let call_id = CallId::new(raw_call_id);
|
let call_id = CallId::new(raw_call_id);
|
||||||
let mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
let mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
||||||
if pjsua_call_get_info(*call_id, ci.as_mut_ptr()) != pj_constants__PJ_SUCCESS as i32 {
|
if pjsua_call_get_info(*call_id, ci.as_mut_ptr()) != pj_constants__PJ_SUCCESS as i32 {
|
||||||
|
|
@ -697,11 +711,11 @@ pub unsafe extern "C" fn on_call_state_cb(raw_call_id: pjsua_call_id, _e: *mut p
|
||||||
if was_authenticated {
|
if was_authenticated {
|
||||||
tracing::info!("Call {} ended (active_media_calls={})", call_id, new_count);
|
tracing::info!("Call {} ended (active_media_calls={})", call_id, new_count);
|
||||||
|
|
||||||
if let Some(callbacks) = CALLBACKS.get() {
|
if let Some(callbacks) = CALLBACKS.get()
|
||||||
if let Some(ref handlers) = *callbacks.lock() {
|
&& let Some(ref handlers) = *callbacks.lock()
|
||||||
|
{
|
||||||
(handlers.on_call_ended)(call_id);
|
(handlers.on_call_ended)(call_id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if new_count == 0 {
|
if new_count == 0 {
|
||||||
tracing::debug!("Last call ended, stopping audio thread");
|
tracing::debug!("Last call ended, stopping audio thread");
|
||||||
|
|
@ -710,9 +724,11 @@ pub unsafe extern "C" fn on_call_state_cb(raw_call_id: pjsua_call_id, _e: *mut p
|
||||||
}
|
}
|
||||||
// Spam/unauthenticated calls - no logging, no callbacks
|
// Spam/unauthenticated calls - no logging, no callbacks
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
||||||
|
unsafe {
|
||||||
let call_id = CallId::new(raw_call_id);
|
let call_id = CallId::new(raw_call_id);
|
||||||
let mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
let mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
||||||
if pjsua_call_get_info(*call_id, ci.as_mut_ptr()) != pj_constants__PJ_SUCCESS as i32 {
|
if pjsua_call_get_info(*call_id, ci.as_mut_ptr()) != pj_constants__PJ_SUCCESS as i32 {
|
||||||
|
|
@ -820,8 +836,9 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// If conf_port changed and call is registered with a channel, reconnect it
|
// If conf_port changed and call is registered with a channel, reconnect it
|
||||||
if let Some(old_port) = old_conf_port {
|
if let Some(old_port) = old_conf_port
|
||||||
if old_port != conf_port {
|
&& old_port != conf_port
|
||||||
|
{
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Call {} conf_port changed from {} to {} (media renegotiation), reconnecting",
|
"Call {} conf_port changed from {} to {} (media renegotiation), reconnecting",
|
||||||
call_id,
|
call_id,
|
||||||
|
|
@ -898,7 +915,6 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Call {} conf_port {} stored, awaiting channel registration",
|
"Call {} conf_port {} stored, awaiting channel registration",
|
||||||
|
|
@ -946,16 +962,17 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
||||||
// underlying media stream during the hold/unhold re-INVITE cycle.
|
// underlying media stream during the hold/unhold re-INVITE cycle.
|
||||||
// For first-time active calls, the call won't be in CHANNEL_CALLS yet,
|
// For first-time active calls, the call won't be in CHANNEL_CALLS yet,
|
||||||
// so this is a no-op.
|
// so this is a no-op.
|
||||||
if is_new {
|
if is_new
|
||||||
if let Some(channel_id) = CALL_CHANNELS
|
&& let Some(channel_id) = CALL_CHANNELS
|
||||||
.get()
|
.get()
|
||||||
.and_then(|c| c.get(&call_id).map(|r| *r))
|
.and_then(|c| c.get(&call_id).map(|r| *r))
|
||||||
{
|
{
|
||||||
let channel_calls = CHANNEL_CALLS
|
let channel_calls = CHANNEL_CALLS
|
||||||
.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()));
|
.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()));
|
||||||
let mut map = channel_calls.write();
|
let mut map = channel_calls.write();
|
||||||
if let Some(calls) = map.get_mut(&channel_id) {
|
if let Some(calls) = map.get_mut(&channel_id)
|
||||||
if calls.remove(&call_id) {
|
&& calls.remove(&call_id)
|
||||||
|
{
|
||||||
if calls.is_empty() {
|
if calls.is_empty() {
|
||||||
map.remove(&channel_id);
|
map.remove(&channel_id);
|
||||||
}
|
}
|
||||||
|
|
@ -965,8 +982,6 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the call was already registered with a channel (Discord connected before
|
// If the call was already registered with a channel (Discord connected before
|
||||||
// media was ready), complete the registration now. This must happen AFTER
|
// media was ready), complete the registration now. This must happen AFTER
|
||||||
|
|
@ -1051,6 +1066,7 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
||||||
call_id
|
call_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// T.38 offer parameters extracted from SDP
|
/// T.38 offer parameters extracted from SDP
|
||||||
|
|
@ -1070,6 +1086,7 @@ unsafe fn sdp_has_t38(offer: *const pjmedia_sdp_session) -> Option<T38OfferParam
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
for i in 0..(*offer).media_count {
|
for i in 0..(*offer).media_count {
|
||||||
let m = (*offer).media[i as usize];
|
let m = (*offer).media[i as usize];
|
||||||
if m.is_null() {
|
if m.is_null() {
|
||||||
|
|
@ -1147,6 +1164,7 @@ unsafe fn sdp_has_t38(offer: *const pjmedia_sdp_session) -> Option<T38OfferParam
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback for incoming re-INVITE with SDP offer.
|
/// Callback for incoming re-INVITE with SDP offer.
|
||||||
|
|
@ -1172,6 +1190,7 @@ pub unsafe extern "C" fn on_call_rx_reinvite_cb(
|
||||||
code: *mut pjsip_status_code,
|
code: *mut pjsip_status_code,
|
||||||
opt: *mut pjsua_call_setting,
|
opt: *mut pjsua_call_setting,
|
||||||
) {
|
) {
|
||||||
|
unsafe {
|
||||||
let call_id = CallId::new(raw_call_id);
|
let call_id = CallId::new(raw_call_id);
|
||||||
|
|
||||||
// Check for T.38 offer BEFORE applying hold-stripping logic
|
// Check for T.38 offer BEFORE applying hold-stripping logic
|
||||||
|
|
@ -1427,6 +1446,7 @@ pub unsafe extern "C" fn on_call_rx_reinvite_cb(
|
||||||
call_id,
|
call_id,
|
||||||
stripped
|
stripped
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Strip hold direction attributes from the SDP negotiator's remote offer clone.
|
/// Strip hold direction attributes from the SDP negotiator's remote offer clone.
|
||||||
|
|
@ -1437,6 +1457,7 @@ unsafe fn strip_hold_from_neg_remote(call_id: CallId, rdata: *mut pjsip_rx_data)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
// rdata → transaction → dialog → inv session → SDP negotiator
|
// rdata → transaction → dialog → inv session → SDP negotiator
|
||||||
let tsx = pjsip_rdata_get_tsx(rdata);
|
let tsx = pjsip_rdata_get_tsx(rdata);
|
||||||
if tsx.is_null() {
|
if tsx.is_null() {
|
||||||
|
|
@ -1512,4 +1533,5 @@ unsafe fn strip_hold_from_neg_remote(call_id: CallId, rdata: *mut pjsip_rx_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
stripped_any
|
stripped_any
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,12 @@ pub unsafe extern "C" fn channel_port_get_frame(
|
||||||
return -1; // PJ_EINVAL
|
return -1; // PJ_EINVAL
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel_id = Snowflake::new((*this_port).port_data.ldata as u64);
|
let channel_id = unsafe { Snowflake::new((*this_port).port_data.ldata as u64) };
|
||||||
if *channel_id == 0 {
|
if *channel_id == 0 {
|
||||||
|
unsafe {
|
||||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_NONE;
|
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_NONE;
|
||||||
(*frame).size = 0;
|
(*frame).size = 0;
|
||||||
|
}
|
||||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,10 +171,10 @@ pub unsafe extern "C" fn channel_port_get_frame(
|
||||||
}
|
}
|
||||||
|
|
||||||
if samples_len > 0 {
|
if samples_len > 0 {
|
||||||
let samples = std::slice::from_raw_parts(samples_ptr, samples_len);
|
let samples = unsafe { std::slice::from_raw_parts(samples_ptr, samples_len) };
|
||||||
super::ffi::frame_utils::fill_audio_frame(frame, samples);
|
unsafe { super::ffi::frame_utils::fill_audio_frame(frame, samples) };
|
||||||
} else {
|
} else {
|
||||||
super::ffi::frame_utils::fill_silence_frame(frame);
|
unsafe { super::ffi::frame_utils::fill_silence_frame(frame) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
|
|
@ -186,8 +188,9 @@ fn get_samples_from_buffer(channel_id: Snowflake, buf: &mut [i16; SAMPLES_PER_FR
|
||||||
static DRAIN_COUNT: AtomicU64 = AtomicU64::new(0);
|
static DRAIN_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||||
static UNDERRUN_COUNT: AtomicU64 = AtomicU64::new(0);
|
static UNDERRUN_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
if let Some(consumer_entry) = get_discord_to_sip_consumers().get(&channel_id) {
|
if let Some(consumer_entry) = get_discord_to_sip_consumers().get(&channel_id)
|
||||||
if let Some(mut consumer) = consumer_entry.try_lock() {
|
&& let Some(mut consumer) = consumer_entry.try_lock()
|
||||||
|
{
|
||||||
let available = consumer.slots();
|
let available = consumer.slots();
|
||||||
if available >= SAMPLES_PER_FRAME {
|
if available >= SAMPLES_PER_FRAME {
|
||||||
let count = DRAIN_COUNT.fetch_add(1, Ordering::Relaxed);
|
let count = DRAIN_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
@ -232,7 +235,6 @@ fn get_samples_from_buffer(channel_id: Snowflake, buf: &mut [i16; SAMPLES_PER_FR
|
||||||
return available;
|
return available;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
0 // No audio available
|
0 // No audio available
|
||||||
}
|
}
|
||||||
|
|
@ -254,11 +256,13 @@ pub unsafe extern "C" fn channel_port_put_frame(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process audio frames with data
|
// Only process audio frames with data
|
||||||
if (*frame).type_ != pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO || (*frame).size == 0 {
|
if unsafe {
|
||||||
|
(*frame).type_ != pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO || (*frame).size == 0
|
||||||
|
} {
|
||||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel_id = Snowflake::new((*this_port).port_data.ldata as u64);
|
let channel_id = unsafe { Snowflake::new((*this_port).port_data.ldata as u64) };
|
||||||
if *channel_id == 0 {
|
if *channel_id == 0 {
|
||||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||||
}
|
}
|
||||||
|
|
@ -270,16 +274,18 @@ pub unsafe extern "C" fn channel_port_put_frame(
|
||||||
call_count,
|
call_count,
|
||||||
this_port,
|
this_port,
|
||||||
channel_id,
|
channel_id,
|
||||||
(*frame).size
|
unsafe { (*frame).size }
|
||||||
);
|
);
|
||||||
} else if call_count == 10 {
|
} else if call_count == 10 {
|
||||||
tracing::trace!("channel_port_put_frame: suppressing further per-call logs");
|
tracing::trace!("channel_port_put_frame: suppressing further per-call logs");
|
||||||
}
|
}
|
||||||
|
|
||||||
// View frame buffer as i16 slice (zero-copy)
|
// View frame buffer as i16 slice (zero-copy)
|
||||||
|
let samples = unsafe {
|
||||||
let num_samples = (*frame).size / 2;
|
let num_samples = (*frame).size / 2;
|
||||||
let frame_buf = (*frame).buf as *const i16;
|
let frame_buf = (*frame).buf as *const i16;
|
||||||
let samples = std::slice::from_raw_parts(frame_buf, num_samples);
|
std::slice::from_raw_parts(frame_buf, num_samples)
|
||||||
|
};
|
||||||
|
|
||||||
// Store in the SIP->Discord buffer for this channel
|
// Store in the SIP->Discord buffer for this channel
|
||||||
let buffers = CHANNEL_AUDIO_IN.get_or_init(DashMap::new);
|
let buffers = CHANNEL_AUDIO_IN.get_or_init(DashMap::new);
|
||||||
|
|
@ -324,7 +330,7 @@ pub unsafe extern "C" fn channel_port_put_frame(
|
||||||
/// Custom on_destroy callback for channel buffer ports
|
/// Custom on_destroy callback for channel buffer ports
|
||||||
pub unsafe extern "C" fn channel_port_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
pub unsafe extern "C" fn channel_port_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
||||||
if !this_port.is_null() {
|
if !this_port.is_null() {
|
||||||
// Remove from reverse mapping
|
// Remove from reverse mapping (no unsafe ops needed here, just pointer-to-usize cast)
|
||||||
let port_key = this_port as usize;
|
let port_key = this_port as usize;
|
||||||
if let Some(mapping) = PORT_TO_CHANNEL.get() {
|
if let Some(mapping) = PORT_TO_CHANNEL.get() {
|
||||||
mapping.lock().remove(&port_key);
|
mapping.lock().remove(&port_key);
|
||||||
|
|
@ -348,10 +354,12 @@ unsafe fn connect_call_to_channel(
|
||||||
) {
|
) {
|
||||||
// Connect this call to other calls in the same channel
|
// Connect this call to other calls in the same channel
|
||||||
for &(other_call_id, other_conf_port) in other_calls {
|
for &(other_call_id, other_conf_port) in other_calls {
|
||||||
let status1 =
|
let (status1, status2) = unsafe {
|
||||||
pjmedia_conf_connect_port(conf, *conf_port as u32, *other_conf_port as u32, 0);
|
(
|
||||||
let status2 =
|
pjmedia_conf_connect_port(conf, *conf_port as u32, *other_conf_port as u32, 0),
|
||||||
pjmedia_conf_connect_port(conf, *other_conf_port as u32, *conf_port as u32, 0);
|
pjmedia_conf_connect_port(conf, *other_conf_port as u32, *conf_port as u32, 0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
if status1 == pj_constants__PJ_SUCCESS as i32 && status2 == pj_constants__PJ_SUCCESS as i32
|
if status1 == pj_constants__PJ_SUCCESS as i32 && status2 == pj_constants__PJ_SUCCESS as i32
|
||||||
{
|
{
|
||||||
|
|
@ -377,10 +385,14 @@ unsafe fn connect_call_to_channel(
|
||||||
|
|
||||||
// Connect call to channel's conference port bidirectionally
|
// Connect call to channel's conference port bidirectionally
|
||||||
if let Some(channel_slot) = get_or_create_channel_port(channel_id) {
|
if let Some(channel_slot) = get_or_create_channel_port(channel_id) {
|
||||||
|
let (status1, status2) = unsafe {
|
||||||
|
(
|
||||||
// Channel port -> call (Discord audio reaches this call)
|
// Channel port -> call (Discord audio reaches this call)
|
||||||
let status1 = pjmedia_conf_connect_port(conf, *channel_slot as u32, *conf_port as u32, 0);
|
pjmedia_conf_connect_port(conf, *channel_slot as u32, *conf_port as u32, 0),
|
||||||
// Call -> channel port (SIP audio goes to channel for Discord)
|
// Call -> channel port (SIP audio goes to channel for Discord)
|
||||||
let status2 = pjmedia_conf_connect_port(conf, *conf_port as u32, *channel_slot as u32, 0);
|
pjmedia_conf_connect_port(conf, *conf_port as u32, *channel_slot as u32, 0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
if status1 != pj_constants__PJ_SUCCESS as i32 {
|
if status1 != pj_constants__PJ_SUCCESS as i32 {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
@ -429,8 +441,10 @@ unsafe fn disconnect_call_from_channel(
|
||||||
// Disconnect from other calls in the channel (both directions)
|
// Disconnect from other calls in the channel (both directions)
|
||||||
for &other_call_id in remaining_calls {
|
for &other_call_id in remaining_calls {
|
||||||
if let Some(other_conf_port) = conf_ports.get(&other_call_id).map(|r| *r) {
|
if let Some(other_conf_port) = conf_ports.get(&other_call_id).map(|r| *r) {
|
||||||
|
unsafe {
|
||||||
pjmedia_conf_disconnect_port(conf, *conf_port as u32, *other_conf_port as u32);
|
pjmedia_conf_disconnect_port(conf, *conf_port as u32, *other_conf_port as u32);
|
||||||
pjmedia_conf_disconnect_port(conf, *other_conf_port as u32, *conf_port as u32);
|
pjmedia_conf_disconnect_port(conf, *other_conf_port as u32, *conf_port as u32);
|
||||||
|
}
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Disconnected call {} from call {} in channel {}",
|
"Disconnected call {} from call {} in channel {}",
|
||||||
call_id,
|
call_id,
|
||||||
|
|
@ -442,8 +456,10 @@ unsafe fn disconnect_call_from_channel(
|
||||||
|
|
||||||
// Disconnect from channel port (both directions)
|
// Disconnect from channel port (both directions)
|
||||||
if let Some(channel_slot) = get_channel_slot(channel_id) {
|
if let Some(channel_slot) = get_channel_slot(channel_id) {
|
||||||
|
unsafe {
|
||||||
pjmedia_conf_disconnect_port(conf, *channel_slot as u32, *conf_port as u32);
|
pjmedia_conf_disconnect_port(conf, *channel_slot as u32, *conf_port as u32);
|
||||||
pjmedia_conf_disconnect_port(conf, *conf_port as u32, *channel_slot as u32);
|
pjmedia_conf_disconnect_port(conf, *conf_port as u32, *channel_slot as u32);
|
||||||
|
}
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Disconnected channel {} slot {} <-> call {} (port {}) bidirectionally",
|
"Disconnected channel {} slot {} <-> call {} (port {}) bidirectionally",
|
||||||
channel_id,
|
channel_id,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
/// Custom get_frame callback for direct player ports
|
/// Custom get_frame callback for direct player ports
|
||||||
/// Returns samples from the player's buffer, advancing position each call
|
/// Returns samples from the player's buffer, advancing position each call
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Called by the pjmedia conference bridge. `this_port` and `frame` must be
|
||||||
|
/// valid, non-null pointers to pjmedia structures owned by pjsua.
|
||||||
pub unsafe extern "C" fn direct_player_get_frame(
|
pub unsafe extern "C" fn direct_player_get_frame(
|
||||||
this_port: *mut pjmedia_port,
|
this_port: *mut pjmedia_port,
|
||||||
frame: *mut pjmedia_frame,
|
frame: *mut pjmedia_frame,
|
||||||
|
|
@ -45,13 +49,13 @@ pub unsafe extern "C" fn direct_player_get_frame(
|
||||||
if let Some((buffer, pos)) = state.get_mut(&port_key) {
|
if let Some((buffer, pos)) = state.get_mut(&port_key) {
|
||||||
if *pos < buffer.len() {
|
if *pos < buffer.len() {
|
||||||
let end = (*pos + SAMPLES_PER_FRAME).min(buffer.len());
|
let end = (*pos + SAMPLES_PER_FRAME).min(buffer.len());
|
||||||
super::frame_utils::fill_audio_frame(frame, &buffer[*pos..end]);
|
unsafe { super::frame_utils::fill_audio_frame(frame, &buffer[*pos..end]) };
|
||||||
*pos = end;
|
*pos = end;
|
||||||
} else {
|
} else {
|
||||||
super::frame_utils::fill_silence_frame(frame); // Playback complete
|
unsafe { super::frame_utils::fill_silence_frame(frame) }; // Playback complete
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
super::frame_utils::fill_silence_frame(frame);
|
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +63,10 @@ pub unsafe extern "C" fn direct_player_get_frame(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom on_destroy callback for direct player ports
|
/// Custom on_destroy callback for direct player ports
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Called by pjmedia when the port is being destroyed. `this_port` must be
|
||||||
|
/// a valid pointer to a pjmedia_port that was previously created by this module.
|
||||||
pub unsafe extern "C" fn direct_player_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
pub unsafe extern "C" fn direct_player_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
||||||
if !this_port.is_null() {
|
if !this_port.is_null() {
|
||||||
let port_key = this_port as usize;
|
let port_key = this_port as usize;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub unsafe fn get_conference_bridge() -> Option<*mut pjmedia_conf> {
|
||||||
if master_port.is_null() {
|
if master_port.is_null() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let conf = (*master_port).port_data.pdata as *mut pjmedia_conf;
|
let conf = unsafe { (*master_port).port_data.pdata as *mut pjmedia_conf };
|
||||||
if conf.is_null() {
|
if conf.is_null() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ pub unsafe fn get_conference_bridge() -> Option<*mut pjmedia_conf> {
|
||||||
/// `frame` must be a valid, non-null pointer to a pjmedia_frame with a buffer
|
/// `frame` must be a valid, non-null pointer to a pjmedia_frame with a buffer
|
||||||
/// large enough for SAMPLES_PER_FRAME i16 samples.
|
/// large enough for SAMPLES_PER_FRAME i16 samples.
|
||||||
pub unsafe fn fill_audio_frame(frame: *mut pjmedia_frame, samples: &[i16]) {
|
pub unsafe fn fill_audio_frame(frame: *mut pjmedia_frame, samples: &[i16]) {
|
||||||
|
unsafe {
|
||||||
let frame_buf = (*frame).buf as *mut i16;
|
let frame_buf = (*frame).buf as *mut i16;
|
||||||
std::ptr::copy_nonoverlapping(samples.as_ptr(), frame_buf, samples.len());
|
std::ptr::copy_nonoverlapping(samples.as_ptr(), frame_buf, samples.len());
|
||||||
// Pad with silence if we got fewer samples than a full frame
|
// Pad with silence if we got fewer samples than a full frame
|
||||||
|
|
@ -50,6 +51,7 @@ pub unsafe fn fill_audio_frame(frame: *mut pjmedia_frame, samples: &[i16]) {
|
||||||
}
|
}
|
||||||
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
||||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill a pjmedia_frame with silence.
|
/// Fill a pjmedia_frame with silence.
|
||||||
|
|
@ -58,10 +60,12 @@ pub unsafe fn fill_audio_frame(frame: *mut pjmedia_frame, samples: &[i16]) {
|
||||||
/// `frame` must be a valid, non-null pointer to a pjmedia_frame with a buffer
|
/// `frame` must be a valid, non-null pointer to a pjmedia_frame with a buffer
|
||||||
/// large enough for SAMPLES_PER_FRAME i16 samples.
|
/// large enough for SAMPLES_PER_FRAME i16 samples.
|
||||||
pub unsafe fn fill_silence_frame(frame: *mut pjmedia_frame) {
|
pub unsafe fn fill_silence_frame(frame: *mut pjmedia_frame) {
|
||||||
|
unsafe {
|
||||||
let frame_buf = (*frame).buf as *mut u8;
|
let frame_buf = (*frame).buf as *mut u8;
|
||||||
std::ptr::write_bytes(frame_buf, 0, SAMPLES_PER_FRAME * 2);
|
std::ptr::write_bytes(frame_buf, 0, SAMPLES_PER_FRAME * 2);
|
||||||
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
||||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No-op put_frame callback for ports that only produce audio.
|
/// No-op put_frame callback for ports that only produce audio.
|
||||||
|
|
@ -119,14 +123,14 @@ pub unsafe fn create_and_connect_port(
|
||||||
) -> Result<ConfPortGuard> {
|
) -> Result<ConfPortGuard> {
|
||||||
// Get or create the memory pool
|
// Get or create the memory pool
|
||||||
let pool = pool.get_or_init(|| {
|
let pool = pool.get_or_init(|| {
|
||||||
let p = pjsua_pool_create(pool_name.as_ptr() as *const _, 4096, 4096);
|
let p = unsafe { pjsua_pool_create(pool_name.as_ptr() as *const _, 4096, 4096) };
|
||||||
Mutex::new(SendablePool(p))
|
Mutex::new(SendablePool(p))
|
||||||
});
|
});
|
||||||
let pool_ptr = pool.lock().0;
|
let pool_ptr = pool.lock().0;
|
||||||
|
|
||||||
// Allocate pjmedia_port structure
|
// Allocate pjmedia_port structure
|
||||||
let port_size = std::mem::size_of::<pjmedia_port>();
|
let port_size = std::mem::size_of::<pjmedia_port>();
|
||||||
let port = pj_pool_alloc(pool_ptr, port_size) as *mut pjmedia_port;
|
let port = unsafe { pj_pool_alloc(pool_ptr, port_size) as *mut pjmedia_port };
|
||||||
if port.is_null() {
|
if port.is_null() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Failed to allocate {} port for call {}",
|
"Failed to allocate {} port for call {}",
|
||||||
|
|
@ -134,7 +138,7 @@ pub unsafe fn create_and_connect_port(
|
||||||
call_id
|
call_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
std::ptr::write_bytes(port as *mut u8, 0, port_size);
|
unsafe { std::ptr::write_bytes(port as *mut u8, 0, port_size) };
|
||||||
|
|
||||||
// Create port name
|
// Create port name
|
||||||
let port_name = format!("{}{}", name_prefix, call_id);
|
let port_name = format!("{}{}", name_prefix, call_id);
|
||||||
|
|
@ -142,6 +146,7 @@ pub unsafe fn create_and_connect_port(
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid port name: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Invalid port name: {}", e))?;
|
||||||
|
|
||||||
// Initialize port info
|
// Initialize port info
|
||||||
|
unsafe {
|
||||||
pjmedia_port_info_init(
|
pjmedia_port_info_init(
|
||||||
&mut (*port).info,
|
&mut (*port).info,
|
||||||
&pj_str(port_name_cstr.as_ptr() as *mut _),
|
&pj_str(port_name_cstr.as_ptr() as *mut _),
|
||||||
|
|
@ -156,24 +161,26 @@ pub unsafe fn create_and_connect_port(
|
||||||
(*port).get_frame = Some(callbacks.get_frame);
|
(*port).get_frame = Some(callbacks.get_frame);
|
||||||
(*port).put_frame = Some(callbacks.put_frame);
|
(*port).put_frame = Some(callbacks.put_frame);
|
||||||
(*port).on_destroy = callbacks.on_destroy;
|
(*port).on_destroy = callbacks.on_destroy;
|
||||||
|
}
|
||||||
|
|
||||||
// Add to conference
|
// Add to conference
|
||||||
let mut player_slot: i32 = 0;
|
let mut player_slot: i32 = 0;
|
||||||
let status = pjsua_conf_add_port(pool_ptr, port, &mut player_slot);
|
let status = unsafe { pjsua_conf_add_port(pool_ptr, port, &mut player_slot) };
|
||||||
if status != pj_constants__PJ_SUCCESS as i32 {
|
if status != pj_constants__PJ_SUCCESS as i32 {
|
||||||
anyhow::bail!("Failed to add {} port to conf: {}", name_prefix, status);
|
anyhow::bail!("Failed to add {} port to conf: {}", name_prefix, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect player port to the target call's port
|
// Connect player port to the target call's port
|
||||||
let conf = get_conference_bridge();
|
let conf = unsafe { get_conference_bridge() };
|
||||||
let Some(conf) = conf else {
|
let Some(conf) = conf else {
|
||||||
pjsua_conf_remove_port(player_slot);
|
unsafe { pjsua_conf_remove_port(player_slot) };
|
||||||
anyhow::bail!("Failed to get conference bridge for {} port", name_prefix);
|
anyhow::bail!("Failed to get conference bridge for {} port", name_prefix);
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = pjmedia_conf_connect_port(conf, player_slot as u32, *call_conf_port as u32, 0);
|
let status =
|
||||||
|
unsafe { pjmedia_conf_connect_port(conf, player_slot as u32, *call_conf_port as u32, 0) };
|
||||||
if status != pj_constants__PJ_SUCCESS as i32 {
|
if status != pj_constants__PJ_SUCCESS as i32 {
|
||||||
pjsua_conf_remove_port(player_slot);
|
unsafe { pjsua_conf_remove_port(player_slot) };
|
||||||
anyhow::bail!("Failed to connect {} port to call: {}", name_prefix, status);
|
anyhow::bail!("Failed to connect {} port to call: {}", name_prefix, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ unsafe extern "C" fn pjsip_log_callback(level: c_int, data: *const c_char, _len:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let c_str = std::ffi::CStr::from_ptr(data);
|
let c_str = unsafe { std::ffi::CStr::from_ptr(data) };
|
||||||
let msg = c_str.to_string_lossy();
|
let msg = c_str.to_string_lossy();
|
||||||
let msg = msg.trim_end();
|
let msg = msg.trim_end();
|
||||||
|
|
||||||
|
|
@ -375,11 +375,11 @@ pub fn init_pjsua(config: &SipConfig, tls_config: Option<&TlsConfig>) -> Result<
|
||||||
tracing::info!("TCP transport created on port {}", config.port);
|
tracing::info!("TCP transport created on port {}", config.port);
|
||||||
|
|
||||||
// Create TLS transport if configured (skip gracefully if certs missing)
|
// Create TLS transport if configured (skip gracefully if certs missing)
|
||||||
if let Some(tls) = tls_config {
|
if let Some(tls) = tls_config
|
||||||
if !create_tls_transport(tls, &config.public_host)? {
|
&& !create_tls_transport(tls, &config.public_host)?
|
||||||
|
{
|
||||||
tracing::warn!("TLS transport not created - running without TLS");
|
tracing::warn!("TLS transport not created - running without TLS");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Start pjsua
|
// Start pjsua
|
||||||
let status = pjsua_start();
|
let status = pjsua_start();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ pub struct LoopingPlayerState {
|
||||||
|
|
||||||
/// Custom get_frame callback for looping player ports
|
/// Custom get_frame callback for looping player ports
|
||||||
/// Returns samples from the player's buffer, looping back to start when reaching end
|
/// Returns samples from the player's buffer, looping back to start when reaching end
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Called by the pjmedia conference bridge. `this_port` and `frame` must be
|
||||||
|
/// valid, non-null pointers to pjmedia structures owned by pjsua.
|
||||||
pub unsafe extern "C" fn looping_player_get_frame(
|
pub unsafe extern "C" fn looping_player_get_frame(
|
||||||
this_port: *mut pjmedia_port,
|
this_port: *mut pjmedia_port,
|
||||||
frame: *mut pjmedia_frame,
|
frame: *mut pjmedia_frame,
|
||||||
|
|
@ -73,7 +77,9 @@ pub unsafe extern "C" fn looping_player_get_frame(
|
||||||
if player_data.is_active.load(Ordering::SeqCst) && !player_data.samples.is_empty() {
|
if player_data.is_active.load(Ordering::SeqCst) && !player_data.samples.is_empty() {
|
||||||
let pos = player_data.position;
|
let pos = player_data.position;
|
||||||
let end = (pos + SAMPLES_PER_FRAME).min(player_data.samples.len());
|
let end = (pos + SAMPLES_PER_FRAME).min(player_data.samples.len());
|
||||||
super::frame_utils::fill_audio_frame(frame, &player_data.samples[pos..end]);
|
unsafe {
|
||||||
|
super::frame_utils::fill_audio_frame(frame, &player_data.samples[pos..end])
|
||||||
|
};
|
||||||
|
|
||||||
// Advance position, loop back if at end
|
// Advance position, loop back if at end
|
||||||
player_data.position = if end >= player_data.samples.len() {
|
player_data.position = if end >= player_data.samples.len() {
|
||||||
|
|
@ -82,10 +88,10 @@ pub unsafe extern "C" fn looping_player_get_frame(
|
||||||
end
|
end
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
super::frame_utils::fill_silence_frame(frame);
|
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
super::frame_utils::fill_silence_frame(frame);
|
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,6 +99,10 @@ pub unsafe extern "C" fn looping_player_get_frame(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom on_destroy callback for looping player ports
|
/// Custom on_destroy callback for looping player ports
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// Called by pjmedia when the port is being destroyed. `this_port` must be
|
||||||
|
/// a valid pointer to a pjmedia_port that was previously created by this module.
|
||||||
pub unsafe extern "C" fn looping_player_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
pub unsafe extern "C" fn looping_player_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
||||||
if !this_port.is_null() {
|
if !this_port.is_null() {
|
||||||
let port_key = this_port as usize;
|
let port_key = this_port as usize;
|
||||||
|
|
@ -194,11 +204,11 @@ pub fn stop_loop(call_id: CallId) {
|
||||||
|
|
||||||
if let Some(state) = state {
|
if let Some(state) = state {
|
||||||
// Mark as inactive (get_frame will return silence)
|
// Mark as inactive (get_frame will return silence)
|
||||||
if let Some(data) = LOOPING_PLAYER_DATA.get() {
|
if let Some(data) = LOOPING_PLAYER_DATA.get()
|
||||||
if let Some(player_data) = data.lock().get(&state.port_key) {
|
&& let Some(player_data) = data.lock().get(&state.port_key)
|
||||||
|
{
|
||||||
player_data.is_active.store(false, Ordering::SeqCst);
|
player_data.is_active.store(false, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from conference
|
// Remove from conference
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
|
|
|
||||||
|
|
@ -118,9 +118,9 @@ pub unsafe extern "C" fn streaming_get_frame(
|
||||||
|
|
||||||
// Fill frame buffer
|
// Fill frame buffer
|
||||||
if !samples.is_empty() {
|
if !samples.is_empty() {
|
||||||
super::frame_utils::fill_audio_frame(frame, &samples);
|
unsafe { super::frame_utils::fill_audio_frame(frame, &samples) };
|
||||||
} else {
|
} else {
|
||||||
super::frame_utils::fill_silence_frame(frame);
|
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||||
}
|
}
|
||||||
|
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ pub unsafe extern "C" fn test_tone_get_frame(
|
||||||
}
|
}
|
||||||
|
|
||||||
if tone_state.finished {
|
if tone_state.finished {
|
||||||
super::frame_utils::fill_silence_frame(frame);
|
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||||
} else {
|
} else {
|
||||||
// Copy from precomputed LUT with wraparound (two memcpy calls max)
|
// Copy from precomputed LUT with wraparound (two memcpy calls max)
|
||||||
let lut = tone_lut();
|
let lut = tone_lut();
|
||||||
|
|
@ -85,6 +85,7 @@ pub unsafe extern "C" fn test_tone_get_frame(
|
||||||
tone_state.phase += SAMPLES_PER_FRAME as u64;
|
tone_state.phase += SAMPLES_PER_FRAME as u64;
|
||||||
|
|
||||||
let first_chunk = (lut_len - phase).min(SAMPLES_PER_FRAME);
|
let first_chunk = (lut_len - phase).min(SAMPLES_PER_FRAME);
|
||||||
|
unsafe {
|
||||||
let frame_buf = (*frame).buf as *mut i16;
|
let frame_buf = (*frame).buf as *mut i16;
|
||||||
std::ptr::copy_nonoverlapping(
|
std::ptr::copy_nonoverlapping(
|
||||||
lut[phase..phase + first_chunk].as_ptr(),
|
lut[phase..phase + first_chunk].as_ptr(),
|
||||||
|
|
@ -104,8 +105,9 @@ pub unsafe extern "C" fn test_tone_get_frame(
|
||||||
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
||||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
super::frame_utils::fill_silence_frame(frame);
|
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ pub unsafe fn pj_str_to_string(s: &pj_str_t) -> String {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let slice = std::slice::from_raw_parts(s.ptr as *const u8, s.slen as usize);
|
let slice = unsafe { std::slice::from_raw_parts(s.ptr as *const u8, s.slen as usize) };
|
||||||
String::from_utf8_lossy(slice).to_string()
|
String::from_utf8_lossy(slice).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -539,10 +539,9 @@ fn make_outbound_call(sip_uri: &str, caller_display_name: Option<&str>) -> Resul
|
||||||
== ::pjsua::pj_constants__PJ_SUCCESS as i32
|
== ::pjsua::pj_constants__PJ_SUCCESS as i32
|
||||||
{
|
{
|
||||||
let ai = acc_info.assume_init();
|
let ai = acc_info.assume_init();
|
||||||
let uri_str = std::ffi::CStr::from_ptr(ai.acc_uri.ptr)
|
std::ffi::CStr::from_ptr(ai.acc_uri.ptr)
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.into_owned();
|
.into_owned()
|
||||||
uri_str
|
|
||||||
} else {
|
} else {
|
||||||
"sip:sipcord@localhost".to_string()
|
"sip:sipcord@localhost".to_string()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,13 @@ fn sanitize_sdp_missing_rtpmap(sdp: &str) -> Option<String> {
|
||||||
let mut rtpmap_pts: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
let mut rtpmap_pts: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
||||||
for line in section_lines {
|
for line in section_lines {
|
||||||
// a=rtpmap:96 opus/48000/2
|
// a=rtpmap:96 opus/48000/2
|
||||||
if let Some(rest) = line.strip_prefix("a=rtpmap:") {
|
if let Some(rest) = line.strip_prefix("a=rtpmap:")
|
||||||
if let Some(pt_str) = rest.split_whitespace().next() {
|
&& let Some(pt_str) = rest.split_whitespace().next()
|
||||||
if let Ok(pt) = pt_str.parse::<u32>() {
|
&& let Ok(pt) = pt_str.parse::<u32>()
|
||||||
|
{
|
||||||
rtpmap_pts.insert(pt);
|
rtpmap_pts.insert(pt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which PTs in the m= line need stripping
|
// Check which PTs in the m= line need stripping
|
||||||
let payload_types = &parts[3..];
|
let payload_types = &parts[3..];
|
||||||
|
|
@ -94,12 +93,13 @@ fn sanitize_sdp_missing_rtpmap(sdp: &str) -> Option<String> {
|
||||||
let mut stripped_pts: Vec<u32> = Vec::new();
|
let mut stripped_pts: Vec<u32> = Vec::new();
|
||||||
|
|
||||||
for pt_str in payload_types {
|
for pt_str in payload_types {
|
||||||
if let Ok(pt) = pt_str.parse::<u32>() {
|
if let Ok(pt) = pt_str.parse::<u32>()
|
||||||
if pt >= 96 && !rtpmap_pts.contains(&pt) {
|
&& pt >= 96
|
||||||
|
&& !rtpmap_pts.contains(&pt)
|
||||||
|
{
|
||||||
stripped_pts.push(pt);
|
stripped_pts.push(pt);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
kept_pts.push(pt_str);
|
kept_pts.push(pt_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,15 +134,13 @@ fn sanitize_sdp_missing_rtpmap(sdp: &str) -> Option<String> {
|
||||||
// Copy section attribute lines, stripping a=fmtp: for removed PTs
|
// Copy section attribute lines, stripping a=fmtp: for removed PTs
|
||||||
let stripped_set: std::collections::HashSet<u32> = stripped_pts.into_iter().collect();
|
let stripped_set: std::collections::HashSet<u32> = stripped_pts.into_iter().collect();
|
||||||
for line in section_lines {
|
for line in section_lines {
|
||||||
if let Some(rest) = line.strip_prefix("a=fmtp:") {
|
if let Some(rest) = line.strip_prefix("a=fmtp:")
|
||||||
if let Some(pt_str) = rest.split_whitespace().next() {
|
&& let Some(pt_str) = rest.split_whitespace().next()
|
||||||
if let Ok(pt) = pt_str.parse::<u32>() {
|
&& let Ok(pt) = pt_str.parse::<u32>()
|
||||||
if stripped_set.contains(&pt) {
|
&& stripped_set.contains(&pt)
|
||||||
|
{
|
||||||
continue; // skip fmtp for stripped PT
|
continue; // skip fmtp for stripped PT
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result_lines.push(line.to_string());
|
result_lines.push(line.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,14 +162,14 @@ fn is_rfc1918(ip: Ipv4Addr) -> bool {
|
||||||
///
|
///
|
||||||
/// Returns `None` if transport info is invalid or the address is not IPv4.
|
/// Returns `None` if transport info is invalid or the address is not IPv4.
|
||||||
unsafe fn extract_dst_ipv4(tdata: *const pjsip_tx_data) -> Option<Ipv4Addr> {
|
unsafe fn extract_dst_ipv4(tdata: *const pjsip_tx_data) -> Option<Ipv4Addr> {
|
||||||
if (*tdata).tp_info.transport.is_null() || (*tdata).tp_info.dst_addr_len <= 0 {
|
if unsafe { (*tdata).tp_info.transport.is_null() || (*tdata).tp_info.dst_addr_len <= 0 } {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dst_addr = &(*tdata).tp_info.dst_addr;
|
let dst_addr = unsafe { &(*tdata).tp_info.dst_addr };
|
||||||
// PJ_AF_INET is typically 2 (same as AF_INET on most systems)
|
// PJ_AF_INET is typically 2 (same as AF_INET on most systems)
|
||||||
if dst_addr.addr.sa_family == 2 {
|
if unsafe { dst_addr.addr.sa_family } == 2 {
|
||||||
let addr_in = &dst_addr.ipv4;
|
let addr_in = unsafe { &dst_addr.ipv4 };
|
||||||
let ip_bytes = addr_in.sin_addr.s_addr.to_ne_bytes();
|
let ip_bytes = addr_in.sin_addr.s_addr.to_ne_bytes();
|
||||||
Some(Ipv4Addr::new(
|
Some(Ipv4Addr::new(
|
||||||
ip_bytes[0],
|
ip_bytes[0],
|
||||||
|
|
@ -195,32 +193,33 @@ unsafe fn rewrite_contact_host(
|
||||||
new_host: &str,
|
new_host: &str,
|
||||||
new_port: u16,
|
new_port: u16,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let contact_hdr = pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut())
|
let contact_hdr =
|
||||||
|
unsafe { pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut()) }
|
||||||
as *mut pjsip_contact_hdr;
|
as *mut pjsip_contact_hdr;
|
||||||
if contact_hdr.is_null() {
|
if contact_hdr.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = (*contact_hdr).uri;
|
let uri = unsafe { (*contact_hdr).uri };
|
||||||
if uri.is_null() {
|
if uri.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap via vtable to handle pjsip_name_addr wrapping
|
// Unwrap via vtable to handle pjsip_name_addr wrapping
|
||||||
let uri_vptr = (*(uri as *const pjsip_uri)).vptr;
|
let uri_vptr = unsafe { (*(uri as *const pjsip_uri)).vptr };
|
||||||
if uri_vptr.is_null() {
|
if uri_vptr.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let get_uri_fn = match (*uri_vptr).p_get_uri {
|
let get_uri_fn = match unsafe { (*uri_vptr).p_get_uri } {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
let sip_uri_raw = get_uri_fn(uri as *mut std::os::raw::c_void);
|
let sip_uri_raw = unsafe { get_uri_fn(uri as *mut std::os::raw::c_void) };
|
||||||
if sip_uri_raw.is_null() {
|
if sip_uri_raw.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
||||||
if (*sip_uri).host.ptr.is_null() || (*sip_uri).host.slen <= 0 {
|
if unsafe { (*sip_uri).host.ptr.is_null() || (*sip_uri).host.slen <= 0 } {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,15 +227,17 @@ unsafe fn rewrite_contact_host(
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let host_len = new_host.len();
|
let host_len = new_host.len();
|
||||||
let pool_str = pj_pool_alloc(pool, host_len + 1) as *mut c_char;
|
let pool_str = unsafe { pj_pool_alloc(pool, host_len + 1) } as *mut c_char;
|
||||||
if pool_str.is_null() {
|
if pool_str.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(host_cstr.as_ptr(), pool_str, host_len + 1);
|
ptr::copy_nonoverlapping(host_cstr.as_ptr(), pool_str, host_len + 1);
|
||||||
(*sip_uri).host.ptr = pool_str;
|
(*sip_uri).host.ptr = pool_str;
|
||||||
(*sip_uri).host.slen = host_len as i64;
|
(*sip_uri).host.slen = host_len as i64;
|
||||||
(*sip_uri).port = new_port as i32;
|
(*sip_uri).port = new_port as i32;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,12 +252,13 @@ unsafe fn rewrite_sdp_body(
|
||||||
old_ip: &str,
|
old_ip: &str,
|
||||||
new_ip: &str,
|
new_ip: &str,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let body = (*msg).body;
|
let body = unsafe { (*msg).body };
|
||||||
if body.is_null() || (*body).len == 0 || (*body).data.is_null() {
|
if body.is_null() || unsafe { (*body).len == 0 || (*body).data.is_null() } {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let body_slice = std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize);
|
let body_slice =
|
||||||
|
unsafe { std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize) };
|
||||||
let Ok(sdp_str) = std::str::from_utf8(body_slice) else {
|
let Ok(sdp_str) = std::str::from_utf8(body_slice) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -284,14 +286,16 @@ unsafe fn rewrite_sdp_body(
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_len = new_sdp.len();
|
let new_len = new_sdp.len();
|
||||||
let new_body_ptr = pj_pool_alloc(pool, new_len) as *mut u8;
|
let new_body_ptr = unsafe { pj_pool_alloc(pool, new_len) } as *mut u8;
|
||||||
if new_body_ptr.is_null() {
|
if new_body_ptr.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(new_sdp.as_ptr(), new_body_ptr, new_len);
|
ptr::copy_nonoverlapping(new_sdp.as_ptr(), new_body_ptr, new_len);
|
||||||
(*body).data = new_body_ptr as *mut _;
|
(*body).data = new_body_ptr as *mut _;
|
||||||
(*body).len = new_len as u32;
|
(*body).len = new_len as u32;
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,7 +314,7 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(dst_ip) = extract_dst_ipv4(tdata) else {
|
let Some(dst_ip) = (unsafe { extract_dst_ipv4(tdata) }) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -318,7 +322,7 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = (*tdata).msg;
|
let msg = unsafe { (*tdata).msg };
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +330,7 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
|
||||||
// Rewrite Contact header
|
// Rewrite Contact header
|
||||||
if rewrite_contact_host((*tdata).pool, msg, local_host, *port) {
|
if unsafe { rewrite_contact_host((*tdata).pool, msg, local_host, *port) } {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Rewrote {} Contact header for local client {}: host -> {}:{}",
|
"Rewrote {} Contact header for local client {}: host -> {}:{}",
|
||||||
direction,
|
direction,
|
||||||
|
|
@ -338,8 +342,9 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite SDP body if we have an RTP public IP to replace
|
// Rewrite SDP body if we have an RTP public IP to replace
|
||||||
if let Some(public_ip) = rtp_public_ip {
|
if let Some(public_ip) = rtp_public_ip
|
||||||
if rewrite_sdp_body((*tdata).pool, msg, public_ip, local_host) {
|
&& unsafe { rewrite_sdp_body((*tdata).pool, msg, public_ip, local_host) }
|
||||||
|
{
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Rewrote {} SDP for local client {}: {} -> {}",
|
"Rewrote {} SDP for local client {}: {} -> {}",
|
||||||
direction,
|
direction,
|
||||||
|
|
@ -349,7 +354,6 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
||||||
);
|
);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
@ -370,42 +374,43 @@ unsafe fn rewrite_private_contact_for_external(tdata: *mut pjsip_tx_data, direct
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = (*tdata).msg;
|
let msg = unsafe { (*tdata).msg };
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find Contact header
|
// Find Contact header
|
||||||
let contact_hdr = pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut())
|
let contact_hdr =
|
||||||
|
unsafe { pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut()) }
|
||||||
as *mut pjsip_contact_hdr;
|
as *mut pjsip_contact_hdr;
|
||||||
if contact_hdr.is_null() {
|
if contact_hdr.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = (*contact_hdr).uri;
|
let uri = unsafe { (*contact_hdr).uri };
|
||||||
if uri.is_null() {
|
if uri.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap via vtable to handle pjsip_name_addr wrapping
|
// Unwrap via vtable to handle pjsip_name_addr wrapping
|
||||||
let uri_vptr = (*(uri as *const pjsip_uri)).vptr;
|
let uri_vptr = unsafe { (*(uri as *const pjsip_uri)).vptr };
|
||||||
if uri_vptr.is_null() {
|
if uri_vptr.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let get_uri_fn = match (*uri_vptr).p_get_uri {
|
let get_uri_fn = match unsafe { (*uri_vptr).p_get_uri } {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => return false,
|
None => return false,
|
||||||
};
|
};
|
||||||
let sip_uri_raw = get_uri_fn(uri as *mut std::os::raw::c_void);
|
let sip_uri_raw = unsafe { get_uri_fn(uri as *mut std::os::raw::c_void) };
|
||||||
if sip_uri_raw.is_null() {
|
if sip_uri_raw.is_null() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
||||||
if (*sip_uri).host.ptr.is_null() || (*sip_uri).host.slen <= 0 {
|
if unsafe { (*sip_uri).host.ptr.is_null() || (*sip_uri).host.slen <= 0 } {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let host = pj_str_to_string(&(*sip_uri).host);
|
let host = unsafe { pj_str_to_string(&(*sip_uri).host) };
|
||||||
|
|
||||||
// Only rewrite if Contact host is a private (RFC 1918) IP
|
// Only rewrite if Contact host is a private (RFC 1918) IP
|
||||||
let contact_ip: Ipv4Addr = match host.parse() {
|
let contact_ip: Ipv4Addr = match host.parse() {
|
||||||
|
|
@ -418,14 +423,14 @@ unsafe fn rewrite_private_contact_for_external(tdata: *mut pjsip_tx_data, direct
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if destination is also private (local-network rewrite handles that)
|
// Skip if destination is also private (local-network rewrite handles that)
|
||||||
if let Some(dst_ip) = extract_dst_ipv4(tdata) {
|
if let Some(dst_ip) = unsafe { extract_dst_ipv4(tdata) }
|
||||||
if is_rfc1918(dst_ip) {
|
&& is_rfc1918(dst_ip)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite Contact to public host
|
// Rewrite Contact to public host
|
||||||
if rewrite_contact_host((*tdata).pool, msg, public_host, *port) {
|
if unsafe { rewrite_contact_host((*tdata).pool, msg, public_host, *port) } {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Rewrote {} Contact for external client: {} -> {}:{}",
|
"Rewrote {} Contact for external client: {} -> {}:{}",
|
||||||
direction,
|
direction,
|
||||||
|
|
@ -448,16 +453,18 @@ unsafe fn rewrite_private_contact_for_external(tdata: *mut pjsip_tx_data, direct
|
||||||
/// 2. Public-host rewrite: for external clients, replace private Contact IPs
|
/// 2. Public-host rewrite: for external clients, replace private Contact IPs
|
||||||
/// with the public hostname so they can route BYE back to us
|
/// with the public hostname so they can route BYE back to us
|
||||||
pub unsafe extern "C" fn on_tx_response_cb(tdata: *mut pjsip_tx_data) -> pj_status_t {
|
pub unsafe extern "C" fn on_tx_response_cb(tdata: *mut pjsip_tx_data) -> pj_status_t {
|
||||||
let local_rewrite = rewrite_local_network_tdata(tdata, "response");
|
let local_rewrite = unsafe { rewrite_local_network_tdata(tdata, "response") };
|
||||||
let public_rewrite = rewrite_private_contact_for_external(tdata, "response");
|
let public_rewrite = unsafe { rewrite_private_contact_for_external(tdata, "response") };
|
||||||
|
|
||||||
// If we modified headers, the buffer was already serialized by mod-msg-print
|
// If we modified headers, the buffer was already serialized by mod-msg-print
|
||||||
// (priority 8, before our module at priority 32). Invalidate and re-encode
|
// (priority 8, before our module at priority 32). Invalidate and re-encode
|
||||||
// so the changes actually reach the wire.
|
// so the changes actually reach the wire.
|
||||||
if local_rewrite || public_rewrite {
|
if local_rewrite || public_rewrite {
|
||||||
|
unsafe {
|
||||||
pjsip_tx_data_invalidate_msg(tdata);
|
pjsip_tx_data_invalidate_msg(tdata);
|
||||||
pjsip_tx_data_encode(tdata);
|
pjsip_tx_data_encode(tdata);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
}
|
}
|
||||||
|
|
@ -465,16 +472,18 @@ pub unsafe extern "C" fn on_tx_response_cb(tdata: *mut pjsip_tx_data) -> pj_stat
|
||||||
/// Callback to rewrite Contact header and SDP body in outgoing requests.
|
/// Callback to rewrite Contact header and SDP body in outgoing requests.
|
||||||
/// Same dual-rewrite logic as the response path.
|
/// Same dual-rewrite logic as the response path.
|
||||||
pub unsafe extern "C" fn on_tx_request_cb(tdata: *mut pjsip_tx_data) -> pj_status_t {
|
pub unsafe extern "C" fn on_tx_request_cb(tdata: *mut pjsip_tx_data) -> pj_status_t {
|
||||||
let local_rewrite = rewrite_local_network_tdata(tdata, "request");
|
let local_rewrite = unsafe { rewrite_local_network_tdata(tdata, "request") };
|
||||||
let public_rewrite = rewrite_private_contact_for_external(tdata, "request");
|
let public_rewrite = unsafe { rewrite_private_contact_for_external(tdata, "request") };
|
||||||
|
|
||||||
// If we modified headers, the buffer was already serialized by mod-msg-print
|
// If we modified headers, the buffer was already serialized by mod-msg-print
|
||||||
// (priority 8, before our module at priority 32). Invalidate and re-encode
|
// (priority 8, before our module at priority 32). Invalidate and re-encode
|
||||||
// so the changes actually reach the wire.
|
// so the changes actually reach the wire.
|
||||||
if local_rewrite || public_rewrite {
|
if local_rewrite || public_rewrite {
|
||||||
|
unsafe {
|
||||||
pjsip_tx_data_invalidate_msg(tdata);
|
pjsip_tx_data_invalidate_msg(tdata);
|
||||||
pjsip_tx_data_encode(tdata);
|
pjsip_tx_data_encode(tdata);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pj_constants__PJ_SUCCESS as pj_status_t
|
pj_constants__PJ_SUCCESS as pj_status_t
|
||||||
}
|
}
|
||||||
|
|
@ -492,38 +501,37 @@ pub unsafe extern "C" fn on_rx_request_nat_fixup_cb(rdata: *mut pjsip_rx_data) -
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = (*rdata).msg_info.msg;
|
let msg = unsafe { (*rdata).msg_info.msg };
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process requests (safety check)
|
// Only process requests (safety check)
|
||||||
if (*msg).type_ != pjsip_msg_type_e_PJSIP_REQUEST_MSG {
|
if unsafe { (*msg).type_ } != pjsip_msg_type_e_PJSIP_REQUEST_MSG {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process INVITE and re-INVITE (they carry SDP with media addresses)
|
// Only process INVITE and re-INVITE (they carry SDP with media addresses)
|
||||||
let method_id = (*msg).line.req.method.id;
|
let method_id = unsafe { (*msg).line.req.method.id };
|
||||||
if method_id != pjsip_method_e_PJSIP_INVITE_METHOD {
|
if method_id != pjsip_method_e_PJSIP_INVITE_METHOD {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's a body (SDP)
|
// Check if there's a body (SDP)
|
||||||
let body = (*msg).body;
|
let body = unsafe { (*msg).body };
|
||||||
if body.is_null() || (*body).len == 0 || (*body).data.is_null() {
|
if body.is_null() || unsafe { (*body).len == 0 || (*body).data.is_null() } {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract source IP from packet info
|
// Extract source IP from packet info
|
||||||
let src_name = &(*rdata).pkt_info.src_name;
|
let src_name = unsafe { &(*rdata).pkt_info.src_name };
|
||||||
let name_len = src_name
|
let name_len = src_name
|
||||||
.iter()
|
.iter()
|
||||||
.position(|&c| c == 0)
|
.position(|&c| c == 0)
|
||||||
.unwrap_or(src_name.len());
|
.unwrap_or(src_name.len());
|
||||||
let src_ip_str = match std::str::from_utf8(std::slice::from_raw_parts(
|
let src_ip_str = match std::str::from_utf8(unsafe {
|
||||||
src_name.as_ptr() as *const u8,
|
std::slice::from_raw_parts(src_name.as_ptr() as *const u8, name_len)
|
||||||
name_len,
|
}) {
|
||||||
)) {
|
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
||||||
};
|
};
|
||||||
|
|
@ -533,70 +541,74 @@ pub unsafe extern "C" fn on_rx_request_nat_fixup_cb(rdata: *mut pjsip_rx_data) -
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse SDP to find c= line IP and check if it's a private address
|
// Parse SDP to find c= line IP and check if it's a private address
|
||||||
let body_slice = std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize);
|
let body_slice =
|
||||||
|
unsafe { std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize) };
|
||||||
let sdp_str = match std::str::from_utf8(body_slice) {
|
let sdp_str = match std::str::from_utf8(body_slice) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find any connection address in the SDP that needs NAT fixup.
|
// Find any connection address in the SDP that needs NAT fixup.
|
||||||
// Check ALL c= lines (session-level and per-media) — if any contain a
|
// Check ALL c= lines (session-level and per-media) -- if any contain a
|
||||||
// private IP different from the packet source, rewrite the SDP.
|
// private IP different from the packet source, rewrite the SDP.
|
||||||
let mut needs_rewrite = false;
|
let mut needs_rewrite = false;
|
||||||
let mut private_ip_str: Option<&str> = None;
|
let mut private_ip_str: Option<&str> = None;
|
||||||
for line in sdp_str.lines() {
|
for line in sdp_str.lines() {
|
||||||
if let Some(addr_str) = line.strip_prefix("c=IN IP4 ") {
|
if let Some(addr_str) = line.strip_prefix("c=IN IP4 ") {
|
||||||
let addr_str = addr_str.trim();
|
let addr_str = addr_str.trim();
|
||||||
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>() {
|
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>()
|
||||||
if is_rfc1918(sdp_ip) && sdp_ip != src_ip {
|
&& is_rfc1918(sdp_ip)
|
||||||
|
&& sdp_ip != src_ip
|
||||||
|
{
|
||||||
needs_rewrite = true;
|
needs_rewrite = true;
|
||||||
private_ip_str = Some(addr_str);
|
private_ip_str = Some(addr_str);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if needs_rewrite {
|
if needs_rewrite && let Some(private_ip) = private_ip_str {
|
||||||
if let Some(private_ip) = private_ip_str {
|
let pool = unsafe { (*rdata).tp_info.pool };
|
||||||
let pool = (*rdata).tp_info.pool;
|
if !pool.is_null() && unsafe { rewrite_sdp_body(pool, msg, private_ip, src_ip_str) } {
|
||||||
if !pool.is_null() {
|
|
||||||
if rewrite_sdp_body(pool, msg, private_ip, src_ip_str) {
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"NAT fixup (INVITE): SDP rewritten {} -> {} (from {}:{})",
|
"NAT fixup (INVITE): SDP rewritten {} -> {} (from {}:{})",
|
||||||
private_ip,
|
private_ip,
|
||||||
src_ip_str,
|
src_ip_str,
|
||||||
src_ip_str,
|
src_ip_str,
|
||||||
(*rdata).pkt_info.src_port
|
unsafe { (*rdata).pkt_info.src_port }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also rewrite Contact header if present and has private IP
|
// Also rewrite Contact header if present and has private IP
|
||||||
let contact_hdr = pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut())
|
let contact_hdr =
|
||||||
|
unsafe { pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut()) }
|
||||||
as *mut pjsip_contact_hdr;
|
as *mut pjsip_contact_hdr;
|
||||||
if !contact_hdr.is_null() {
|
if !contact_hdr.is_null() {
|
||||||
let uri = (*contact_hdr).uri;
|
let uri = unsafe { (*contact_hdr).uri };
|
||||||
if !uri.is_null() {
|
if !uri.is_null() {
|
||||||
let uri_vptr = (*(uri as *const pjsip_uri)).vptr;
|
let uri_vptr = unsafe { (*(uri as *const pjsip_uri)).vptr };
|
||||||
if !uri_vptr.is_null() {
|
if !uri_vptr.is_null()
|
||||||
if let Some(get_uri_fn) = (*uri_vptr).p_get_uri {
|
&& let Some(get_uri_fn) = unsafe { (*uri_vptr).p_get_uri }
|
||||||
let sip_uri_raw = get_uri_fn(uri as *mut std::os::raw::c_void);
|
{
|
||||||
|
let sip_uri_raw = unsafe { get_uri_fn(uri as *mut std::os::raw::c_void) };
|
||||||
if !sip_uri_raw.is_null() {
|
if !sip_uri_raw.is_null() {
|
||||||
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
||||||
let contact_host = pj_str_to_string(&(*sip_uri).host);
|
let contact_host = unsafe { pj_str_to_string(&(*sip_uri).host) };
|
||||||
if let Ok(contact_ip) = contact_host.parse::<Ipv4Addr>() {
|
if let Ok(contact_ip) = contact_host.parse::<Ipv4Addr>()
|
||||||
if is_rfc1918(contact_ip) && contact_ip != src_ip {
|
&& is_rfc1918(contact_ip)
|
||||||
let src_port = (*rdata).pkt_info.src_port as u16;
|
&& contact_ip != src_ip
|
||||||
let pool = (*rdata).tp_info.pool;
|
{
|
||||||
if !pool.is_null() {
|
let src_port = unsafe { (*rdata).pkt_info.src_port } as u16;
|
||||||
if let Ok(new_host_cstr) = CString::new(src_ip_str) {
|
let pool = unsafe { (*rdata).tp_info.pool };
|
||||||
|
if !pool.is_null()
|
||||||
|
&& let Ok(new_host_cstr) = CString::new(src_ip_str)
|
||||||
|
{
|
||||||
let host_len = src_ip_str.len();
|
let host_len = src_ip_str.len();
|
||||||
let pool_str =
|
let pool_str =
|
||||||
pj_pool_alloc(pool, host_len + 1) as *mut c_char;
|
unsafe { pj_pool_alloc(pool, host_len + 1) } as *mut c_char;
|
||||||
if !pool_str.is_null() {
|
if !pool_str.is_null() {
|
||||||
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(
|
ptr::copy_nonoverlapping(
|
||||||
new_host_cstr.as_ptr(),
|
new_host_cstr.as_ptr(),
|
||||||
pool_str,
|
pool_str,
|
||||||
|
|
@ -605,6 +617,7 @@ pub unsafe extern "C" fn on_rx_request_nat_fixup_cb(rdata: *mut pjsip_rx_data) -
|
||||||
(*sip_uri).host.ptr = pool_str;
|
(*sip_uri).host.ptr = pool_str;
|
||||||
(*sip_uri).host.slen = host_len as i64;
|
(*sip_uri).host.slen = host_len as i64;
|
||||||
(*sip_uri).port = src_port as i32;
|
(*sip_uri).port = src_port as i32;
|
||||||
|
}
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"NAT fixup (INVITE): Contact rewritten {} -> {}:{}",
|
"NAT fixup (INVITE): Contact rewritten {} -> {}:{}",
|
||||||
contact_host,
|
contact_host,
|
||||||
|
|
@ -618,36 +631,35 @@ pub unsafe extern "C" fn on_rx_request_nat_fixup_cb(rdata: *mut pjsip_rx_data) -
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize SDP: strip dynamic payload types (96+) that lack a=rtpmap attributes.
|
// Sanitize SDP: strip dynamic payload types (96+) that lack a=rtpmap attributes.
|
||||||
// Without this, PJSIP's SDP validator rejects these INVITEs with EMISSINGRTPMAP.
|
// Without this, PJSIP's SDP validator rejects these INVITEs with EMISSINGRTPMAP.
|
||||||
let body = (*msg).body;
|
let body = unsafe { (*msg).body };
|
||||||
if !body.is_null() && (*body).len > 0 && !(*body).data.is_null() {
|
if !body.is_null() && unsafe { (*body).len > 0 && !(*body).data.is_null() } {
|
||||||
let body_slice =
|
let body_slice =
|
||||||
std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize);
|
unsafe { std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize) };
|
||||||
if let Ok(sdp_str) = std::str::from_utf8(body_slice) {
|
if let Ok(sdp_str) = std::str::from_utf8(body_slice)
|
||||||
if let Some(sanitized) = sanitize_sdp_missing_rtpmap(sdp_str) {
|
&& let Some(sanitized) = sanitize_sdp_missing_rtpmap(sdp_str)
|
||||||
let pool = (*rdata).tp_info.pool;
|
{
|
||||||
|
let pool = unsafe { (*rdata).tp_info.pool };
|
||||||
if !pool.is_null() {
|
if !pool.is_null() {
|
||||||
let new_len = sanitized.len();
|
let new_len = sanitized.len();
|
||||||
let new_body_ptr = pj_pool_alloc(pool, new_len) as *mut u8;
|
let new_body_ptr = unsafe { pj_pool_alloc(pool, new_len) } as *mut u8;
|
||||||
if !new_body_ptr.is_null() {
|
if !new_body_ptr.is_null() {
|
||||||
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(sanitized.as_ptr(), new_body_ptr, new_len);
|
ptr::copy_nonoverlapping(sanitized.as_ptr(), new_body_ptr, new_len);
|
||||||
(*body).data = new_body_ptr as *mut _;
|
(*body).data = new_body_ptr as *mut _;
|
||||||
(*body).len = new_len as u32;
|
(*body).len = new_len as u32;
|
||||||
|
}
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"SDP sanitized: stripped orphan dynamic payload types (from {}:{})",
|
"SDP sanitized: stripped orphan dynamic payload types (from {}:{})",
|
||||||
src_ip_str,
|
src_ip_str,
|
||||||
(*rdata).pkt_info.src_port
|
unsafe { (*rdata).pkt_info.src_port }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pj_constants__PJ_FALSE as pj_bool_t
|
pj_constants__PJ_FALSE as pj_bool_t
|
||||||
}
|
}
|
||||||
|
|
@ -670,27 +682,26 @@ pub unsafe extern "C" fn on_rx_response_nat_fixup_cb(rdata: *mut pjsip_rx_data)
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = (*rdata).msg_info.msg;
|
let msg = unsafe { (*rdata).msg_info.msg };
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process 1xx/2xx responses (provisional and success)
|
// Only process 1xx/2xx responses (provisional and success)
|
||||||
let status_code = (*msg).line.status.code;
|
let status_code = unsafe { (*msg).line.status.code };
|
||||||
if !(100..300).contains(&status_code) {
|
if !(100..300).contains(&status_code) {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract source IP from pkt_info.src_name (null-terminated char array)
|
// Extract source IP from pkt_info.src_name (null-terminated char array)
|
||||||
let src_name = &(*rdata).pkt_info.src_name;
|
let src_name = unsafe { &(*rdata).pkt_info.src_name };
|
||||||
let name_len = src_name
|
let name_len = src_name
|
||||||
.iter()
|
.iter()
|
||||||
.position(|&c| c == 0)
|
.position(|&c| c == 0)
|
||||||
.unwrap_or(src_name.len());
|
.unwrap_or(src_name.len());
|
||||||
let src_ip_str = match std::str::from_utf8(std::slice::from_raw_parts(
|
let src_ip_str = match std::str::from_utf8(unsafe {
|
||||||
src_name.as_ptr() as *const u8,
|
std::slice::from_raw_parts(src_name.as_ptr() as *const u8, name_len)
|
||||||
name_len,
|
}) {
|
||||||
)) {
|
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
||||||
};
|
};
|
||||||
|
|
@ -698,10 +709,11 @@ pub unsafe extern "C" fn on_rx_response_nat_fixup_cb(rdata: *mut pjsip_rx_data)
|
||||||
Ok(ip) => ip,
|
Ok(ip) => ip,
|
||||||
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t, // IPv6 or invalid
|
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t, // IPv6 or invalid
|
||||||
};
|
};
|
||||||
let src_port = (*rdata).pkt_info.src_port as u16;
|
let src_port = unsafe { (*rdata).pkt_info.src_port } as u16;
|
||||||
|
|
||||||
// Find Contact header in the response
|
// Find Contact header in the response
|
||||||
let contact_hdr = pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut())
|
let contact_hdr =
|
||||||
|
unsafe { pjsip_msg_find_hdr(msg, pjsip_hdr_e_PJSIP_H_CONTACT, ptr::null_mut()) }
|
||||||
as *mut pjsip_contact_hdr;
|
as *mut pjsip_contact_hdr;
|
||||||
if contact_hdr.is_null() {
|
if contact_hdr.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
|
|
@ -711,26 +723,26 @@ pub unsafe extern "C" fn on_rx_response_nat_fixup_cb(rdata: *mut pjsip_rx_data)
|
||||||
// The rx path requires vtable-based URI unwrapping (p_get_uri) because
|
// The rx path requires vtable-based URI unwrapping (p_get_uri) because
|
||||||
// the Contact URI may be wrapped in a pjsip_name_addr, unlike the tx
|
// the Contact URI may be wrapped in a pjsip_name_addr, unlike the tx
|
||||||
// path where we can cast directly.
|
// path where we can cast directly.
|
||||||
let uri = (*contact_hdr).uri;
|
let uri = unsafe { (*contact_hdr).uri };
|
||||||
if uri.is_null() {
|
if uri.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
let uri_vptr = (*(uri as *const pjsip_uri)).vptr;
|
let uri_vptr = unsafe { (*(uri as *const pjsip_uri)).vptr };
|
||||||
if uri_vptr.is_null() {
|
if uri_vptr.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
let get_uri_fn = match (*uri_vptr).p_get_uri {
|
let get_uri_fn = match unsafe { (*uri_vptr).p_get_uri } {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
None => return pj_constants__PJ_FALSE as pj_bool_t,
|
None => return pj_constants__PJ_FALSE as pj_bool_t,
|
||||||
};
|
};
|
||||||
let sip_uri_raw = get_uri_fn(uri as *mut std::os::raw::c_void);
|
let sip_uri_raw = unsafe { get_uri_fn(uri as *mut std::os::raw::c_void) };
|
||||||
if sip_uri_raw.is_null() {
|
if sip_uri_raw.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
||||||
|
|
||||||
// Parse Contact host as IPv4
|
// Parse Contact host as IPv4
|
||||||
let contact_host = pj_str_to_string(&(*sip_uri).host);
|
let contact_host = unsafe { pj_str_to_string(&(*sip_uri).host) };
|
||||||
let contact_ip: Ipv4Addr = match contact_host.parse() {
|
let contact_ip: Ipv4Addr = match contact_host.parse() {
|
||||||
Ok(ip) => ip,
|
Ok(ip) => ip,
|
||||||
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t, // Hostname, skip
|
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t, // Hostname, skip
|
||||||
|
|
@ -753,12 +765,14 @@ pub unsafe extern "C" fn on_rx_response_nat_fixup_cb(rdata: *mut pjsip_rx_data)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rewrite Contact URI host to the public source IP
|
// Rewrite Contact URI host to the public source IP
|
||||||
let pool = (*rdata).tp_info.pool;
|
let pool = unsafe { (*rdata).tp_info.pool };
|
||||||
if !pool.is_null() {
|
if !pool.is_null()
|
||||||
if let Ok(new_host_cstr) = CString::new(src_ip_str) {
|
&& let Ok(new_host_cstr) = CString::new(src_ip_str)
|
||||||
|
{
|
||||||
let host_len = src_ip_str.len();
|
let host_len = src_ip_str.len();
|
||||||
let pool_str = pj_pool_alloc(pool, host_len + 1) as *mut c_char;
|
let pool_str = unsafe { pj_pool_alloc(pool, host_len + 1) } as *mut c_char;
|
||||||
if !pool_str.is_null() {
|
if !pool_str.is_null() {
|
||||||
|
unsafe {
|
||||||
ptr::copy_nonoverlapping(new_host_cstr.as_ptr(), pool_str, host_len + 1);
|
ptr::copy_nonoverlapping(new_host_cstr.as_ptr(), pool_str, host_len + 1);
|
||||||
(*sip_uri).host.ptr = pool_str;
|
(*sip_uri).host.ptr = pool_str;
|
||||||
(*sip_uri).host.slen = host_len as i64;
|
(*sip_uri).host.slen = host_len as i64;
|
||||||
|
|
@ -768,19 +782,21 @@ pub unsafe extern "C" fn on_rx_response_nat_fixup_cb(rdata: *mut pjsip_rx_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite SDP body: replace private IP with public source IP.
|
// Rewrite SDP body: replace private IP with public source IP.
|
||||||
// Parse the SDP c= line directly to get the actual media IP — it may differ
|
// Parse the SDP c= line directly to get the actual media IP -- it may differ
|
||||||
// from the Contact header IP (e.g., dual-homed phone or double NAT).
|
// from the Contact header IP (e.g., dual-homed phone or double NAT).
|
||||||
let body = (*msg).body;
|
let body = unsafe { (*msg).body };
|
||||||
if !body.is_null() && (*body).len > 0 && !(*body).data.is_null() {
|
if !body.is_null() && unsafe { (*body).len > 0 && !(*body).data.is_null() } {
|
||||||
let body_slice =
|
let body_slice =
|
||||||
std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize);
|
unsafe { std::slice::from_raw_parts((*body).data as *const u8, (*body).len as usize) };
|
||||||
if let Ok(sdp_str) = std::str::from_utf8(body_slice) {
|
if let Ok(sdp_str) = std::str::from_utf8(body_slice) {
|
||||||
for line in sdp_str.lines() {
|
for line in sdp_str.lines() {
|
||||||
if let Some(addr_str) = line.strip_prefix("c=IN IP4 ") {
|
if let Some(addr_str) = line.strip_prefix("c=IN IP4 ") {
|
||||||
let addr_str = addr_str.trim();
|
let addr_str = addr_str.trim();
|
||||||
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>() {
|
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>()
|
||||||
if is_rfc1918(sdp_ip) && sdp_ip != src_ip {
|
&& is_rfc1918(sdp_ip)
|
||||||
if rewrite_sdp_body(pool, msg, addr_str, src_ip_str) {
|
&& sdp_ip != src_ip
|
||||||
|
{
|
||||||
|
if unsafe { rewrite_sdp_body(pool, msg, addr_str, src_ip_str) } {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"NAT fixup: SDP rewritten {} -> {}",
|
"NAT fixup: SDP rewritten {} -> {}",
|
||||||
addr_str,
|
addr_str,
|
||||||
|
|
@ -793,7 +809,6 @@ pub unsafe extern "C" fn on_rx_response_nat_fixup_cb(rdata: *mut pjsip_rx_data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Return FALSE to let other modules also process this response
|
// Return FALSE to let other modules also process this response
|
||||||
pj_constants__PJ_FALSE as pj_bool_t
|
pj_constants__PJ_FALSE as pj_bool_t
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,15 @@ pub fn set_register_module_ptr(ptr: *mut pjsip_module) {
|
||||||
/// Initialize a pjsip_hdr as a list head (equivalent to pj_list_init C macro).
|
/// Initialize a pjsip_hdr as a list head (equivalent to pj_list_init C macro).
|
||||||
#[inline]
|
#[inline]
|
||||||
unsafe fn pj_list_init_hdr(hdr: *mut pjsip_hdr) {
|
unsafe fn pj_list_init_hdr(hdr: *mut pjsip_hdr) {
|
||||||
|
unsafe {
|
||||||
(*hdr).next = hdr as *mut _;
|
(*hdr).next = hdr as *mut _;
|
||||||
(*hdr).prev = hdr as *mut _;
|
(*hdr).prev = hdr as *mut _;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a simple stateless SIP response (no custom headers).
|
/// Send a simple stateless SIP response (no custom headers).
|
||||||
unsafe fn send_simple_response(rdata: *mut pjsip_rx_data, status_code: u16, reason: &str) {
|
unsafe fn send_simple_response(rdata: *mut pjsip_rx_data, status_code: u16, reason: &str) {
|
||||||
|
unsafe {
|
||||||
let endpt = pjsua_get_pjsip_endpt();
|
let endpt = pjsua_get_pjsip_endpt();
|
||||||
if !endpt.is_null() {
|
if !endpt.is_null() {
|
||||||
let reason_cstr = CString::new(reason).unwrap();
|
let reason_cstr = CString::new(reason).unwrap();
|
||||||
|
|
@ -91,10 +94,12 @@ unsafe fn send_simple_response(rdata: *mut pjsip_rx_data, status_code: u16, reas
|
||||||
ptr::null(),
|
ptr::null(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a stateless 200 OK with an Expires header.
|
/// Send a stateless 200 OK with an Expires header.
|
||||||
unsafe fn send_register_ok(rdata: *mut pjsip_rx_data, expires: u32) {
|
unsafe fn send_register_ok(rdata: *mut pjsip_rx_data, expires: u32) {
|
||||||
|
unsafe {
|
||||||
let endpt = pjsua_get_pjsip_endpt();
|
let endpt = pjsua_get_pjsip_endpt();
|
||||||
if endpt.is_null() {
|
if endpt.is_null() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -111,7 +116,8 @@ unsafe fn send_register_ok(rdata: *mut pjsip_rx_data, expires: u32) {
|
||||||
let hdr = pjsip_generic_string_hdr_create(pool, &name, &value);
|
let hdr = pjsip_generic_string_hdr_create(pool, &name, &value);
|
||||||
|
|
||||||
if !hdr.is_null() {
|
if !hdr.is_null() {
|
||||||
let hdr_list = pj_pool_alloc(pool, std::mem::size_of::<pjsip_hdr>()) as *mut pjsip_hdr;
|
let hdr_list =
|
||||||
|
pj_pool_alloc(pool, std::mem::size_of::<pjsip_hdr>()) as *mut pjsip_hdr;
|
||||||
if !hdr_list.is_null() {
|
if !hdr_list.is_null() {
|
||||||
pj_list_init_hdr(hdr_list);
|
pj_list_init_hdr(hdr_list);
|
||||||
pj_list_insert_before(hdr_list as *mut pj_list_type, hdr as *mut pj_list_type);
|
pj_list_insert_before(hdr_list as *mut pj_list_type, hdr as *mut pj_list_type);
|
||||||
|
|
@ -143,10 +149,12 @@ unsafe fn send_register_ok(rdata: *mut pjsip_rx_data, expires: u32) {
|
||||||
if status != pj_constants__PJ_SUCCESS as i32 {
|
if status != pj_constants__PJ_SUCCESS as i32 {
|
||||||
tracing::warn!("Failed to respond 200 OK to REGISTER: {}", status);
|
tracing::warn!("Failed to respond 200 OK to REGISTER: {}", status);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect transport type (UDP/TCP/TLS) from the incoming request.
|
/// Detect transport type (UDP/TCP/TLS) from the incoming request.
|
||||||
unsafe fn detect_transport(rdata: *mut pjsip_rx_data) -> crate::services::registrar::SipTransport {
|
unsafe fn detect_transport(rdata: *mut pjsip_rx_data) -> crate::services::registrar::SipTransport {
|
||||||
|
unsafe {
|
||||||
if !(*rdata).tp_info.transport.is_null() {
|
if !(*rdata).tp_info.transport.is_null() {
|
||||||
let tp_type = (*(*rdata).tp_info.transport).key.type_ as u32;
|
let tp_type = (*(*rdata).tp_info.transport).key.type_ as u32;
|
||||||
if tp_type == pjsip_transport_type_e_PJSIP_TRANSPORT_TLS {
|
if tp_type == pjsip_transport_type_e_PJSIP_TRANSPORT_TLS {
|
||||||
|
|
@ -159,6 +167,7 @@ unsafe fn detect_transport(rdata: *mut pjsip_rx_data) -> crate::services::regist
|
||||||
} else {
|
} else {
|
||||||
crate::services::registrar::SipTransport::Udp
|
crate::services::registrar::SipTransport::Udp
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a UAS transaction + pre-built response tdata for deferred REGISTER
|
/// Create a UAS transaction + pre-built response tdata for deferred REGISTER
|
||||||
|
|
@ -168,6 +177,7 @@ unsafe fn create_register_tsx(
|
||||||
rdata: *mut pjsip_rx_data,
|
rdata: *mut pjsip_rx_data,
|
||||||
expires: u32,
|
expires: u32,
|
||||||
) -> Option<PendingRegisterTsx> {
|
) -> Option<PendingRegisterTsx> {
|
||||||
|
unsafe {
|
||||||
let endpt = pjsua_get_pjsip_endpt();
|
let endpt = pjsua_get_pjsip_endpt();
|
||||||
let module_ptr = REGISTER_MODULE_PTR.load(Ordering::Acquire);
|
let module_ptr = REGISTER_MODULE_PTR.load(Ordering::Acquire);
|
||||||
|
|
||||||
|
|
@ -199,6 +209,7 @@ unsafe fn create_register_tsx(
|
||||||
tdata: SendableTdata(tdata),
|
tdata: SendableTdata(tdata),
|
||||||
expires,
|
expires,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main callback
|
// Main callback
|
||||||
|
|
@ -216,6 +227,7 @@ unsafe fn create_register_tsx(
|
||||||
/// b. Cache hit + mismatch -> immediate 403 Forbidden (stateless)
|
/// b. Cache hit + mismatch -> immediate 403 Forbidden (stateless)
|
||||||
/// c. Cache miss -> defer via UAS transaction, verify via API, respond later
|
/// c. Cache miss -> defer via UAS transaction, verify via API, respond later
|
||||||
pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_t {
|
pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_t {
|
||||||
|
unsafe {
|
||||||
if rdata.is_null() {
|
if rdata.is_null() {
|
||||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||||
}
|
}
|
||||||
|
|
@ -242,9 +254,11 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
let source_port = (*rdata).pkt_info.src_port as u16;
|
let source_port = (*rdata).pkt_info.src_port as u16;
|
||||||
|
|
||||||
// Ban checks: skip if banning disabled or IP is whitelisted
|
// Ban checks: skip if banning disabled or IP is whitelisted
|
||||||
if let Some(ip) = source_ip {
|
if let Some(ip) = source_ip
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
&& let Some(ban_mgr) = crate::services::ban::global()
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
&& ban_mgr.is_enabled()
|
||||||
|
&& !ban_mgr.is_whitelisted(&ip)
|
||||||
|
{
|
||||||
// Check if IP is banned
|
// Check if IP is banned
|
||||||
let result = ban_mgr.check_banned(&ip);
|
let result = ban_mgr.check_banned(&ip);
|
||||||
if result.is_banned {
|
if result.is_banned {
|
||||||
|
|
@ -253,17 +267,17 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check User-Agent for SIPVicious scanners - instant permaban
|
// Check User-Agent for SIPVicious scanners - instant permaban
|
||||||
if let Some(user_agent) = extract_user_agent(rdata) {
|
if let Some(user_agent) = extract_user_agent(rdata)
|
||||||
if is_sipvicious_scanner(&user_agent) {
|
&& is_sipvicious_scanner(&user_agent)
|
||||||
|
{
|
||||||
if let Some(ip) = source_ip {
|
if let Some(ip) = source_ip {
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
if let Some(ban_mgr) = crate::services::ban::global()
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
&& ban_mgr.is_enabled()
|
||||||
let result =
|
&& !ban_mgr.is_whitelisted(&ip)
|
||||||
ban_mgr.record_permanent_ban(ip, "sipvicious_scanner_register");
|
{
|
||||||
|
let result = ban_mgr.record_permanent_ban(ip, "sipvicious_scanner_register");
|
||||||
if result.should_log {
|
if result.should_log {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"PERMABAN IP {} - SIPVicious scanner detected in REGISTER: User-Agent='{}'",
|
"PERMABAN IP {} - SIPVicious scanner detected in REGISTER: User-Agent='{}'",
|
||||||
|
|
@ -272,7 +286,6 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"SIPVicious scanner detected in REGISTER but no IP available: User-Agent='{}'",
|
"SIPVicious scanner detected in REGISTER but no IP available: User-Agent='{}'",
|
||||||
|
|
@ -282,18 +295,18 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
send_simple_response(rdata, 403, "Forbidden");
|
send_simple_response(rdata, 403, "Forbidden");
|
||||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limit REGISTER requests
|
// Rate limit REGISTER requests
|
||||||
if let Some(ip) = source_ip {
|
if let Some(ip) = source_ip
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
&& let Some(ban_mgr) = crate::services::ban::global()
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) && ban_mgr.record_register(ip) {
|
&& ban_mgr.is_enabled()
|
||||||
|
&& !ban_mgr.is_whitelisted(&ip)
|
||||||
|
&& ban_mgr.record_register(ip)
|
||||||
|
{
|
||||||
tracing::debug!("Rejecting REGISTER from {} - rate limit exceeded", ip);
|
tracing::debug!("Rejecting REGISTER from {} - rate limit exceeded", ip);
|
||||||
send_simple_response(rdata, 429, "Too Many Requests");
|
send_simple_response(rdata, 429, "Too Many Requests");
|
||||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to extract Digest auth params from Authorization header
|
// Try to extract Digest auth params from Authorization header
|
||||||
let digest_params = extract_digest_auth_from_rdata(rdata);
|
let digest_params = extract_digest_auth_from_rdata(rdata);
|
||||||
|
|
@ -303,8 +316,9 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
params.method = "REGISTER".to_string();
|
params.method = "REGISTER".to_string();
|
||||||
|
|
||||||
// Check auth failure cooldown before processing
|
// Check auth failure cooldown before processing
|
||||||
if let Some(cache) = crate::services::auth_cache::AuthCache::global() {
|
if let Some(cache) = crate::services::auth_cache::AuthCache::global()
|
||||||
if cache.is_in_cooldown(¶ms.username) {
|
&& cache.is_in_cooldown(¶ms.username)
|
||||||
|
{
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Rejecting REGISTER from {} (user={}) - auth cooldown active",
|
"Rejecting REGISTER from {} (user={}) - auth cooldown active",
|
||||||
ip_str,
|
ip_str,
|
||||||
|
|
@ -313,7 +327,6 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
send_simple_response(rdata, 429, "Too Many Requests");
|
send_simple_response(rdata, 429, "Too Many Requests");
|
||||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Extract fields needed for all code paths
|
// Extract fields needed for all code paths
|
||||||
let contact_uri = extract_contact_uri(rdata);
|
let contact_uri = extract_contact_uri(rdata);
|
||||||
|
|
@ -459,7 +472,8 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
let hdr = pjsip_generic_string_hdr_create(pool, &name, &value);
|
let hdr = pjsip_generic_string_hdr_create(pool, &name, &value);
|
||||||
|
|
||||||
if !hdr.is_null() {
|
if !hdr.is_null() {
|
||||||
let hdr_list = pj_pool_alloc(pool, std::mem::size_of::<pjsip_hdr>()) as *mut pjsip_hdr;
|
let hdr_list =
|
||||||
|
pj_pool_alloc(pool, std::mem::size_of::<pjsip_hdr>()) as *mut pjsip_hdr;
|
||||||
if !hdr_list.is_null() {
|
if !hdr_list.is_null() {
|
||||||
pj_list_init_hdr(hdr_list);
|
pj_list_init_hdr(hdr_list);
|
||||||
pj_list_insert_before(hdr_list as *mut pj_list_type, hdr as *mut pj_list_type);
|
pj_list_insert_before(hdr_list as *mut pj_list_type, hdr as *mut pj_list_type);
|
||||||
|
|
@ -484,6 +498,7 @@ pub unsafe extern "C" fn on_rx_request_cb(rdata: *mut pjsip_rx_data) -> pj_bool_
|
||||||
|
|
||||||
// Return TRUE to indicate we handled this request
|
// Return TRUE to indicate we handled this request
|
||||||
pj_constants__PJ_TRUE as pj_bool_t
|
pj_constants__PJ_TRUE as pj_bool_t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extraction helpers
|
// Extraction helpers
|
||||||
|
|
@ -494,6 +509,7 @@ unsafe fn extract_contact_uri(rdata: *mut pjsip_rx_data) -> Option<String> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
let msg = (*rdata).msg_info.msg;
|
let msg = (*rdata).msg_info.msg;
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -544,6 +560,7 @@ unsafe fn extract_contact_uri(rdata: *mut pjsip_rx_data) -> Option<String> {
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(uri_str)
|
Some(uri_str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract Expires value from REGISTER request (header or Contact param)
|
/// Extract Expires value from REGISTER request (header or Contact param)
|
||||||
|
|
@ -552,6 +569,7 @@ unsafe fn extract_expires(rdata: *mut pjsip_rx_data) -> u32 {
|
||||||
return 3600;
|
return 3600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
let msg = (*rdata).msg_info.msg;
|
let msg = (*rdata).msg_info.msg;
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
return 3600;
|
return 3600;
|
||||||
|
|
@ -567,6 +585,7 @@ unsafe fn extract_expires(rdata: *mut pjsip_rx_data) -> u32 {
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
3600
|
3600
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue