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).
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()