use anyhow::{Result, anyhow};
use async_trait::async_trait;
use regex::Regex;
use rsipstack::{
dialog::{authenticate::Credential, invitation::InviteOption},
transport::SipAddr,
};
use std::{
collections::{HashMap, hash_map::DefaultHasher},
hash::{Hash, Hasher},
sync::Arc,
};
use tracing::info;
use crate::{
call::{DialDirection, RoutingState, policy::PolicyCheckStatus},
config::{DialplanHints, MediaProxyMode, RouteResult},
proxy::routing::{
ActionType, MediaMode, RouteQueueConfig, RouteRule, SourceTrunk, TrunkConfig,
},
};
#[derive(Debug, Default, Clone)]
pub struct RouteTrace {
pub matched_rule: Option<String>,
pub selected_trunk: Option<String>,
pub used_default_route: bool,
pub rewrite_operations: Vec<String>,
pub abort: Option<RouteAbortTrace>,
}
#[derive(Debug, Clone)]
pub struct RouteAbortTrace {
pub code: u16,
pub reason: Option<String>,
}
#[async_trait]
pub trait RouteResourceLookup: Send + Sync {
async fn load_queue(&self, path: &str) -> Result<Option<RouteQueueConfig>>;
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MatchMode {
Execute,
Inspect,
}
#[allow(clippy::too_many_arguments)]
pub async fn match_invite(
trunks: Option<&HashMap<String, TrunkConfig>>,
routes: Option<&Vec<RouteRule>>,
resource_lookup: Option<&dyn RouteResourceLookup>,
option: InviteOption,
origin: &rsipstack::sip::Request,
source_trunk: Option<&SourceTrunk>,
routing_state: Arc<RoutingState>,
direction: &DialDirection,
) -> Result<RouteResult> {
match_invite_impl(
trunks,
routes,
resource_lookup,
option,
origin,
source_trunk,
routing_state,
direction,
MatchMode::Execute,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn match_invite_with_trace(
trunks: Option<&HashMap<String, TrunkConfig>>,
routes: Option<&Vec<RouteRule>>,
resource_lookup: Option<&dyn RouteResourceLookup>,
option: InviteOption,
origin: &rsipstack::sip::Request,
source_trunk: Option<&SourceTrunk>,
routing_state: Arc<RoutingState>,
direction: &DialDirection,
trace: &mut RouteTrace,
) -> Result<RouteResult> {
match_invite_impl(
trunks,
routes,
resource_lookup,
option,
origin,
source_trunk,
routing_state,
direction,
MatchMode::Execute,
Some(trace),
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn inspect_invite(
trunks: Option<&HashMap<String, TrunkConfig>>,
routes: Option<&Vec<RouteRule>>,
resource_lookup: Option<&dyn RouteResourceLookup>,
option: InviteOption,
origin: &rsipstack::sip::Request,
source_trunk: Option<&SourceTrunk>,
routing_state: Arc<RoutingState>,
direction: &DialDirection,
) -> Result<RouteResult> {
match_invite_impl(
trunks,
routes,
resource_lookup,
option,
origin,
source_trunk,
routing_state,
direction,
MatchMode::Inspect,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn match_invite_impl(
trunks: Option<&HashMap<String, TrunkConfig>>,
routes: Option<&Vec<RouteRule>>,
resource_lookup: Option<&dyn RouteResourceLookup>,
option: InviteOption,
origin: &rsipstack::sip::Request,
source_trunk: Option<&SourceTrunk>,
routing_state: Arc<RoutingState>,
direction: &DialDirection,
mode: MatchMode,
mut trace: Option<&mut RouteTrace>,
) -> Result<RouteResult> {
let mut option = option;
let mut source_hints = None;
if let Some(trunk) =
source_trunk.and_then(|source| trunks.and_then(|trunks| trunks.get(&source.name)))
{
merge_trunk_media_hints(&mut source_hints, trunk);
}
let routes = match routes {
Some(routes) => routes,
None => return Ok(RouteResult::NotHandled(option, source_hints)),
};
let caller_user = option.caller.user().unwrap_or_default().to_string();
let caller_host = option.caller.host().clone();
let callee_user = option.callee.user().unwrap_or_default().to_string();
let callee_host = option.callee.host().clone();
let request_user = origin.uri.user().unwrap_or_default().to_string();
let request_host = origin.uri.host().clone();
info!(
"Matching {:?} caller={}@{}, callee={}@{}, request={}@{}",
direction, caller_user, caller_host, callee_user, callee_host, request_user, request_host
);
for rule in routes {
if let Some(true) = rule.disabled {
continue;
}
if !rule.source_trunks.is_empty() {
match source_trunk {
Some(trunk)
if rule
.source_trunks
.iter()
.any(|name| name.eq_ignore_ascii_case(&trunk.name)) => {}
Some(_) => continue,
None => continue,
}
}
if !rule.source_trunk_ids.is_empty() {
match source_trunk.and_then(|t| t.id) {
Some(id) if rule.source_trunk_ids.contains(&id) => {}
_ => continue,
}
}
let ctx = MatchContext {
origin,
caller_user: &caller_user,
caller_host: &caller_host,
callee_user: &callee_user,
callee_host: &callee_host,
request_user: &request_user,
request_host: &request_host,
};
let rule_matched = matches_rule(rule, &ctx)?;
if !rule_matched {
continue;
}
let origin_country = if let Some(source) = source_trunk {
trunks
.and_then(|t| t.get(&source.name))
.and_then(|c| c.country.as_deref())
} else {
None
};
let captures = collect_match_captures(rule, &ctx)?;
if let Some(trace) = &mut trace {
trace.matched_rule = Some(rule.name.clone());
}
let rewrites = if let Some(rewrite) = &rule.rewrite {
if let Some(trace) = &mut trace {
trace
.rewrite_operations
.extend(describe_rewrite_ops(rewrite));
}
apply_rewrite_rules(&mut option, rewrite, origin, &captures)?
} else {
HashMap::new()
};
info!(
"Matched rule: {:?} action:{:?} rewrites:{:?}",
rule.name, rule.action, rewrites
);
if let Some(policy) = &rule.policy
&& let Some(guard) = &routing_state.policy_guard
{
let current_caller = option.caller.user().unwrap_or_default();
let current_callee = option.callee.user().unwrap_or_default();
if let PolicyCheckStatus::Rejected(rejection) = guard
.check_policy(
&rule.name,
policy,
current_caller,
current_callee,
origin_country,
)
.await?
{
let reason = rejection.to_string();
info!(
"Call rejected by route policy: {} reason: {}",
rule.name, reason
);
if let Some(trace) = &mut trace {
trace.abort = Some(RouteAbortTrace {
code: rsipstack::sip::StatusCode::Forbidden.into(),
reason: Some(reason.clone()),
});
}
return Ok(RouteResult::Abort(
rsipstack::sip::StatusCode::Forbidden,
Some(reason),
));
}
}
let mut hints = source_hints.clone();
if rule.disable_ice_servers.is_some() {
let hints = hints.get_or_insert_with(DialplanHints::default);
hints.disable_ice_servers = rule.disable_ice_servers;
}
match rule.action.get_action_type() {
ActionType::Reject => {
if let Some(reject_config) = &rule.action.reject {
let reason = reject_config.reason.clone();
info!(
"Rejecting call with code {} and reason: {:?}",
reject_config.code, reason
);
if let Some(trace) = &mut trace {
trace.abort = Some(RouteAbortTrace {
code: reject_config.code,
reason: reason.clone(),
});
}
return Ok(RouteResult::Abort(reject_config.code.into(), reason));
} else {
if let Some(trace) = &mut trace {
trace.abort = Some(RouteAbortTrace {
code: rsipstack::sip::StatusCode::Forbidden.into(),
reason: None,
});
}
return Ok(RouteResult::Abort(
rsipstack::sip::StatusCode::Forbidden,
None,
));
}
}
ActionType::Busy => {
if let Some(trace) = &mut trace {
trace.abort = Some(RouteAbortTrace {
code: rsipstack::sip::StatusCode::BusyHere.into(),
reason: None,
});
}
return Ok(RouteResult::Abort(
rsipstack::sip::StatusCode::BusyHere,
None,
));
}
ActionType::Forward => {
if let Some(dest_config) = &rule.action.dest
&& mode == MatchMode::Execute
{
let selected_trunk = select_trunk(
dest_config,
&rule.action.select,
&rule.action.hash_key,
&option,
routing_state.clone(),
trunks,
)?;
if let Some(trace) = &mut trace {
trace.selected_trunk = Some(selected_trunk.clone());
}
if let Some(trunk_config) = trunks
.as_ref()
.and_then(|trunks| trunks.get(&selected_trunk))
{
if let Some(policy) = &trunk_config.policy
&& let Some(guard) = &routing_state.policy_guard
{
let current_caller = option.caller.user().unwrap_or_default();
let current_callee = option.callee.user().unwrap_or_default();
if let PolicyCheckStatus::Rejected(rejection) = guard
.check_policy(
&format!("trunk:{}", selected_trunk),
policy,
current_caller,
current_callee,
origin_country,
)
.await?
{
let reason = rejection.to_string();
info!(
"Call rejected by trunk policy: {} reason: {}",
selected_trunk, reason
);
if let Some(trace) = &mut trace {
trace.abort = Some(RouteAbortTrace {
code: rsipstack::sip::StatusCode::Forbidden.into(),
reason: Some(reason.clone()),
});
}
return Ok(RouteResult::Abort(
rsipstack::sip::StatusCode::Forbidden,
Some(reason),
));
}
}
apply_trunk_config(&mut option, trunk_config)?;
merge_trunk_media_hints(&mut hints, trunk_config);
info!(
"Selected trunk: {} for destination: {}",
selected_trunk, trunk_config.dest
);
} else {
info!("Trunk '{}' not found in configuration", selected_trunk);
}
}
if !rule.codecs.is_empty() {
hints
.get_or_insert_with(DialplanHints::default)
.allow_codecs = Some(rule.codecs.clone());
}
return Ok(RouteResult::Forward(option, hints));
}
ActionType::Queue => {
let queue_ref = rule
.action
.queue
.as_ref()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow!("queue action requires a 'queue' reference"))?;
let lookup = resource_lookup.ok_or_else(|| {
anyhow!(
"queue action cannot resolve '{}' without resource lookup",
queue_ref
)
})?;
let lookup_ref = if queue_ref.chars().all(|c| c.is_ascii_digit()) {
format!("db-{}", queue_ref)
} else {
queue_ref.clone()
};
let queue_cfg = lookup
.load_queue(lookup_ref.as_str())
.await?
.ok_or_else(|| anyhow!("queue '{}' not found", queue_ref))?;
let mut queue_plan = queue_cfg.to_queue_plan()?;
if queue_plan.label.is_none() {
queue_plan.label = Some(queue_ref.clone());
}
let needs_trunk = queue_plan.dial_strategy.is_none();
if needs_trunk {
let dest_config = rule.action.dest.as_ref().ok_or_else(|| {
anyhow!("queue action requires 'dest' or inline queue targets")
})?;
if mode == MatchMode::Execute {
let selected_trunk = select_trunk(
dest_config,
&rule.action.select,
&rule.action.hash_key,
&option,
routing_state.clone(),
trunks,
)?;
if let Some(trace) = &mut trace {
trace.selected_trunk = Some(selected_trunk.clone());
}
if let Some(trunk_config) = trunks
.as_ref()
.and_then(|trunks| trunks.get(&selected_trunk))
{
if let Some(policy) = &trunk_config.policy
&& let Some(guard) = &routing_state.policy_guard
{
let current_caller = option.caller.user().unwrap_or_default();
let current_callee = option.callee.user().unwrap_or_default();
if let PolicyCheckStatus::Rejected(rejection) = guard
.check_policy(
&format!("trunk:{}", selected_trunk),
policy,
current_caller,
current_callee,
origin_country,
)
.await?
{
let reason = rejection.to_string();
info!(
"Call rejected by trunk policy: {} reason: {}",
selected_trunk, reason
);
if let Some(trace) = &mut trace {
trace.abort = Some(RouteAbortTrace {
code: rsipstack::sip::StatusCode::Forbidden.into(),
reason: Some(reason.clone()),
});
}
return Ok(RouteResult::Abort(
rsipstack::sip::StatusCode::Forbidden,
Some(reason),
));
}
}
apply_trunk_config(&mut option, trunk_config)?;
merge_trunk_media_hints(&mut hints, trunk_config);
}
}
}
if !rule.codecs.is_empty() {
hints
.get_or_insert_with(DialplanHints::default)
.allow_codecs = Some(rule.codecs.clone());
}
return Ok(RouteResult::Queue {
option,
queue: queue_plan,
hints,
});
}
ActionType::Application => {
let app_name = rule
.action
.app
.as_ref()
.ok_or_else(|| anyhow!("application action requires 'app' field"))?;
return Ok(RouteResult::Application {
option,
app_name: app_name.clone(),
app_params: rule.action.app_params.clone(),
auto_answer: rule.action.auto_answer,
});
}
}
}
Ok(RouteResult::NotHandled(option, source_hints))
}
struct MatchContext<'a> {
origin: &'a rsipstack::sip::Request,
caller_user: &'a str,
caller_host: &'a rsipstack::sip::Host,
callee_user: &'a str,
callee_host: &'a rsipstack::sip::Host,
request_user: &'a str,
request_host: &'a rsipstack::sip::Host,
}
fn matches_rule(rule: &crate::proxy::routing::RouteRule, ctx: &MatchContext) -> Result<bool> {
let conditions = &rule.match_conditions;
if let Some(pattern) = &conditions.from_user
&& !matches_pattern(pattern, ctx.caller_user)?
{
return Ok(false);
}
if let Some(pattern) = &conditions.from_host
&& !matches_pattern(pattern, &ctx.caller_host.to_string())?
{
return Ok(false);
}
if let Some(pattern) = &conditions.to_user
&& !matches_pattern(pattern, ctx.callee_user)?
{
return Ok(false);
}
if let Some(pattern) = &conditions.to_host
&& !matches_pattern(pattern, &ctx.callee_host.to_string())?
{
return Ok(false);
}
if let Some(pattern) = &conditions.request_uri_user
&& !matches_pattern(pattern, ctx.request_user)?
{
return Ok(false);
}
if let Some(pattern) = &conditions.request_uri_host
&& !matches_pattern(pattern, &ctx.request_host.to_string())?
{
return Ok(false);
}
if let Some(pattern) = &conditions.caller {
let caller_full = format!("{}@{}", ctx.caller_user, ctx.caller_host);
if !matches_pattern(pattern, &caller_full)? {
return Ok(false);
}
}
if let Some(pattern) = &conditions.callee {
let callee_full = format!("{}@{}", ctx.callee_user, ctx.callee_host);
if !matches_pattern(pattern, &callee_full)? {
return Ok(false);
}
}
for (header_key, pattern) in &conditions.headers {
if let Some(header_name) = header_key.strip_prefix("header.") {
if let Some(header_value) = get_header_value(ctx.origin, header_name) {
if !matches_pattern(pattern, &header_value)? {
return Ok(false);
}
} else {
return Ok(false); }
}
}
Ok(true)
}
fn collect_match_captures(
rule: &crate::proxy::routing::RouteRule,
ctx: &MatchContext,
) -> Result<HashMap<String, Vec<String>>> {
let mut captures = HashMap::new();
let conditions = &rule.match_conditions;
collect_field_capture(
&mut captures,
"from.user",
conditions.from_user.as_deref(),
ctx.caller_user,
)?;
let caller_host = ctx.caller_host.to_string();
collect_field_capture(
&mut captures,
"from.host",
conditions.from_host.as_deref(),
&caller_host,
)?;
collect_field_capture(
&mut captures,
"to.user",
conditions.to_user.as_deref(),
ctx.callee_user,
)?;
let callee_host = ctx.callee_host.to_string();
collect_field_capture(
&mut captures,
"to.host",
conditions.to_host.as_deref(),
&callee_host,
)?;
collect_field_capture(
&mut captures,
"request_uri.user",
conditions.request_uri_user.as_deref(),
ctx.request_user,
)?;
let request_host = ctx.request_host.to_string();
collect_field_capture(
&mut captures,
"request_uri.host",
conditions.request_uri_host.as_deref(),
&request_host,
)?;
if let Some(pattern) = &conditions.caller {
let caller_full = format!("{}@{}", ctx.caller_user, ctx.caller_host);
collect_field_capture(
&mut captures,
"caller",
Some(pattern.as_str()),
&caller_full,
)?;
}
if let Some(pattern) = &conditions.callee {
let callee_full = format!("{}@{}", ctx.callee_user, ctx.callee_host);
collect_field_capture(
&mut captures,
"callee",
Some(pattern.as_str()),
&callee_full,
)?;
}
for (header_key, pattern) in &conditions.headers {
if let Some(header_name) = header_key.strip_prefix("header.")
&& let Some(value) = get_header_value(ctx.origin, header_name)
{
collect_field_capture(&mut captures, header_key, Some(pattern.as_str()), &value)?;
}
}
Ok(captures)
}
fn collect_field_capture(
captures: &mut HashMap<String, Vec<String>>,
key: &str,
pattern: Option<&str>,
value: &str,
) -> Result<()> {
if let Some(pattern) = pattern
&& let Some(groups) = extract_regex_captures(pattern, value)?
{
captures.insert(key.to_string(), groups);
}
Ok(())
}
fn extract_regex_captures(pattern: &str, value: &str) -> Result<Option<Vec<String>>> {
if pattern.is_empty() {
return Ok(None);
}
let regex =
Regex::new(pattern).map_err(|e| anyhow!("Invalid regex pattern '{}': {}", pattern, e))?;
if let Some(captures) = regex.captures(value) {
let mut groups = Vec::new();
for index in 0..captures.len() {
groups.push(
captures
.get(index)
.map(|m| m.as_str().to_string())
.unwrap_or_default(),
);
}
return Ok(Some(groups));
}
Ok(None)
}
fn matches_pattern(pattern: &str, value: &str) -> Result<bool> {
if !pattern.contains('^')
&& !pattern.contains('$')
&& !pattern.contains('*')
&& !pattern.contains('+')
&& !pattern.contains('?')
&& !pattern.contains('[')
&& !pattern.contains('(')
&& !pattern.contains('\\')
{
return Ok(pattern == value);
}
let regex =
Regex::new(pattern).map_err(|e| anyhow!("Invalid regex pattern '{}': {}", pattern, e))?;
Ok(regex.is_match(value))
}
fn get_header_value(request: &rsipstack::sip::Request, header_name: &str) -> Option<String> {
for header in request.headers.iter() {
match header {
rsipstack::sip::Header::Other(name, value)
if name.to_lowercase() == header_name.to_lowercase() =>
{
return Some(value.clone());
}
rsipstack::sip::Header::UserAgent(value)
if header_name.to_lowercase() == "user-agent" =>
{
return Some(value.to_string());
}
rsipstack::sip::Header::Contact(contact) if header_name.to_lowercase() == "contact" => {
return Some(contact.to_string());
}
_ => continue,
}
}
None
}
fn apply_rewrite_rules(
option: &mut InviteOption,
rewrite: &crate::proxy::routing::RewriteRules,
origin: &rsipstack::sip::Request,
captures: &HashMap<String, Vec<String>>,
) -> Result<HashMap<String, String>> {
let mut rewrites = HashMap::new();
if let Some(pattern) = &rewrite.from_user {
let new_user = apply_rewrite_pattern_with_match(
pattern,
option.caller.user().unwrap_or_default(),
captures.get("from.user"),
)?;
option.caller = update_uri_user(&option.caller, &new_user)?;
rewrites.insert("from.user".to_string(), new_user);
}
if let Some(pattern) = &rewrite.from_host {
let current_host = option.caller.host().to_string();
let new_host =
apply_rewrite_pattern_with_match(pattern, ¤t_host, captures.get("from.host"))?;
option.caller = update_uri_host(&option.caller, &new_host)?;
rewrites.insert("from.host".to_string(), new_host);
}
if let Some(pattern) = &rewrite.to_user {
let new_user = apply_rewrite_pattern_with_match(
pattern,
option.callee.user().unwrap_or_default(),
captures.get("to.user"),
)?;
option.callee = update_uri_user(&option.callee, &new_user)?;
rewrites.insert("to.user".to_string(), new_user);
}
if let Some(pattern) = &rewrite.to_host {
let current_host = option.callee.host().to_string();
let new_host =
apply_rewrite_pattern_with_match(pattern, ¤t_host, captures.get("to.host"))?;
option.callee = update_uri_host(&option.callee, &new_host)?;
rewrites.insert("to.host".to_string(), new_host);
}
for (header_key, pattern) in &rewrite.headers {
if let Some(header_name) = header_key.strip_prefix("header.") {
let new_value = apply_rewrite_pattern(pattern, "", origin)?;
let new_header = rsipstack::sip::Header::Other(header_name.to_string(), new_value);
if option.headers.is_none() {
option.headers = Some(Vec::new());
}
option.headers.as_mut().unwrap().push(new_header);
}
}
Ok(rewrites)
}
fn describe_rewrite_ops(rewrite: &crate::proxy::routing::RewriteRules) -> Vec<String> {
let mut ops = Vec::new();
let mut push = |label: &str, value: &Option<String>| {
if value.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
ops.push(label.to_string());
}
};
push("from.user", &rewrite.from_user);
push("from.host", &rewrite.from_host);
push("to.user", &rewrite.to_user);
push("to.host", &rewrite.to_host);
push("to.port", &rewrite.to_port);
push("request_uri.user", &rewrite.request_uri_user);
push("request_uri.host", &rewrite.request_uri_host);
push("request_uri.port", &rewrite.request_uri_port);
for header in rewrite.headers.keys() {
ops.push(header.to_string());
}
ops
}
fn apply_rewrite_pattern_with_match(
pattern: &str,
original: &str,
capture_groups: Option<&Vec<String>>,
) -> Result<String> {
if !pattern.contains('{') {
return Ok(pattern.to_string());
}
let mut result = String::new();
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut index_buffer = String::new();
let mut found_closing = false;
while let Some(&next) = chars.peek() {
chars.next();
if next == '}' {
found_closing = true;
break;
}
index_buffer.push(next);
}
if !found_closing {
return Err(anyhow!(
"Unclosed capture group placeholder in rewrite pattern '{}'",
pattern
));
}
if index_buffer.is_empty() {
return Err(anyhow!(
"Empty capture group placeholder in rewrite pattern '{}'",
pattern
));
}
let index_value = index_buffer.parse::<usize>().map_err(|e| {
anyhow!(
"Invalid capture group index '{}' in rewrite pattern '{}': {}",
index_buffer,
pattern,
e
)
})?;
let replacement = capture_groups
.and_then(|groups| groups.get(index_value).cloned())
.or_else(|| {
if index_value == 0 {
Some(original.to_string())
} else {
extract_capture_group(original, index_value)
}
})
.unwrap_or_else(|| original.to_string());
result.push_str(&replacement);
} else {
result.push(ch);
}
}
Ok(result)
}
fn extract_capture_group(original: &str, group_num: usize) -> Option<String> {
if group_num == 0 {
return Some(original.to_string());
}
let patterns = [
(r"^(\d+)$", vec![0]), (r"^[^\d]*(\d+)[^\d]*$", vec![]), ];
for (pattern_str, positions) in &patterns {
if let Ok(regex) = Regex::new(pattern_str)
&& let Some(captures) = regex.captures(original)
{
if group_num <= captures.len()
&& group_num > 0
&& let Some(capture) = captures.get(group_num)
{
return Some(capture.as_str().to_string());
}
if !positions.is_empty() && group_num == 1 {
let start_pos = positions[0];
if original.len() > start_pos {
let substr = &original[start_pos..];
let digits: String =
substr.chars().take_while(|c| c.is_ascii_digit()).collect();
if !digits.is_empty() {
return Some(digits);
}
}
}
}
}
None
}
fn apply_rewrite_pattern(
pattern: &str,
original: &str,
_origin: &rsipstack::sip::Request,
) -> Result<String> {
if pattern.contains('{') && pattern.contains('}') {
let start = pattern.find('{').unwrap();
let end = pattern.find('}').unwrap();
let prefix = &pattern[..start];
let suffix = &pattern[end + 1..];
let _group_num: usize = pattern[start + 1..end].parse().unwrap_or(1);
Ok(format!("{}{}{}", prefix, original, suffix))
} else {
Ok(pattern.to_string())
}
}
fn update_uri_user(uri: &rsipstack::sip::Uri, new_user: &str) -> Result<rsipstack::sip::Uri> {
let mut new_uri = uri.clone();
new_uri.auth = Some(rsipstack::sip::Auth {
user: new_user.to_string(),
password: uri.auth.as_ref().and_then(|a| a.password.clone()),
});
Ok(new_uri)
}
fn update_uri_host(uri: &rsipstack::sip::Uri, new_host: &str) -> Result<rsipstack::sip::Uri> {
let mut new_uri = uri.clone();
new_uri.host_with_port = new_host
.try_into()
.map_err(|e| anyhow!("Invalid host '{}': {:?}", new_host, e))?;
Ok(new_uri)
}
fn select_trunk(
dest_config: &crate::proxy::routing::DestConfig,
select_method: &str,
hash_key: &Option<String>,
option: &InviteOption,
routing_state: Arc<RoutingState>,
trunks_config: Option<&std::collections::HashMap<String, crate::proxy::routing::TrunkConfig>>,
) -> Result<String> {
let trunks = match dest_config {
crate::proxy::routing::DestConfig::Single(trunk) => vec![trunk.clone()],
crate::proxy::routing::DestConfig::Multiple(trunk_list) => trunk_list.clone(),
};
if trunks.is_empty() {
return Err(anyhow!("No trunks configured"));
}
if trunks.len() == 1 {
return Ok(trunks[0].clone());
}
match select_method {
"random" => {
use rand::RngExt;
let index = rand::rng().random_range(0..trunks.len());
Ok(trunks[index].clone())
}
"hash" => {
let hash_value = if let Some(key) = hash_key {
match key.as_str() {
"from.user" => option.caller.user().unwrap_or_default().to_string(),
"to.user" => option.callee.user().unwrap_or_default().to_string(),
"call-id" => "default".to_string(), _ => key.clone(),
}
} else {
option.caller.to_string()
};
let mut hasher = DefaultHasher::new();
hash_value.hash(&mut hasher);
let index = (hasher.finish() as usize) % trunks.len();
Ok(trunks[index].clone())
}
"rr" => {
let destination_key = format!("{:?}", dest_config);
let index = routing_state.next_round_robin_index(&destination_key, trunks.len());
Ok(trunks[index].clone())
}
"weighted" => {
select_trunk_weighted(&trunks, trunks_config)
}
_ => {
let destination_key = format!("{:?}", dest_config);
let index = routing_state.next_round_robin_index(&destination_key, trunks.len());
Ok(trunks[index].clone())
}
}
}
fn select_trunk_weighted(
trunks: &[String],
trunks_config: Option<&std::collections::HashMap<String, crate::proxy::routing::TrunkConfig>>,
) -> Result<String> {
use rand::RngExt;
if trunks.is_empty() {
return Err(anyhow!("No trunks for weighted selection"));
}
if trunks.len() == 1 {
return Ok(trunks[0].clone());
}
let mut weights: Vec<u32> = Vec::with_capacity(trunks.len());
let mut total_weight: u32 = 0;
for trunk_name in trunks {
let weight = trunks_config
.and_then(|configs| configs.get(trunk_name))
.and_then(|config| config.weight)
.unwrap_or(100);
weights.push(weight);
total_weight = total_weight.saturating_add(weight);
}
if total_weight == 0 {
let index = rand::rng().random_range(0..trunks.len());
return Ok(trunks[index].clone());
}
let mut rng = rand::rng();
let random_val = rng.random_range(0..total_weight);
let mut cumulative_weight: u32 = 0;
for (idx, weight) in weights.iter().enumerate() {
cumulative_weight = cumulative_weight.saturating_add(*weight);
if random_val < cumulative_weight {
return Ok(trunks[idx].clone());
}
}
Ok(trunks[trunks.len() - 1].clone())
}
pub(crate) fn apply_trunk_config(option: &mut InviteOption, trunk: &TrunkConfig) -> Result<()> {
let dest_uri: rsipstack::sip::Uri = trunk
.dest
.as_str()
.try_into()
.map_err(|e| anyhow!("Invalid trunk destination '{}': {:?}", trunk.dest, e))?;
let transport = trunk
.transport
.as_deref()
.and_then(super::resolve_transport_from_str);
option.destination = Some(SipAddr {
r#type: transport,
addr: dest_uri.host_with_port.clone(),
});
let original_caller = option.caller.clone();
if trunk.rewrite_hostport {
option.callee.host_with_port = dest_uri.host_with_port.clone();
option.caller.host_with_port = dest_uri.host_with_port.clone();
}
if let (Some(username), Some(password)) = (&trunk.username, &trunk.password) {
option.credential = Some(Credential {
username: username.clone(),
password: password.clone(),
realm: dest_uri.host().to_string().into(),
});
}
if option.headers.is_none() {
option.headers = Some(Vec::new());
}
let headers = option.headers.as_mut().unwrap();
if trunk.username.is_some() {
let pai_header = rsipstack::sip::Header::Other(
"P-Asserted-Identity".to_string(),
format!("<{}>", original_caller),
);
headers.push(pai_header);
}
Ok(())
}
fn merge_trunk_media_hints(hints: &mut Option<DialplanHints>, trunk: &TrunkConfig) {
if trunk.codec.is_empty()
&& trunk.media_mode.is_none()
&& trunk.video_policy.is_none()
&& trunk.recording.is_none()
&& trunk.ringback.is_none()
{
return;
}
let hints = hints.get_or_insert_with(DialplanHints::default);
if !trunk.codec.is_empty() {
hints.allow_codecs = Some(trunk.codec.clone());
}
if let Some(media_mode) = trunk.media_mode.clone() {
hints.media_mode = Some(trunk_media_mode_to_proxy_mode(media_mode));
}
if let Some(video_policy) = trunk.video_policy.clone() {
hints.video_policy = Some(video_policy);
}
if let Some(recording) = trunk.recording.clone() {
hints.recording = Some(recording);
}
if let Some(ringback) = &trunk.ringback {
hints.ringback = Some(ringback.clone());
}
}
fn trunk_media_mode_to_proxy_mode(mode: MediaMode) -> MediaProxyMode {
match mode {
MediaMode::None => MediaProxyMode::None,
MediaMode::Bypass => MediaProxyMode::Bypass,
MediaMode::Auto => MediaProxyMode::Auto,
MediaMode::ForceTranscode => MediaProxyMode::All,
}
}