mirror of
https://github.com/coral/sipcord-bridge.git
synced 2026-04-12 12:32:32 -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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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 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).
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
|
|
@ -214,7 +214,7 @@ fn main() {
|
|||
}
|
||||
|
||||
/// 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 pjproject_src = manifest_dir.join("pjproject");
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ use tokio_util::sync::CancellationToken;
|
|||
use tracing::{debug, error, info, trace, warn};
|
||||
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).
|
||||
/// 3200 samples = 200ms of audio, enough for timing jitter.
|
||||
const DISCORD_TO_SIP_RING_BUFFER_SIZE: usize = 3200;
|
||||
|
|
@ -107,7 +110,7 @@ struct BridgeContext {
|
|||
sip_calls: Arc<DashMap<CallId, SipCallInfo>>,
|
||||
/// Active fax sessions keyed by SIP call ID.
|
||||
/// 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>,
|
||||
sip_cmd_tx: Sender<SipCommand>,
|
||||
sound_manager: Arc<SoundManager>,
|
||||
|
|
@ -127,7 +130,7 @@ pub struct BridgeCoordinator {
|
|||
sip_calls: Arc<DashMap<CallId, SipCallInfo>>,
|
||||
/// Active fax sessions keyed by SIP call ID.
|
||||
/// 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.
|
||||
/// Entries are cleaned on answer/fail and periodically swept for stale entries.
|
||||
outbound_requests: Arc<DashMap<String, OutboundCallRequest>>,
|
||||
|
|
@ -221,8 +224,9 @@ impl BridgeCoordinator {
|
|||
);
|
||||
|
||||
// Check for config-based extension sounds (easter eggs)
|
||||
if let Ok(ext_num) = extension.parse::<u32>() {
|
||||
if let Some(sound_name) = sound_manager.get_extension_sound(ext_num) {
|
||||
if let Ok(ext_num) = extension.parse::<u32>()
|
||||
&& let Some(sound_name) = sound_manager.get_extension_sound(ext_num)
|
||||
{
|
||||
info!(
|
||||
"Extension {} maps to sound '{}' (call {})",
|
||||
ext_num, sound_name, call_id
|
||||
|
|
@ -243,7 +247,6 @@ impl BridgeCoordinator {
|
|||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Track this call
|
||||
sip_calls.insert(
|
||||
|
|
@ -325,8 +328,9 @@ impl BridgeCoordinator {
|
|||
backend.on_call_ended(&sip_call_id_str).await;
|
||||
});
|
||||
|
||||
if let Some((_, call_info)) = sip_calls.remove(&call_id) {
|
||||
if let Some(channel_id) = call_info.channel_id {
|
||||
if let Some((_, call_info)) = sip_calls.remove(&call_id)
|
||||
&& let Some(channel_id) = call_info.channel_id
|
||||
{
|
||||
let should_destroy = {
|
||||
if let Some(mut bridge) = bridges.get_mut(&channel_id) {
|
||||
bridge.sip_calls.remove(&call_id);
|
||||
|
|
@ -356,7 +360,6 @@ impl BridgeCoordinator {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SipEvent::CallTimeout { call_id, rx_count } => {
|
||||
warn!(
|
||||
|
|
@ -366,17 +369,16 @@ impl BridgeCoordinator {
|
|||
|
||||
// If no audio was ever received, report no_audio to the coordinator
|
||||
// so the Discord embed can show a diagnostic message
|
||||
if rx_count == 0 {
|
||||
if let Some(call_info) = sip_calls.get(&call_id) {
|
||||
if let Some(ref tracking_id) = call_info.tracking_id {
|
||||
if rx_count == 0
|
||||
&& let Some(call_info) = sip_calls.get(&call_id)
|
||||
&& let Some(ref tracking_id) = call_info.tracking_id
|
||||
{
|
||||
info!(
|
||||
"Call {} had zero RTP packets, reporting no_audio (tracking_id={})",
|
||||
call_id, tracking_id
|
||||
);
|
||||
backend_for_sip.report_call_status(tracking_id, "no_audio");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if sound_manager.is_streaming(sound_name) {
|
||||
if let Some(config) = sound_manager.get_streaming(sound_name) {
|
||||
if sound_manager.is_streaming(sound_name)
|
||||
&& let Some(config) = sound_manager.get_streaming(sound_name)
|
||||
{
|
||||
info!(
|
||||
"Starting streaming playback '{}' from {} for call {}",
|
||||
sound_name,
|
||||
|
|
@ -1641,7 +1644,6 @@ async fn play_extension_sound_and_hangup(
|
|||
// or when the call ends (detected via CALL_CONF_PORTS check)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Preloaded sound - play all at once
|
||||
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 to_write = tx_generated.min(tx_available);
|
||||
if to_write > 0 {
|
||||
if let Ok(mut chunk) = tx_producer.write_chunk(to_write) {
|
||||
if to_write > 0
|
||||
&& let Ok(mut chunk) = tx_producer.write_chunk(to_write)
|
||||
{
|
||||
let (first, second) = chunk.as_mut_slices();
|
||||
let first_len = first.len().min(to_write);
|
||||
first[..first_len].copy_from_slice(&tx_buf[..first_len]);
|
||||
if first_len < to_write {
|
||||
second[..to_write - first_len]
|
||||
.copy_from_slice(&tx_buf[first_len..to_write]);
|
||||
second[..to_write - first_len].copy_from_slice(&tx_buf[first_len..to_write]);
|
||||
}
|
||||
chunk.commit_all();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
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(mut consumer) = consumer_entry.try_lock() {
|
||||
if let Some(consumer_entry) = get_fax_tx_consumers().get(&call_id_ldata)
|
||||
&& let Some(mut consumer) = consumer_entry.try_lock()
|
||||
{
|
||||
let available = consumer.slots();
|
||||
if available >= SAMPLES_PER_FRAME {
|
||||
if let Ok(chunk) = consumer.read_chunk(SAMPLES_PER_FRAME) {
|
||||
if available >= SAMPLES_PER_FRAME
|
||||
&& let Ok(chunk) = consumer.read_chunk(SAMPLES_PER_FRAME)
|
||||
{
|
||||
let (first, second) = chunk.as_slices();
|
||||
let out = unsafe {
|
||||
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);
|
||||
if !second.is_empty() {
|
||||
out[first.len()..first.len() + second.len()].copy_from_slice(second);
|
||||
}
|
||||
chunk.commit_all();
|
||||
unsafe {
|
||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||
(*frame).size = SAMPLES_PER_FRAME * 2;
|
||||
}
|
||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No TX audio available — return silence audio frame (not NONE).
|
||||
// Returning FRAME_TYPE_NONE can cause PJSIP's conference bridge to
|
||||
// exclude this port from the audio mix, breaking the TX path.
|
||||
unsafe {
|
||||
let buf = (*frame).buf as *mut i16;
|
||||
let out = std::slice::from_raw_parts_mut(buf, SAMPLES_PER_FRAME);
|
||||
out.fill(0);
|
||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||
(*frame).size = SAMPLES_PER_FRAME * 2;
|
||||
}
|
||||
pj_constants__PJ_SUCCESS as pj_status_t
|
||||
}
|
||||
|
||||
/// on_destroy callback — no-op since cleanup is done in remove_fax_audio_port().
|
||||
/// 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 {
|
||||
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.
|
||||
|
|
@ -306,20 +312,25 @@ unsafe extern "C" fn fax_port_put_frame(
|
|||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
let samples = unsafe {
|
||||
let num_samples = (*frame).size / 2;
|
||||
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
|
||||
if let Some(producer_entry) = get_fax_rx_producers().get(&call_id_ldata) {
|
||||
if let Some(mut producer) = producer_entry.try_lock() {
|
||||
if let Some(producer_entry) = get_fax_rx_producers().get(&call_id_ldata)
|
||||
&& let Some(mut producer) = producer_entry.try_lock()
|
||||
{
|
||||
let available = producer.slots();
|
||||
if available >= 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ unsafe fn configure_log_state(log_state: *mut spandsp_sys::logging_state_t) {
|
|||
return;
|
||||
}
|
||||
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_message_handler(
|
||||
log_state,
|
||||
|
|
@ -122,6 +123,7 @@ unsafe fn configure_log_state(log_state: *mut spandsp_sys::logging_state_t) {
|
|||
std::ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check fax reception completion status from callback state.
|
||||
fn check_completion(state: &FaxCallbackState) -> FaxRxStatus {
|
||||
|
|
@ -467,8 +469,11 @@ unsafe extern "C" fn tx_packet_handler(
|
|||
if user_data.is_null() || buf.is_null() || len <= 0 {
|
||||
return -1;
|
||||
}
|
||||
let (state, data) = unsafe {
|
||||
let state = &*(user_data as *const TxCallbackState);
|
||||
let data = std::slice::from_raw_parts(buf, len as usize);
|
||||
(state, data)
|
||||
};
|
||||
debug!("SpanDSP TX IFP: {}B (count={})", len, count);
|
||||
// Send the packet `count` times as SpanDSP requests.
|
||||
// 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.
|
||||
unsafe extern "C" fn phase_b_handler(user_data: *mut std::ffi::c_void, result: i32) -> i32 {
|
||||
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;
|
||||
info!(
|
||||
"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.
|
||||
unsafe extern "C" fn phase_d_handler(user_data: *mut std::ffi::c_void, result: i32) -> i32 {
|
||||
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;
|
||||
info!(
|
||||
"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).
|
||||
unsafe extern "C" fn phase_e_handler(user_data: *mut std::ffi::c_void, completion_code: i32) {
|
||||
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.completed = true;
|
||||
|
||||
|
|
@ -550,7 +555,7 @@ unsafe extern "C" fn spandsp_log_handler(
|
|||
if text.is_null() {
|
||||
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
|
||||
|
||||
match level {
|
||||
|
|
|
|||
|
|
@ -941,12 +941,12 @@ fn decode_group4(data: &[u8], width: u32, height: u32) -> Result<Vec<Vec<u16>>>
|
|||
|
||||
for _ in 0..height {
|
||||
// Check for EOFB (End Of Facsimile Block): two consecutive EOL codes
|
||||
if let Some(v) = reader.peek(12) {
|
||||
if v == 0x001 {
|
||||
if let Some(v) = reader.peek(12)
|
||||
&& v == 0x001
|
||||
{
|
||||
// Possible EOFB — check for second EOL
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let line = match decode_line_2d(&mut reader, &reference, w) {
|
||||
Some(l) => l,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ pub struct Registrar {
|
|||
discord_to_sip: DashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for Registrar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Registrar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -68,11 +74,11 @@ impl Registrar {
|
|||
.find(|r| r.source_addr == reg.source_addr && r.contact_uri == reg.contact_uri)
|
||||
{
|
||||
// If discord_username changed, remove the old reverse mapping
|
||||
if existing.discord_username != reg.discord_username {
|
||||
if let Some(ref old_du) = existing.discord_username {
|
||||
if existing.discord_username != reg.discord_username
|
||||
&& let Some(ref old_du) = existing.discord_username
|
||||
{
|
||||
self.discord_to_sip.remove(old_du);
|
||||
}
|
||||
}
|
||||
|
||||
existing.expires_at = reg.expires_at;
|
||||
existing.registered_at = reg.registered_at;
|
||||
|
|
|
|||
|
|
@ -1112,9 +1112,10 @@ impl VoiceEventHandler for VoiceReceiver {
|
|||
// CRITICAL: Skip our own SSRC to prevent feedback loop
|
||||
// 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 let Some(ref map) = ssrc_map {
|
||||
if let Some(&user_id) = map.get(ssrc) {
|
||||
if user_id == self.bot_user_id {
|
||||
if let Some(ref map) = ssrc_map
|
||||
&& let Some(&user_id) = map.get(ssrc)
|
||||
&& user_id == self.bot_user_id
|
||||
{
|
||||
skipped_self = true;
|
||||
if should_log {
|
||||
trace!(
|
||||
|
|
@ -1124,8 +1125,6 @@ impl VoiceEventHandler for VoiceReceiver {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref decoded) = voice_data.decoded_voice {
|
||||
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
|
||||
// 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.
|
||||
if let Some(handle_storage) = AUDIO_THREAD_HANDLE.get() {
|
||||
if let Some(handle) = handle_storage.lock().take() {
|
||||
if let Some(handle_storage) = AUDIO_THREAD_HANDLE.get()
|
||||
&& let Some(handle) = handle_storage.lock().take()
|
||||
{
|
||||
tracing::debug!("Joining audio thread (2s timeout)...");
|
||||
let (done_tx, done_rx) = std::sync::mpsc::channel();
|
||||
let join_thread = std::thread::spawn(move || {
|
||||
|
|
@ -254,7 +255,6 @@ pub fn stop_audio_thread() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process any pending channel registration completions
|
||||
/// Called from the audio thread after it has processed its first frame
|
||||
|
|
@ -331,8 +331,9 @@ fn process_pending_pjsua_ops() {
|
|||
PendingPjsuaOp::Hangup { call_id } => Some(*call_id),
|
||||
PendingPjsuaOp::ConnectFaxPort { call_id, .. } => Some(*call_id),
|
||||
};
|
||||
if let Some(cid) = call_id {
|
||||
if !is_call_valid(cid) {
|
||||
if let Some(cid) = call_id
|
||||
&& !is_call_valid(cid)
|
||||
{
|
||||
tracing::warn!("Skipping stale op for dead call {}: {:?}", cid, op);
|
||||
// For ConnectFaxPort, signal failure so the caller doesn't hang
|
||||
if let PendingPjsuaOp::ConnectFaxPort { done_tx, .. } = op {
|
||||
|
|
@ -340,7 +341,6 @@ fn process_pending_pjsua_ops() {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
count += 1;
|
||||
match op {
|
||||
PendingPjsuaOp::PlayDirect { call_id, samples } => {
|
||||
|
|
@ -559,7 +559,7 @@ unsafe fn process_audio_frame(
|
|||
timestamp: pj_timestamp { u64_: *timestamp },
|
||||
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
|
||||
// during the conference tick above.
|
||||
|
|
@ -718,15 +718,15 @@ pub fn check_rtp_inactivity() {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(sender_lock) = TIMEOUT_EVENT_TX.get() {
|
||||
if let Some(ref tx) = *sender_lock.lock() {
|
||||
if let Some(sender_lock) = TIMEOUT_EVENT_TX.get()
|
||||
&& let Some(ref tx) = *sender_lock.lock()
|
||||
{
|
||||
for (call_id, rx_count) in timed_out_calls {
|
||||
let _ = tx.send(super::SipEvent::CallTimeout { call_id, rx_count });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate all entries in COUNTED_CALL_IDS are still valid PJSUA calls
|
||||
/// Removes stale entries and returns the number removed.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ pub unsafe fn extract_source_ip(rdata: *const pjsip_rx_data) -> Option<IpAddr> {
|
|||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// 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;
|
||||
|
||||
|
|
@ -96,6 +97,7 @@ pub unsafe fn extract_source_ip(rdata: *const pjsip_rx_data) -> Option<IpAddr> {
|
|||
// so parse directly as IpAddr. This handles both IPv4 and IPv6.
|
||||
ip_str.parse().ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract User-Agent header from pjsip_rx_data
|
||||
pub unsafe fn extract_user_agent(rdata: *const pjsip_rx_data) -> Option<String> {
|
||||
|
|
@ -103,6 +105,7 @@ pub unsafe fn extract_user_agent(rdata: *const pjsip_rx_data) -> Option<String>
|
|||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let msg = (*rdata).msg_info.msg;
|
||||
if msg.is_null() {
|
||||
return None;
|
||||
|
|
@ -127,6 +130,7 @@ pub unsafe fn extract_user_agent(rdata: *const pjsip_rx_data) -> Option<String>
|
|||
let value = pj_str_to_string(&(*str_hdr).hvalue);
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if User-Agent indicates a SIPVicious scanner or similar tool
|
||||
pub fn is_sipvicious_scanner(user_agent: &str) -> bool {
|
||||
|
|
@ -144,6 +148,7 @@ pub unsafe fn extract_digest_auth_from_rdata(
|
|||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let rdata = &*rdata;
|
||||
let msg = rdata.msg_info.msg;
|
||||
if msg.is_null() {
|
||||
|
|
@ -226,9 +231,11 @@ pub unsafe fn extract_digest_auth_from_rdata(
|
|||
|
||||
Some(params)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send 401 Unauthorized response with WWW-Authenticate header
|
||||
pub unsafe fn send_401_challenge(call_id: CallId, www_auth: &str) {
|
||||
unsafe {
|
||||
// Create the WWW-Authenticate header
|
||||
let hdr_name = CString::new("WWW-Authenticate").unwrap();
|
||||
let hdr_value = CString::new(www_auth).unwrap();
|
||||
|
|
@ -282,10 +289,12 @@ pub unsafe fn send_401_challenge(call_id: CallId, www_auth: &str) {
|
|||
// prevents use-after-free crashes.
|
||||
// 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
|
||||
/// 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) {
|
||||
unsafe {
|
||||
// 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
|
||||
// DISCONNECTED before we get here. Calling pjsua_call_answer on a disconnected call
|
||||
|
|
@ -374,6 +383,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
|
||||
// after pjsua_call_answer returns. Same issue as send_401_challenge.
|
||||
}
|
||||
}
|
||||
|
||||
// PJSUA C callbacks
|
||||
|
||||
|
|
@ -382,6 +392,7 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
|||
raw_call_id: pjsua_call_id,
|
||||
rdata: *mut pjsip_rx_data,
|
||||
) {
|
||||
unsafe {
|
||||
let call_id = CallId::new(raw_call_id);
|
||||
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 {
|
||||
|
|
@ -403,9 +414,11 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
|||
let source_ip = extract_source_ip(rdata);
|
||||
|
||||
// Check if IP is banned or timed out - silently drop
|
||||
if let Some(ip) = source_ip {
|
||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
||||
if let Some(ip) = source_ip
|
||||
&& let Some(ban_mgr) = crate::services::ban::global()
|
||||
&& ban_mgr.is_enabled()
|
||||
&& !ban_mgr.is_whitelisted(&ip)
|
||||
{
|
||||
let result = ban_mgr.check_banned(&ip);
|
||||
if result.is_banned {
|
||||
if result.should_log {
|
||||
|
|
@ -426,15 +439,16 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
|||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check User-Agent for SIPVicious scanners - instant permaban
|
||||
if let Some(user_agent) = extract_user_agent(rdata) {
|
||||
if is_sipvicious_scanner(&user_agent) {
|
||||
if let Some(user_agent) = extract_user_agent(rdata)
|
||||
&& is_sipvicious_scanner(&user_agent)
|
||||
{
|
||||
if let Some(ip) = source_ip {
|
||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
||||
if let Some(ban_mgr) = crate::services::ban::global()
|
||||
&& ban_mgr.is_enabled()
|
||||
&& !ban_mgr.is_whitelisted(&ip)
|
||||
{
|
||||
let result = ban_mgr.record_permanent_ban(ip, "sipvicious_scanner");
|
||||
if result.should_log {
|
||||
tracing::warn!(
|
||||
|
|
@ -445,7 +459,6 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"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());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Extension-length ban checks use config values
|
||||
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
|
||||
// The bridge coordinator handles the call flow from here
|
||||
if let Some(callbacks) = CALLBACKS.get() {
|
||||
if let Some(ref handlers) = *callbacks.lock() {
|
||||
if let Some(callbacks) = CALLBACKS.get()
|
||||
&& let Some(ref handlers) = *callbacks.lock()
|
||||
{
|
||||
(handlers.on_incoming_call)(call_id, sip_username, extension.clone(), source_ip);
|
||||
(handlers.on_call_authenticated)(call_id, params, extension, source_ip);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No Authorization header - send 401 challenge
|
||||
tracing::info!("No auth header, sending 401 challenge for call {}", call_id);
|
||||
|
|
@ -590,20 +602,22 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
|||
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) {
|
||||
let call_id = CallId::new(raw_call_id);
|
||||
let digit_char = char::from_u32(digit as u32).unwrap_or('?');
|
||||
|
||||
// Forward DTMF to callback handler (buffering done in mod.rs)
|
||||
if let Some(callbacks) = CALLBACKS.get() {
|
||||
if let Some(ref handlers) = *callbacks.lock() {
|
||||
if let Some(callbacks) = CALLBACKS.get()
|
||||
&& let Some(ref handlers) = *callbacks.lock()
|
||||
{
|
||||
(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) {
|
||||
unsafe {
|
||||
let call_id = CallId::new(raw_call_id);
|
||||
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 {
|
||||
|
|
@ -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 {
|
||||
tracing::info!("Call {} ended (active_media_calls={})", call_id, new_count);
|
||||
|
||||
if let Some(callbacks) = CALLBACKS.get() {
|
||||
if let Some(ref handlers) = *callbacks.lock() {
|
||||
if let Some(callbacks) = CALLBACKS.get()
|
||||
&& let Some(ref handlers) = *callbacks.lock()
|
||||
{
|
||||
(handlers.on_call_ended)(call_id);
|
||||
}
|
||||
}
|
||||
|
||||
if new_count == 0 {
|
||||
tracing::debug!("Last call ended, stopping audio thread");
|
||||
|
|
@ -711,8 +725,10 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 mut ci = MaybeUninit::<pjsua_call_info>::uninit();
|
||||
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 let Some(old_port) = old_conf_port {
|
||||
if old_port != conf_port {
|
||||
if let Some(old_port) = old_conf_port
|
||||
&& old_port != conf_port
|
||||
{
|
||||
tracing::info!(
|
||||
"Call {} conf_port changed from {} to {} (media renegotiation), reconnecting",
|
||||
call_id,
|
||||
|
|
@ -898,7 +915,6 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"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.
|
||||
// For first-time active calls, the call won't be in CHANNEL_CALLS yet,
|
||||
// so this is a no-op.
|
||||
if is_new {
|
||||
if let Some(channel_id) = CALL_CHANNELS
|
||||
if is_new
|
||||
&& let Some(channel_id) = CALL_CHANNELS
|
||||
.get()
|
||||
.and_then(|c| c.get(&call_id).map(|r| *r))
|
||||
{
|
||||
let channel_calls = CHANNEL_CALLS
|
||||
.get_or_init(|| parking_lot::RwLock::new(std::collections::HashMap::new()));
|
||||
let mut map = channel_calls.write();
|
||||
if let Some(calls) = map.get_mut(&channel_id) {
|
||||
if calls.remove(&call_id) {
|
||||
if let Some(calls) = map.get_mut(&channel_id)
|
||||
&& calls.remove(&call_id)
|
||||
{
|
||||
if calls.is_empty() {
|
||||
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
|
||||
// media was ready), complete the registration now. This must happen AFTER
|
||||
|
|
@ -1052,6 +1067,7 @@ pub unsafe extern "C" fn on_call_media_state_cb(raw_call_id: pjsua_call_id) {
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// T.38 offer parameters extracted from SDP
|
||||
#[derive(Debug)]
|
||||
|
|
@ -1070,6 +1086,7 @@ unsafe fn sdp_has_t38(offer: *const pjmedia_sdp_session) -> Option<T38OfferParam
|
|||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
for i in 0..(*offer).media_count {
|
||||
let m = (*offer).media[i as usize];
|
||||
if m.is_null() {
|
||||
|
|
@ -1148,6 +1165,7 @@ unsafe fn sdp_has_t38(offer: *const pjmedia_sdp_session) -> Option<T38OfferParam
|
|||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
opt: *mut pjsua_call_setting,
|
||||
) {
|
||||
unsafe {
|
||||
let call_id = CallId::new(raw_call_id);
|
||||
|
||||
// Check for T.38 offer BEFORE applying hold-stripping logic
|
||||
|
|
@ -1428,6 +1447,7 @@ pub unsafe extern "C" fn on_call_rx_reinvite_cb(
|
|||
stripped
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip hold direction attributes from the SDP negotiator's remote offer clone.
|
||||
/// Returns true if any hold attributes were found and removed.
|
||||
|
|
@ -1437,6 +1457,7 @@ unsafe fn strip_hold_from_neg_remote(call_id: CallId, rdata: *mut pjsip_rx_data)
|
|||
return false;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// rdata → transaction → dialog → inv session → SDP negotiator
|
||||
let tsx = pjsip_rdata_get_tsx(rdata);
|
||||
if tsx.is_null() {
|
||||
|
|
@ -1513,3 +1534,4 @@ unsafe fn strip_hold_from_neg_remote(call_id: CallId, rdata: *mut pjsip_rx_data)
|
|||
|
||||
stripped_any
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,10 +84,12 @@ pub unsafe extern "C" fn channel_port_get_frame(
|
|||
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 {
|
||||
unsafe {
|
||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_NONE;
|
||||
(*frame).size = 0;
|
||||
}
|
||||
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 {
|
||||
let samples = std::slice::from_raw_parts(samples_ptr, samples_len);
|
||||
super::ffi::frame_utils::fill_audio_frame(frame, samples);
|
||||
let samples = unsafe { std::slice::from_raw_parts(samples_ptr, samples_len) };
|
||||
unsafe { super::ffi::frame_utils::fill_audio_frame(frame, samples) };
|
||||
} 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
|
||||
|
|
@ -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 UNDERRUN_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
if let Some(consumer_entry) = get_discord_to_sip_consumers().get(&channel_id) {
|
||||
if let Some(mut consumer) = consumer_entry.try_lock() {
|
||||
if let Some(consumer_entry) = get_discord_to_sip_consumers().get(&channel_id)
|
||||
&& let Some(mut consumer) = consumer_entry.try_lock()
|
||||
{
|
||||
let available = consumer.slots();
|
||||
if available >= SAMPLES_PER_FRAME {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0 // No audio available
|
||||
}
|
||||
|
|
@ -254,11 +256,13 @@ pub unsafe extern "C" fn channel_port_put_frame(
|
|||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
return pj_constants__PJ_SUCCESS as pj_status_t;
|
||||
}
|
||||
|
|
@ -270,16 +274,18 @@ pub unsafe extern "C" fn channel_port_put_frame(
|
|||
call_count,
|
||||
this_port,
|
||||
channel_id,
|
||||
(*frame).size
|
||||
unsafe { (*frame).size }
|
||||
);
|
||||
} else if call_count == 10 {
|
||||
tracing::trace!("channel_port_put_frame: suppressing further per-call logs");
|
||||
}
|
||||
|
||||
// View frame buffer as i16 slice (zero-copy)
|
||||
let samples = unsafe {
|
||||
let num_samples = (*frame).size / 2;
|
||||
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
|
||||
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
|
||||
pub unsafe extern "C" fn channel_port_on_destroy(this_port: *mut pjmedia_port) -> pj_status_t {
|
||||
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;
|
||||
if let Some(mapping) = PORT_TO_CHANNEL.get() {
|
||||
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
|
||||
for &(other_call_id, other_conf_port) in other_calls {
|
||||
let status1 =
|
||||
pjmedia_conf_connect_port(conf, *conf_port as u32, *other_conf_port as u32, 0);
|
||||
let status2 =
|
||||
pjmedia_conf_connect_port(conf, *other_conf_port as u32, *conf_port as u32, 0);
|
||||
let (status1, status2) = unsafe {
|
||||
(
|
||||
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),
|
||||
)
|
||||
};
|
||||
|
||||
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
|
||||
if let Some(channel_slot) = get_or_create_channel_port(channel_id) {
|
||||
let (status1, status2) = unsafe {
|
||||
(
|
||||
// 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)
|
||||
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 {
|
||||
tracing::warn!(
|
||||
|
|
@ -429,8 +441,10 @@ unsafe fn disconnect_call_from_channel(
|
|||
// Disconnect from other calls in the channel (both directions)
|
||||
for &other_call_id in remaining_calls {
|
||||
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, *other_conf_port as u32, *conf_port as u32);
|
||||
}
|
||||
tracing::debug!(
|
||||
"Disconnected call {} from call {} in channel {}",
|
||||
call_id,
|
||||
|
|
@ -442,8 +456,10 @@ unsafe fn disconnect_call_from_channel(
|
|||
|
||||
// Disconnect from channel port (both directions)
|
||||
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, *conf_port as u32, *channel_slot as u32);
|
||||
}
|
||||
tracing::debug!(
|
||||
"Disconnected channel {} slot {} <-> call {} (port {}) bidirectionally",
|
||||
channel_id,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ use std::collections::HashMap;
|
|||
|
||||
/// Custom get_frame callback for direct player ports
|
||||
/// 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(
|
||||
this_port: *mut pjmedia_port,
|
||||
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 *pos < 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;
|
||||
} else {
|
||||
super::frame_utils::fill_silence_frame(frame); // Playback complete
|
||||
unsafe { super::frame_utils::fill_silence_frame(frame) }; // Playback complete
|
||||
}
|
||||
} 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
|
||||
///
|
||||
/// # 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 {
|
||||
if !this_port.is_null() {
|
||||
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() {
|
||||
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() {
|
||||
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
|
||||
/// large enough for SAMPLES_PER_FRAME i16 samples.
|
||||
pub unsafe fn fill_audio_frame(frame: *mut pjmedia_frame, samples: &[i16]) {
|
||||
unsafe {
|
||||
let frame_buf = (*frame).buf as *mut i16;
|
||||
std::ptr::copy_nonoverlapping(samples.as_ptr(), frame_buf, samples.len());
|
||||
// Pad with silence if we got fewer samples than a full frame
|
||||
|
|
@ -51,6 +52,7 @@ pub unsafe fn fill_audio_frame(frame: *mut pjmedia_frame, samples: &[i16]) {
|
|||
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a pjmedia_frame with silence.
|
||||
///
|
||||
|
|
@ -58,11 +60,13 @@ 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
|
||||
/// large enough for SAMPLES_PER_FRAME i16 samples.
|
||||
pub unsafe fn fill_silence_frame(frame: *mut pjmedia_frame) {
|
||||
unsafe {
|
||||
let frame_buf = (*frame).buf as *mut u8;
|
||||
std::ptr::write_bytes(frame_buf, 0, SAMPLES_PER_FRAME * 2);
|
||||
(*frame).size = (SAMPLES_PER_FRAME * 2) as pj_size_t;
|
||||
(*frame).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_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> {
|
||||
// Get or create the memory pool
|
||||
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))
|
||||
});
|
||||
let pool_ptr = pool.lock().0;
|
||||
|
||||
// Allocate pjmedia_port structure
|
||||
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() {
|
||||
anyhow::bail!(
|
||||
"Failed to allocate {} port for call {}",
|
||||
|
|
@ -134,7 +138,7 @@ pub unsafe fn create_and_connect_port(
|
|||
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
|
||||
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))?;
|
||||
|
||||
// Initialize port info
|
||||
unsafe {
|
||||
pjmedia_port_info_init(
|
||||
&mut (*port).info,
|
||||
&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).put_frame = Some(callbacks.put_frame);
|
||||
(*port).on_destroy = callbacks.on_destroy;
|
||||
}
|
||||
|
||||
// Add to conference
|
||||
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 {
|
||||
anyhow::bail!("Failed to add {} port to conf: {}", name_prefix, status);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
pjsua_conf_remove_port(player_slot);
|
||||
unsafe { pjsua_conf_remove_port(player_slot) };
|
||||
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 {
|
||||
pjsua_conf_remove_port(player_slot);
|
||||
unsafe { pjsua_conf_remove_port(player_slot) };
|
||||
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;
|
||||
}
|
||||
|
||||
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 = 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);
|
||||
|
||||
// Create TLS transport if configured (skip gracefully if certs missing)
|
||||
if let Some(tls) = tls_config {
|
||||
if !create_tls_transport(tls, &config.public_host)? {
|
||||
if let Some(tls) = tls_config
|
||||
&& !create_tls_transport(tls, &config.public_host)?
|
||||
{
|
||||
tracing::warn!("TLS transport not created - running without TLS");
|
||||
}
|
||||
}
|
||||
|
||||
// Start pjsua
|
||||
let status = pjsua_start();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ pub struct LoopingPlayerState {
|
|||
|
||||
/// Custom get_frame callback for looping player ports
|
||||
/// 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(
|
||||
this_port: *mut pjmedia_port,
|
||||
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() {
|
||||
let pos = player_data.position;
|
||||
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
|
||||
player_data.position = if end >= player_data.samples.len() {
|
||||
|
|
@ -82,10 +88,10 @@ pub unsafe extern "C" fn looping_player_get_frame(
|
|||
end
|
||||
};
|
||||
} else {
|
||||
super::frame_utils::fill_silence_frame(frame);
|
||||
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||
}
|
||||
} 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
|
||||
///
|
||||
/// # 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 {
|
||||
if !this_port.is_null() {
|
||||
let port_key = this_port as usize;
|
||||
|
|
@ -194,11 +204,11 @@ pub fn stop_loop(call_id: CallId) {
|
|||
|
||||
if let Some(state) = state {
|
||||
// Mark as inactive (get_frame will return silence)
|
||||
if let Some(data) = LOOPING_PLAYER_DATA.get() {
|
||||
if let Some(player_data) = data.lock().get(&state.port_key) {
|
||||
if let Some(data) = LOOPING_PLAYER_DATA.get()
|
||||
&& let Some(player_data) = data.lock().get(&state.port_key)
|
||||
{
|
||||
player_data.is_active.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from conference
|
||||
tracing::trace!(
|
||||
|
|
|
|||
|
|
@ -118,9 +118,9 @@ pub unsafe extern "C" fn streaming_get_frame(
|
|||
|
||||
// Fill frame buffer
|
||||
if !samples.is_empty() {
|
||||
super::frame_utils::fill_audio_frame(frame, &samples);
|
||||
unsafe { super::frame_utils::fill_audio_frame(frame, &samples) };
|
||||
} else {
|
||||
super::frame_utils::fill_silence_frame(frame);
|
||||
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||
}
|
||||
|
||||
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 {
|
||||
super::frame_utils::fill_silence_frame(frame);
|
||||
unsafe { super::frame_utils::fill_silence_frame(frame) };
|
||||
} else {
|
||||
// Copy from precomputed LUT with wraparound (two memcpy calls max)
|
||||
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;
|
||||
|
||||
let first_chunk = (lut_len - phase).min(SAMPLES_PER_FRAME);
|
||||
unsafe {
|
||||
let frame_buf = (*frame).buf as *mut i16;
|
||||
std::ptr::copy_nonoverlapping(
|
||||
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).type_ = pjmedia_frame_type_PJMEDIA_FRAME_TYPE_AUDIO;
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -539,10 +539,9 @@ fn make_outbound_call(sip_uri: &str, caller_display_name: Option<&str>) -> Resul
|
|||
== ::pjsua::pj_constants__PJ_SUCCESS as i32
|
||||
{
|
||||
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()
|
||||
.into_owned();
|
||||
uri_str
|
||||
.into_owned()
|
||||
} else {
|
||||
"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();
|
||||
for line in section_lines {
|
||||
// a=rtpmap:96 opus/48000/2
|
||||
if let Some(rest) = line.strip_prefix("a=rtpmap:") {
|
||||
if let Some(pt_str) = rest.split_whitespace().next() {
|
||||
if let Ok(pt) = pt_str.parse::<u32>() {
|
||||
if let Some(rest) = line.strip_prefix("a=rtpmap:")
|
||||
&& let Some(pt_str) = rest.split_whitespace().next()
|
||||
&& let Ok(pt) = pt_str.parse::<u32>()
|
||||
{
|
||||
rtpmap_pts.insert(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check which PTs in the m= line need stripping
|
||||
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();
|
||||
|
||||
for pt_str in payload_types {
|
||||
if let Ok(pt) = pt_str.parse::<u32>() {
|
||||
if pt >= 96 && !rtpmap_pts.contains(&pt) {
|
||||
if let Ok(pt) = pt_str.parse::<u32>()
|
||||
&& pt >= 96
|
||||
&& !rtpmap_pts.contains(&pt)
|
||||
{
|
||||
stripped_pts.push(pt);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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
|
||||
let stripped_set: std::collections::HashSet<u32> = stripped_pts.into_iter().collect();
|
||||
for line in section_lines {
|
||||
if let Some(rest) = line.strip_prefix("a=fmtp:") {
|
||||
if let Some(pt_str) = rest.split_whitespace().next() {
|
||||
if let Ok(pt) = pt_str.parse::<u32>() {
|
||||
if stripped_set.contains(&pt) {
|
||||
if let Some(rest) = line.strip_prefix("a=fmtp:")
|
||||
&& let Some(pt_str) = rest.split_whitespace().next()
|
||||
&& let Ok(pt) = pt_str.parse::<u32>()
|
||||
&& stripped_set.contains(&pt)
|
||||
{
|
||||
continue; // skip fmtp for stripped PT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
if dst_addr.addr.sa_family == 2 {
|
||||
let addr_in = &dst_addr.ipv4;
|
||||
if unsafe { dst_addr.addr.sa_family } == 2 {
|
||||
let addr_in = unsafe { &dst_addr.ipv4 };
|
||||
let ip_bytes = addr_in.sin_addr.s_addr.to_ne_bytes();
|
||||
Some(Ipv4Addr::new(
|
||||
ip_bytes[0],
|
||||
|
|
@ -195,32 +193,33 @@ unsafe fn rewrite_contact_host(
|
|||
new_host: &str,
|
||||
new_port: u16,
|
||||
) -> 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;
|
||||
if contact_hdr.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let uri = (*contact_hdr).uri;
|
||||
let uri = unsafe { (*contact_hdr).uri };
|
||||
if uri.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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,
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -228,15 +227,17 @@ unsafe fn rewrite_contact_host(
|
|||
return false;
|
||||
};
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(host_cstr.as_ptr(), pool_str, host_len + 1);
|
||||
(*sip_uri).host.ptr = pool_str;
|
||||
(*sip_uri).host.slen = host_len as i64;
|
||||
(*sip_uri).port = new_port as i32;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -251,12 +252,13 @@ unsafe fn rewrite_sdp_body(
|
|||
old_ip: &str,
|
||||
new_ip: &str,
|
||||
) -> bool {
|
||||
let body = (*msg).body;
|
||||
if body.is_null() || (*body).len == 0 || (*body).data.is_null() {
|
||||
let body = unsafe { (*msg).body };
|
||||
if body.is_null() || unsafe { (*body).len == 0 || (*body).data.is_null() } {
|
||||
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 {
|
||||
return false;
|
||||
};
|
||||
|
|
@ -284,14 +286,16 @@ unsafe fn rewrite_sdp_body(
|
|||
}
|
||||
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(new_sdp.as_ptr(), new_body_ptr, new_len);
|
||||
(*body).data = new_body_ptr as *mut _;
|
||||
(*body).len = new_len as u32;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -310,7 +314,7 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
|||
return false;
|
||||
}
|
||||
|
||||
let Some(dst_ip) = extract_dst_ipv4(tdata) else {
|
||||
let Some(dst_ip) = (unsafe { extract_dst_ipv4(tdata) }) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
@ -318,7 +322,7 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
|||
return false;
|
||||
}
|
||||
|
||||
let msg = (*tdata).msg;
|
||||
let msg = unsafe { (*tdata).msg };
|
||||
if msg.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -326,7 +330,7 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
|||
let mut changed = false;
|
||||
|
||||
// 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!(
|
||||
"Rewrote {} Contact header for local client {}: host -> {}:{}",
|
||||
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
|
||||
if let Some(public_ip) = rtp_public_ip {
|
||||
if rewrite_sdp_body((*tdata).pool, msg, public_ip, local_host) {
|
||||
if let Some(public_ip) = rtp_public_ip
|
||||
&& unsafe { rewrite_sdp_body((*tdata).pool, msg, public_ip, local_host) }
|
||||
{
|
||||
tracing::debug!(
|
||||
"Rewrote {} SDP for local client {}: {} -> {}",
|
||||
direction,
|
||||
|
|
@ -349,7 +354,6 @@ unsafe fn rewrite_local_network_tdata(tdata: *mut pjsip_tx_data, direction: &str
|
|||
);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
|
@ -370,42 +374,43 @@ unsafe fn rewrite_private_contact_for_external(tdata: *mut pjsip_tx_data, direct
|
|||
return false;
|
||||
}
|
||||
|
||||
let msg = (*tdata).msg;
|
||||
let msg = unsafe { (*tdata).msg };
|
||||
if msg.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if contact_hdr.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let uri = (*contact_hdr).uri;
|
||||
let uri = unsafe { (*contact_hdr).uri };
|
||||
if uri.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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,
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
if let Some(dst_ip) = extract_dst_ipv4(tdata) {
|
||||
if is_rfc1918(dst_ip) {
|
||||
if let Some(dst_ip) = unsafe { extract_dst_ipv4(tdata) }
|
||||
&& is_rfc1918(dst_ip)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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!(
|
||||
"Rewrote {} Contact for external client: {} -> {}:{}",
|
||||
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
|
||||
/// 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 {
|
||||
let local_rewrite = rewrite_local_network_tdata(tdata, "response");
|
||||
let public_rewrite = rewrite_private_contact_for_external(tdata, "response");
|
||||
let local_rewrite = unsafe { rewrite_local_network_tdata(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
|
||||
// (priority 8, before our module at priority 32). Invalidate and re-encode
|
||||
// so the changes actually reach the wire.
|
||||
if local_rewrite || public_rewrite {
|
||||
unsafe {
|
||||
pjsip_tx_data_invalidate_msg(tdata);
|
||||
pjsip_tx_data_encode(tdata);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// 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 {
|
||||
let local_rewrite = rewrite_local_network_tdata(tdata, "request");
|
||||
let public_rewrite = rewrite_private_contact_for_external(tdata, "request");
|
||||
let local_rewrite = unsafe { rewrite_local_network_tdata(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
|
||||
// (priority 8, before our module at priority 32). Invalidate and re-encode
|
||||
// so the changes actually reach the wire.
|
||||
if local_rewrite || public_rewrite {
|
||||
unsafe {
|
||||
pjsip_tx_data_invalidate_msg(tdata);
|
||||
pjsip_tx_data_encode(tdata);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let msg = (*rdata).msg_info.msg;
|
||||
let msg = unsafe { (*rdata).msg_info.msg };
|
||||
if msg.is_null() {
|
||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||
}
|
||||
|
||||
// Check if there's a body (SDP)
|
||||
let body = (*msg).body;
|
||||
if body.is_null() || (*body).len == 0 || (*body).data.is_null() {
|
||||
let body = unsafe { (*msg).body };
|
||||
if body.is_null() || unsafe { (*body).len == 0 || (*body).data.is_null() } {
|
||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||
}
|
||||
|
||||
// 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
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(src_name.len());
|
||||
let src_ip_str = match std::str::from_utf8(std::slice::from_raw_parts(
|
||||
src_name.as_ptr() as *const u8,
|
||||
name_len,
|
||||
)) {
|
||||
let src_ip_str = match std::str::from_utf8(unsafe {
|
||||
std::slice::from_raw_parts(src_name.as_ptr() as *const u8, name_len)
|
||||
}) {
|
||||
Ok(s) => s,
|
||||
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
|
||||
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) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return pj_constants__PJ_FALSE as pj_bool_t,
|
||||
};
|
||||
|
||||
// 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.
|
||||
let mut needs_rewrite = false;
|
||||
let mut private_ip_str: Option<&str> = None;
|
||||
for line in sdp_str.lines() {
|
||||
if let Some(addr_str) = line.strip_prefix("c=IN IP4 ") {
|
||||
let addr_str = addr_str.trim();
|
||||
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>() {
|
||||
if is_rfc1918(sdp_ip) && sdp_ip != src_ip {
|
||||
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>()
|
||||
&& is_rfc1918(sdp_ip)
|
||||
&& sdp_ip != src_ip
|
||||
{
|
||||
needs_rewrite = true;
|
||||
private_ip_str = Some(addr_str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needs_rewrite {
|
||||
if let Some(private_ip) = private_ip_str {
|
||||
let pool = (*rdata).tp_info.pool;
|
||||
if !pool.is_null() {
|
||||
if rewrite_sdp_body(pool, msg, private_ip, src_ip_str) {
|
||||
if needs_rewrite && let Some(private_ip) = private_ip_str {
|
||||
let pool = unsafe { (*rdata).tp_info.pool };
|
||||
if !pool.is_null() && unsafe { rewrite_sdp_body(pool, msg, private_ip, src_ip_str) } {
|
||||
tracing::debug!(
|
||||
"NAT fixup (INVITE): SDP rewritten {} -> {} (from {}:{})",
|
||||
private_ip,
|
||||
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
|
||||
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;
|
||||
if !contact_hdr.is_null() {
|
||||
let uri = (*contact_hdr).uri;
|
||||
let uri = unsafe { (*contact_hdr).uri };
|
||||
if !uri.is_null() {
|
||||
let uri_vptr = (*(uri as *const pjsip_uri)).vptr;
|
||||
if !uri_vptr.is_null() {
|
||||
if let Some(get_uri_fn) = (*uri_vptr).p_get_uri {
|
||||
let sip_uri_raw = get_uri_fn(uri as *mut std::os::raw::c_void);
|
||||
let uri_vptr = unsafe { (*(uri as *const pjsip_uri)).vptr };
|
||||
if !uri_vptr.is_null()
|
||||
&& let Some(get_uri_fn) = unsafe { (*uri_vptr).p_get_uri }
|
||||
{
|
||||
let sip_uri_raw = unsafe { get_uri_fn(uri as *mut std::os::raw::c_void) };
|
||||
if !sip_uri_raw.is_null() {
|
||||
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
||||
let contact_host = pj_str_to_string(&(*sip_uri).host);
|
||||
if let Ok(contact_ip) = contact_host.parse::<Ipv4Addr>() {
|
||||
if is_rfc1918(contact_ip) && contact_ip != src_ip {
|
||||
let src_port = (*rdata).pkt_info.src_port as u16;
|
||||
let pool = (*rdata).tp_info.pool;
|
||||
if !pool.is_null() {
|
||||
if let Ok(new_host_cstr) = CString::new(src_ip_str) {
|
||||
let contact_host = unsafe { pj_str_to_string(&(*sip_uri).host) };
|
||||
if let Ok(contact_ip) = contact_host.parse::<Ipv4Addr>()
|
||||
&& is_rfc1918(contact_ip)
|
||||
&& contact_ip != src_ip
|
||||
{
|
||||
let src_port = unsafe { (*rdata).pkt_info.src_port } as u16;
|
||||
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 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() {
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
new_host_cstr.as_ptr(),
|
||||
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.slen = host_len as i64;
|
||||
(*sip_uri).port = src_port as i32;
|
||||
}
|
||||
tracing::debug!(
|
||||
"NAT fixup (INVITE): Contact rewritten {} -> {}:{}",
|
||||
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.
|
||||
// Without this, PJSIP's SDP validator rejects these INVITEs with EMISSINGRTPMAP.
|
||||
let body = (*msg).body;
|
||||
if !body.is_null() && (*body).len > 0 && !(*body).data.is_null() {
|
||||
let body = unsafe { (*msg).body };
|
||||
if !body.is_null() && unsafe { (*body).len > 0 && !(*body).data.is_null() } {
|
||||
let body_slice =
|
||||
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 Some(sanitized) = sanitize_sdp_missing_rtpmap(sdp_str) {
|
||||
let pool = (*rdata).tp_info.pool;
|
||||
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)
|
||||
&& let Some(sanitized) = sanitize_sdp_missing_rtpmap(sdp_str)
|
||||
{
|
||||
let pool = unsafe { (*rdata).tp_info.pool };
|
||||
if !pool.is_null() {
|
||||
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() {
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(sanitized.as_ptr(), new_body_ptr, new_len);
|
||||
(*body).data = new_body_ptr as *mut _;
|
||||
(*body).len = new_len as u32;
|
||||
}
|
||||
tracing::debug!(
|
||||
"SDP sanitized: stripped orphan dynamic payload types (from {}:{})",
|
||||
src_ip_str,
|
||||
(*rdata).pkt_info.src_port
|
||||
unsafe { (*rdata).pkt_info.src_port }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let msg = (*rdata).msg_info.msg;
|
||||
let msg = unsafe { (*rdata).msg_info.msg };
|
||||
if msg.is_null() {
|
||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||
}
|
||||
|
||||
// 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
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(src_name.len());
|
||||
let src_ip_str = match std::str::from_utf8(std::slice::from_raw_parts(
|
||||
src_name.as_ptr() as *const u8,
|
||||
name_len,
|
||||
)) {
|
||||
let src_ip_str = match std::str::from_utf8(unsafe {
|
||||
std::slice::from_raw_parts(src_name.as_ptr() as *const u8, name_len)
|
||||
}) {
|
||||
Ok(s) => s,
|
||||
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,
|
||||
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
|
||||
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;
|
||||
if contact_hdr.is_null() {
|
||||
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 Contact URI may be wrapped in a pjsip_name_addr, unlike the tx
|
||||
// path where we can cast directly.
|
||||
let uri = (*contact_hdr).uri;
|
||||
let uri = unsafe { (*contact_hdr).uri };
|
||||
if uri.is_null() {
|
||||
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() {
|
||||
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,
|
||||
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() {
|
||||
return pj_constants__PJ_FALSE as pj_bool_t;
|
||||
}
|
||||
let sip_uri = sip_uri_raw as *mut pjsip_sip_uri;
|
||||
|
||||
// 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() {
|
||||
Ok(ip) => ip,
|
||||
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
|
||||
let pool = (*rdata).tp_info.pool;
|
||||
if !pool.is_null() {
|
||||
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 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() {
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(new_host_cstr.as_ptr(), pool_str, host_len + 1);
|
||||
(*sip_uri).host.ptr = pool_str;
|
||||
(*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.
|
||||
// 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).
|
||||
let body = (*msg).body;
|
||||
if !body.is_null() && (*body).len > 0 && !(*body).data.is_null() {
|
||||
let body = unsafe { (*msg).body };
|
||||
if !body.is_null() && unsafe { (*body).len > 0 && !(*body).data.is_null() } {
|
||||
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) {
|
||||
for line in sdp_str.lines() {
|
||||
if let Some(addr_str) = line.strip_prefix("c=IN IP4 ") {
|
||||
let addr_str = addr_str.trim();
|
||||
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>() {
|
||||
if is_rfc1918(sdp_ip) && sdp_ip != src_ip {
|
||||
if rewrite_sdp_body(pool, msg, addr_str, src_ip_str) {
|
||||
if let Ok(sdp_ip) = addr_str.parse::<Ipv4Addr>()
|
||||
&& is_rfc1918(sdp_ip)
|
||||
&& sdp_ip != src_ip
|
||||
{
|
||||
if unsafe { rewrite_sdp_body(pool, msg, addr_str, src_ip_str) } {
|
||||
tracing::debug!(
|
||||
"NAT fixup: SDP rewritten {} -> {}",
|
||||
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
|
||||
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).
|
||||
#[inline]
|
||||
unsafe fn pj_list_init_hdr(hdr: *mut pjsip_hdr) {
|
||||
unsafe {
|
||||
(*hdr).next = hdr as *mut _;
|
||||
(*hdr).prev = hdr as *mut _;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let endpt = pjsua_get_pjsip_endpt();
|
||||
if !endpt.is_null() {
|
||||
let reason_cstr = CString::new(reason).unwrap();
|
||||
|
|
@ -92,9 +95,11 @@ unsafe fn send_simple_response(rdata: *mut pjsip_rx_data, status_code: u16, reas
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a stateless 200 OK with an Expires header.
|
||||
unsafe fn send_register_ok(rdata: *mut pjsip_rx_data, expires: u32) {
|
||||
unsafe {
|
||||
let endpt = pjsua_get_pjsip_endpt();
|
||||
if endpt.is_null() {
|
||||
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);
|
||||
|
||||
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() {
|
||||
pj_list_init_hdr(hdr_list);
|
||||
pj_list_insert_before(hdr_list as *mut pj_list_type, hdr as *mut pj_list_type);
|
||||
|
|
@ -144,9 +150,11 @@ unsafe fn send_register_ok(rdata: *mut pjsip_rx_data, expires: u32) {
|
|||
tracing::warn!("Failed to respond 200 OK to REGISTER: {}", status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect transport type (UDP/TCP/TLS) from the incoming request.
|
||||
unsafe fn detect_transport(rdata: *mut pjsip_rx_data) -> crate::services::registrar::SipTransport {
|
||||
unsafe {
|
||||
if !(*rdata).tp_info.transport.is_null() {
|
||||
let tp_type = (*(*rdata).tp_info.transport).key.type_ as u32;
|
||||
if tp_type == pjsip_transport_type_e_PJSIP_TRANSPORT_TLS {
|
||||
|
|
@ -160,6 +168,7 @@ unsafe fn detect_transport(rdata: *mut pjsip_rx_data) -> crate::services::regist
|
|||
crate::services::registrar::SipTransport::Udp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a UAS transaction + pre-built response tdata for deferred REGISTER
|
||||
/// responses. Returns `None` if transaction creation fails (caller should fall
|
||||
|
|
@ -168,6 +177,7 @@ unsafe fn create_register_tsx(
|
|||
rdata: *mut pjsip_rx_data,
|
||||
expires: u32,
|
||||
) -> Option<PendingRegisterTsx> {
|
||||
unsafe {
|
||||
let endpt = pjsua_get_pjsip_endpt();
|
||||
let module_ptr = REGISTER_MODULE_PTR.load(Ordering::Acquire);
|
||||
|
||||
|
|
@ -200,6 +210,7 @@ unsafe fn create_register_tsx(
|
|||
expires,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Main callback
|
||||
|
||||
|
|
@ -216,6 +227,7 @@ unsafe fn create_register_tsx(
|
|||
/// b. Cache hit + mismatch -> immediate 403 Forbidden (stateless)
|
||||
/// 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 {
|
||||
unsafe {
|
||||
if rdata.is_null() {
|
||||
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;
|
||||
|
||||
// Ban checks: skip if banning disabled or IP is whitelisted
|
||||
if let Some(ip) = source_ip {
|
||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
||||
if let Some(ip) = source_ip
|
||||
&& let Some(ban_mgr) = crate::services::ban::global()
|
||||
&& ban_mgr.is_enabled()
|
||||
&& !ban_mgr.is_whitelisted(&ip)
|
||||
{
|
||||
// Check if IP is banned
|
||||
let result = ban_mgr.check_banned(&ip);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check User-Agent for SIPVicious scanners - instant permaban
|
||||
if let Some(user_agent) = extract_user_agent(rdata) {
|
||||
if is_sipvicious_scanner(&user_agent) {
|
||||
if let Some(user_agent) = extract_user_agent(rdata)
|
||||
&& is_sipvicious_scanner(&user_agent)
|
||||
{
|
||||
if let Some(ip) = source_ip {
|
||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
||||
let result =
|
||||
ban_mgr.record_permanent_ban(ip, "sipvicious_scanner_register");
|
||||
if let Some(ban_mgr) = crate::services::ban::global()
|
||||
&& ban_mgr.is_enabled()
|
||||
&& !ban_mgr.is_whitelisted(&ip)
|
||||
{
|
||||
let result = ban_mgr.record_permanent_ban(ip, "sipvicious_scanner_register");
|
||||
if result.should_log {
|
||||
tracing::warn!(
|
||||
"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 {
|
||||
tracing::warn!(
|
||||
"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");
|
||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limit REGISTER requests
|
||||
if let Some(ip) = source_ip {
|
||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) && ban_mgr.record_register(ip) {
|
||||
if let Some(ip) = source_ip
|
||||
&& let Some(ban_mgr) = crate::services::ban::global()
|
||||
&& ban_mgr.is_enabled()
|
||||
&& !ban_mgr.is_whitelisted(&ip)
|
||||
&& ban_mgr.record_register(ip)
|
||||
{
|
||||
tracing::debug!("Rejecting REGISTER from {} - rate limit exceeded", ip);
|
||||
send_simple_response(rdata, 429, "Too Many Requests");
|
||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract Digest auth params from Authorization header
|
||||
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();
|
||||
|
||||
// Check auth failure cooldown before processing
|
||||
if let Some(cache) = crate::services::auth_cache::AuthCache::global() {
|
||||
if cache.is_in_cooldown(¶ms.username) {
|
||||
if let Some(cache) = crate::services::auth_cache::AuthCache::global()
|
||||
&& cache.is_in_cooldown(¶ms.username)
|
||||
{
|
||||
tracing::debug!(
|
||||
"Rejecting REGISTER from {} (user={}) - auth cooldown active",
|
||||
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");
|
||||
return pj_constants__PJ_TRUE as pj_bool_t;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract fields needed for all code paths
|
||||
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);
|
||||
|
||||
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() {
|
||||
pj_list_init_hdr(hdr_list);
|
||||
pj_list_insert_before(hdr_list as *mut pj_list_type, hdr as *mut pj_list_type);
|
||||
|
|
@ -485,6 +499,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
|
||||
pj_constants__PJ_TRUE as pj_bool_t
|
||||
}
|
||||
}
|
||||
|
||||
// Extraction helpers
|
||||
|
||||
|
|
@ -494,6 +509,7 @@ unsafe fn extract_contact_uri(rdata: *mut pjsip_rx_data) -> Option<String> {
|
|||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let msg = (*rdata).msg_info.msg;
|
||||
if msg.is_null() {
|
||||
return None;
|
||||
|
|
@ -545,6 +561,7 @@ unsafe fn extract_contact_uri(rdata: *mut pjsip_rx_data) -> Option<String> {
|
|||
|
||||
Some(uri_str)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract Expires value from REGISTER request (header or Contact param)
|
||||
unsafe fn extract_expires(rdata: *mut pjsip_rx_data) -> u32 {
|
||||
|
|
@ -552,6 +569,7 @@ unsafe fn extract_expires(rdata: *mut pjsip_rx_data) -> u32 {
|
|||
return 3600;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let msg = (*rdata).msg_info.msg;
|
||||
if msg.is_null() {
|
||||
return 3600;
|
||||
|
|
@ -568,6 +586,7 @@ unsafe fn extract_expires(rdata: *mut pjsip_rx_data) -> u32 {
|
|||
// Default
|
||||
3600
|
||||
}
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue