Proxy & B2BUA¶
The proxy and b2bua namespaces register the event handlers that make
routing decisions, plus the helpers those handlers lean on (rate limiting,
sanity checks, ENUM lookup) and the generic SUBSCRIBE-dialog state store.
from siphon import proxy, b2bua
@proxy.on_request
def route(request):
request.relay()
@b2bua.on_invite
async def call(call):
call.dial(call.ruri)
proxy namespace¶
Mock proxy namespace with decorator registration and utility stubs.
Decorators
@proxy.on_request/@proxy.on_request("INVITE")@proxy.on_reply@proxy.on_failure@proxy.on_cancel@proxy.on_register_reply
Example::
from siphon import proxy
@proxy.on_request("REGISTER")
def handle_register(request):
request.reply(200, "OK")
sent_requests
property
¶
List of requests sent via send_request() (for test assertions).
on_request
¶
Register a handler for incoming SIP requests.
Can be used as
@proxy.on_request— handle all methods@proxy.on_request()— same, explicit call@proxy.on_request("REGISTER")— single method filter@proxy.on_request("INVITE|SUBSCRIBE")— pipe-separated filter
on_reply
staticmethod
¶
Register a handler for SIP replies.
Handler signature: (request, reply) -> None
on_failure
staticmethod
¶
Register a handler for proxy failure (all branches failed).
Handler signature: (request, reply) -> None
on_cancel
staticmethod
¶
Register a handler for a CANCELled INVITE (RFC 3261 §9).
Handler signature: (request) -> None
Fires once, with the original INVITE, when a relayed INVITE is
CANCELled before any final response — the one teardown that neither
on_reply nor on_failure delivers (the proxy answers the CANCEL
with 487 at the transaction layer and the session is gone). Use it to
release per-call resources that no BYE will ever clear: Diameter
Rx / N5 QoS sessions, rtpengine media anchors, charging maps.
Fire-and-forget — it does not gate or alter the 487 sent to the UAC.
Example::
@proxy.on_cancel
async def handle_cancel(request):
await _release_qos(request.call_id)
await rtpengine.delete(request)
on_register_reply
staticmethod
¶
Register a handler for REGISTER replies.
Handler signature: (request, reply) -> None
send_request
async
¶
send_request(
method: str,
ruri: str,
headers: Optional[dict[str, str]] = None,
body: Optional[Any] = None,
next_hop: Optional[str] = None,
wait_for_response: bool = False,
timeout_ms: int = 2000,
) -> Any
Originate an outbound SIP request.
Always returns an awaitable — scripts must await it. Fire-and-forget
by default; when wait_for_response=True, the awaitable resolves to
a configured mock Reply (or None on timeout).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
SIP method name (e.g. "NOTIFY", "OPTIONS", "MESSAGE"). |
required |
ruri
|
str
|
Request-URI string (e.g. "sip:alice@10.0.0.1:5060"). |
required |
headers
|
Optional[dict[str, str]]
|
Optional dict of header name → value to add. When a
|
None
|
body
|
Optional[Any]
|
Optional body — |
None
|
next_hop
|
Optional[str]
|
Optional next-hop URI override. Outranks a |
None
|
wait_for_response
|
bool
|
When |
False
|
timeout_ms
|
int
|
Response timeout (not meaningfully enforced in the mock). |
2000
|
set_response_for
¶
Test helper: configure the mock reply returned by
send_request(wait_for_response=True) for a given (method, ruri).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
SIP method (e.g. "OPTIONS"). |
required |
ruri
|
str
|
Request-URI the script will pass. |
required |
reply
|
Any
|
Any object (often a |
required |
proxy utilities¶
Reached as proxy.rate_limit, proxy.sanity_check, proxy.enum_lookup, and
proxy.memory_used_pct.
Mock proxy._utils namespace.
Provides rate limiting, sanity checking, ENUM lookup, and memory stats. In the mock, these return configurable defaults.
rate_limit
¶
Check if a request is within the rate limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Any
|
The SIP request object. |
required |
window_secs
|
float
|
Sliding window duration in seconds. |
required |
max_requests
|
int
|
Maximum requests allowed in the window. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
In the mock, returns the value of _rate_limit_allow (default True).
sanity_check
¶
Validate request per RFC 3261 (mandatory headers, Max-Forwards, etc.).
Returns:
| Type | Description |
|---|---|
bool
|
|
In the mock, returns _sanity_check_pass (default True).
enum_lookup
async
¶
DNS NAPTR lookup for phone number to SIP URI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
number
|
str
|
E.164 number (e.g. |
required |
suffix
|
str
|
DNS suffix (default |
'e164.arpa.'
|
service
|
str
|
Service type (default |
'E2U+sip'
|
Returns:
| Type | Description |
|---|---|
Optional[str]
|
SIP URI string or |
In the mock, looks up _enum_results dict.
memory_used_pct
¶
Process RSS memory usage as percentage (0–100).
In the mock, returns _memory_pct (default 25).
b2bua namespace¶
Mock B2BUA namespace with decorator registration.
Decorators
@b2bua.on_invite— new call@b2bua.on_early_media— provisional response with SDP (183/180)@b2bua.on_answer— call answered@b2bua.on_failure— all B-legs failed@b2bua.on_bye— call ended@b2bua.on_refer— call transfer (RFC 3515)@b2bua.on_cancel— unanswered call cancelled (RFC 3261 §9)
Imperative
b2bua.terminate(call_id)— end a call by SIP Call-ID from any context (records ontoterminatesfor test assertions)b2bua.refer(call_id, target)— transfer a call by SIP Call-ID from any context (records ontorefersfor test assertions)
terminate
¶
Imperatively end a B2BUA call by its SIP Call-ID.
Unlike call.terminate() (deferred until its handler returns), this
acts immediately and is keyed by SIP Call-ID, so it works from an
out-of-band event callback (@rtpengine.on_dtmf,
@rtpengine.on_media_timeout), a timer, or a normal handler.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
call_id
|
str
|
the SIP Call-ID of the call to end. |
required |
reason
|
str
|
free-text hangup reason (RFC 3326 |
'Normal Clearing'
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if a matching call was found and torn down, False if the |
bool
|
Call-ID is unknown / already gone. Never raises. |
In the mock, records {"call_id", "reason"} on terminates and
returns True. Inspect via siphon.get_b2bua().terminates.
Usage::
@rtpengine.on_dtmf
def on_ivr_dtmf(call_id, from_tag, digit, duration_ms, volume):
if digit == "#":
b2bua.terminate(call_id)
refer
¶
Imperatively transfer a B2BUA call by its SIP Call-ID.
The imperative twin of :meth:Call.refer. Unlike call.refer()
(a deferred call action, honoured after its handler returns), this
acts immediately and is keyed by SIP Call-ID, so it works from an
out-of-band event callback (@rtpengine.on_dtmf, a timer) where
no call object is in scope and deferred actions are no-ops — the
same reason :meth:terminate exists alongside call.terminate().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
call_id
|
str
|
the SIP Call-ID of the call to transfer. |
required |
target
|
str
|
the Refer-To URI (transfer destination). |
required |
replaces
|
Optional[dict]
|
optional attended-transfer dict (RFC 3891) with
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if a matching call was found and the REFER was |
bool
|
originated, False if the Call-ID is unknown / already gone. |
|
bool
|
Never raises for a missing call. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
In the mock, records {"call_id", "target", "replaces"} on
refers and returns True. Inspect via
siphon.get_b2bua().refers.
Usage::
@rtpengine.on_dtmf
def on_ivr_dtmf(call_id, from_tag, digit, duration_ms, volume):
if digit == "*":
b2bua.refer(call_id, "sip:+15550142@example.com")
on_invite
staticmethod
¶
Register handler for new INVITE (new call).
Handler signature: (call) -> None
on_early_media
staticmethod
¶
Register handler for provisional response with SDP (183/180).
Called when the B-leg sends a provisional response containing SDP (early media). Use this to process the SDP through RTPEngine so early media is anchored correctly.
Handler signature: (call, reply) -> None
Example::
@b2bua.on_early_media
async def early_media(call, reply):
await rtpengine.answer(reply)
on_answer
staticmethod
¶
Register handler for call answered (200 OK on B-leg).
Handler signature: (call, reply) -> None
on_failure
staticmethod
¶
Register handler for B-leg failure.
Handler signature: (call, code, reason) -> None
on_bye
staticmethod
¶
Register handler for BYE (call ended).
Handler signature: (call, initiator) -> None
initiator is a :class:ByeInitiator with a .side property
("a" or "b").
on_refer
staticmethod
¶
Register handler for REFER (call transfer, RFC 3515).
Handler signature is single-arg (call) -> None. A REFER is a
SIP request, not a response, so there is no reply object —
do NOT write (call, reply) and do NOT call rtpengine.answer()
here. Read the transfer target off :attr:Call.refer_to (and
:attr:Call.refer_replaces for an attended transfer), then decide
with :meth:Call.accept_refer or :meth:Call.reject_refer.
Example::
@b2bua.on_refer
def handle_refer(call):
log.info(f"Transfer requested to {call.refer_to}")
call.accept_refer()
on_cancel
staticmethod
¶
Register handler for a CANCELled call (RFC 3261 §9).
Handler signature: (call) -> None
Fires once, with the Call object, when an unanswered call
(Calling/Ringing) is CANCELled — the teardown that on_failure
(B-leg error) and on_bye (answered call) never cover. A 2xx that
wins the CANCEL/answer glare is ACK+BYE'd by the framework and never
delivers on_answer, so this hook only ever sees a genuinely
abandoned call. Use it to release per-call resources that no BYE will
clear: rtpengine media anchors, QoS sessions.
Example::
@b2bua.on_cancel
async def handle_cancel(call):
await rtpengine.delete(call)
proxy.subscribe_state¶
Generic SUBSCRIBE-dialog state (RFC 6665) for any event package, with optional Redis-backed persistence.
Mock of the Rust proxy.subscribe_state namespace.
Used from scripts under test as proxy.subscribe_state.create(request).
Records NOTIFY and terminate invocations on notifies /
terminates lists for test assertions.
send
¶
send(
ruri: str,
event: str,
expires: int,
accept: Optional[str] = None,
target_uri: Optional[str] = None,
headers: Optional[dict] = None,
timeout_ms: int = 2000,
) -> MockSubscribeHandle
Mock outbound SUBSCRIBE — records the call and synthesises a dialog.
Tests can assert on the recorded self.sends list to verify a
script originated a SUBSCRIBE with the expected parameters.
find
¶
Mock dialog lookup by tags. Returns the first live dialog
matching all three identity fields, or None.
SubscribeHandle¶
A single subscription dialog returned by proxy.subscribe_state.create(...).
Mock of the Rust SubscribeHandle.
In the mock, NOTIFY / terminate calls are recorded on the parent
MockSubscribeState for test assertions. No real SIP message is
produced.
event_version
property
¶
Current event-package body version (read-only).
Mirrors the Rust SubscribeHandle.event_version — used for
RFC 3680 reginfo / RFC 4235 dialog-info / RFC 4575 conference
bodies that require a monotonic version= attribute.
next_event_version
¶
Atomically increment and return the next event-package body version.
Call before building a NOTIFY body whose monotonicity matters::
version = handle.next_event_version()
body = registrar.reginfo_xml(aor, state="full", version=version)
handle.notify(body=body, content_type="application/reginfo+xml")
refresh
¶
Mock refresh — records the call and updates the dialog's expiry.
Tests can assert on the parent's refreshes list. Raises if
the dialog wasn't created via send() (consistent with the
Rust contract that refresh is only valid on outbound dialogs).