Reply¶
The Reply object wraps a SIP response. It reaches your script in a
@proxy.on_reply handler (every response on a relayed transaction) and in a
@proxy.on_failure handler (the best error after all branches failed).
from siphon import proxy
@proxy.on_reply
def observe(request, reply):
if reply.status_code == 200 and request.method == "INVITE":
reply.set_header("X-Answered-By", "siphon")
reply.relay()
A SIP response message.
This object is passed as the second argument to reply handlers:
@proxy.on_reply— all responses@proxy.on_failure— aggregated failure response@proxy.on_register_reply— REGISTER responses
Example::
@proxy.on_reply
async def reply_route(request, reply):
if reply.status_code == 200 and reply.has_body("application/sdp"):
await rtpengine.answer(reply)
reply.relay()
source_ip
property
¶
Source IP of the entity that sent this response, or None.
Populated on @proxy.on_reply, @proxy.on_failure per-relay
callbacks, and the B2BUA @b2bua.on_answer / @b2bua.on_early_media
replies (the B-leg peer that answered). None on a fork-aggregated
@proxy.on_failure reply, where the "best" error is selected across
branches and no single source applies. Reply-side counterpart of
request.source_ip.
source_port
property
¶
Source port of the entity that sent this response, or None
(see :attr:source_ip for when it is populated).
from_gateway
¶
Check if this response's source IP is a member of a gateway group.
The reply-side counterpart of request.from_gateway /
call.from_gateway — returns True when the source IP of the entity
that sent this response is one of the resolved addresses of the gateway
group group_name (configured under gateway.groups in
siphon.yaml, or via gateway.add_group). Use it on a response to
decide which trunk actually answered — siphon's answer to Kamailio
ds_is_from_list() / OpenSIPS ds_is_in_list() on the reply path.
The match is on IP only (source port ignored) against every resolved address in the group, so a hostname that round-robins across many IPs matches on any of them.
Infallible: returns False (never raises) when the group does not
exist, no gateway is configured, the response source is unknown (see
:attr:source_ip), or the source IP does not parse.
Security: on connection-oriented transports (TCP/TLS/WS/WSS) the source
IP is handshake-verified and trustworthy as an authorization signal; on
UDP it is spoofable, so from_gateway there is a best-effort
direction hint, not an auth gate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the gateway group to test membership against. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Example::
@b2bua.on_answer
async def on_answer(call, reply):
if reply.from_gateway("carriers"):
log.info("answered by the carrier trunk")
get_header
¶
Get the first value of a header (case-insensitive).
When the header has several values this is the first — use
:meth:get_headers for all of them.
get_headers
¶
Every value of a header, in order — [] when it is absent.
:meth:get_header returns only the first value, which silently
truncates a header the peer spread over several lines. Record-Route,
Via, Route, Contact, Supported, Path and the P-* family are all
routinely multi-value (RFC 3261 §7.3.1), so read those with this.
Values come back one entry per header line. A single line holding several comma-separated URIs stays one entry — split it yourself if you need the individual URIs.
To exercise a multi-line header in a test, pass a list for that name when constructing the mock::
request = Request(headers={"Record-Route": [
"<sip:198.51.100.1:5060;lr>",
"<sip:198.51.100.1:5066;lr>",
]})
assert len(request.get_headers("Record-Route")) == 2
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name (case-insensitive). |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
All values for the header, in order. |
set_header
¶
Set (replace) a header value.
On a B2BUA response in @b2bua.on_answer or
@b2bua.on_early_media the value reaches the caller as written: the
header policy neither strips nor rewrites it, and siphon's own
Supported / Allow do not replace it (replaces is still
merged into Supported). siphon keeps its own Contact, drops the
callee's Record-Route, and decides a provisional's reliability
itself: RSeq and the 100rel tag in Require are siphon's own
toward the caller (RFC 3262), so a value set for either here does not
reach it.
remove_header
¶
Remove a header entirely.
On a B2BUA response in @b2bua.on_answer or
@b2bua.on_early_media the removal stands: neither the header policy
nor siphon's own Supported / Allow put the header back.
has_body
¶
Check if the reply has a body matching the given content type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
content_type
|
str
|
MIME type (e.g. |
required |
take_av
¶
Extract IMS-AKA CK/IK from auth headers and strip ck=/ik=.
Scans WWW-Authenticate, Proxy-Authenticate and
Authentication-Info (in that order). Returns a
:class:MockAuthVectorHandle only when both ck and ik
parsed cleanly; otherwise leaves the headers untouched and returns
None.
Idempotent: after stripping, a second call returns None because
no header still carries ck/ik.
relay
¶
Forward the reply upstream to the UAC.
Example::
@proxy.on_reply
def handle_reply(request, reply):
reply.relay()
reject
¶
Reject an in-progress proxied INVITE from @proxy.on_reply.
The proxy-side equivalent of the B2BUA's call.reject() — needed
because media authorization (sbi.create_session /
diameter.rx_aar) runs at answer time, when the negotiated SDP is
available, and a media-authorization failure must reject the leg rather
than proceed medialess.
Behaviour depends on the stage of the response this handler is running for:
- Provisional (1xx) — no final answer yet: records the reject and
returns
True. siphon then sendscode reasonupstream to the UAC and CANCELs the pending downstream branch(es). This is the clean path — typically a reliable183 Session Progressin the VoLTE preconditions / early-media flow, where the SDP answer rides the provisional. - Final (>= 200) — UAS already answered: a proxy cannot retract a
2xx, so this is a no-op and returns
False. Branch on the return value: log the failed authorization and :meth:relayto let the answer through (best-effort, no dedicated bearer).
Takes precedence over :meth:relay / :meth:forward when it returns
True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
int
|
SIP final-response code in the 400–699 range (e.g. |
required |
reason
|
Optional[str]
|
optional reason phrase; a sensible default is derived from
|
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
send the error + CANCEL); |
bool
|
UAS already sent a final response). |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Example::
@proxy.on_reply
async def on_reply(request, reply):
if request.method == "INVITE" and reply.has_body("application/sdp"):
if not await authorize_media(request, reply):
if reply.reject(503, "Media Authorization Failed"):
return # 503 + CANCEL sent by siphon
log.warn("could not reject answered call; proceeding best-effort")
reply.relay()