Request¶
The Request object is passed to every @proxy.on_request handler. It gives
read access to the parsed SIP message and methods to reply, relay, fork, and
rewrite headers before forwarding.
from siphon import proxy
@proxy.on_request("INVITE")
def route(request):
if request.ruri.is_local:
request.relay()
else:
request.reply(403, "Forbidden")
A SIP request message.
This object is passed to @proxy.on_request handlers. It provides
read-only access to parsed SIP headers and methods to reply, relay,
fork, and manipulate the message before forwarding.
In the real SIPhon engine this is backed by a Rust PyRequest with
an Arc<Mutex<SipMessage>> inside. The mock version stores
everything in plain Python attributes.
Example::
@proxy.on_request
def route(request):
if request.method == "REGISTER":
request.reply(200, "OK")
return
request.relay()
from_tag
property
¶
From-tag parameter (always present for outgoing requests).
to_tag
property
¶
To-tag parameter. None for initial (out-of-dialog) requests.
in_dialog
property
¶
True if both From-tag and To-tag are present (mid-dialog request).
content_type
property
¶
Content-Type header value (e.g. "application/sdp").
auth_user
property
writable
¶
Authenticated username (set after digest auth succeeds).
contact_expires
property
¶
Contact expires value from Contact expires= param or Expires header.
route_user
property
¶
User part of the top Route header URI, or None.
Reflects the current state of the Route header — after any
loose_route() calls have stripped routes addressed to this
proxy. For the user-part of a Route the framework consumed
(e.g. the IMS service-route's orig/term indicator), use
:attr:consumed_route_user.
consumed_route_user
property
¶
User part of the first Route entry that loose_route()
consumed, or None if no Route was popped yet.
Mirrors the production request.consumed_route_user getter so
IMS scripts can read the orig/term user-part of the
service-route the P-CSCF preloaded.
consumed_routes
property
¶
URIs of all Route entries consumed by loose_route(),
in the order they were popped (topmost first).
Empty until the script calls :meth:loose_route.
consumed_route
property
¶
First Route entry consumed by loose_route(), parsed as a
:class:SipUri so the script can read user / host / port
directly without string-munging.
Returns None when no Route was consumed yet. Convenience over
:attr:consumed_route_user for P-CSCF Path-token MT routing where
the script needs both the userpart (the opaque token to look up)
and the host (to verify it points at this proxy).
flow
property
¶
View of the inbound flow this request arrived on, or None
for synthetic requests without listener context.
Pass to :meth:relay (flow= kwarg) for Path-token MT routing
— sends the request back over the same listener that received the
REGISTER without DNS-resolving the URI (RFC 3327 §5 /
TS 24.229 §5.2.7.2).
In the mock, returns the test-fixture flow if one was attached
via the test harness; otherwise None.
charging_params
property
¶
List of (name, value) tuples stashed via
:meth:set_charging_param. Test helper.
reply_headers
property
¶
All (name, value) pairs queued for the response, with
replace/add ops resolved (replace wipes earlier values for the
same name, add appends). Order preserved per name.
reply_header_ops
property
¶
All (op, name, value) triples as queued — exposes the raw
replace/add semantics for tests that need to inspect the queue
before resolution.
is_ipsec_protected
property
¶
Whether this request arrived over an IPsec-protected SA.
Mock returns False by default; tests can override the underlying
_is_ipsec_protected attribute.
reply
¶
Send a SIP response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
int
|
SIP status code (e.g. 200, 401, 404, 486). |
required |
reason
|
str
|
Reason phrase (e.g. |
required |
reliable
|
bool
|
RFC 3262 — when True and |
False
|
Example::
request.reply(200, "OK")
request.reply(486, "Busy Here")
request.reply(183, "Session Progress", reliable=True)
relay
¶
relay(
next_hop: Optional[str] = None,
on_reply: Optional[Callable] = None,
on_failure: Optional[Callable] = None,
flow: Optional[Flow] = None,
send_socket: Optional[str] = None,
) -> None
Forward the request to its destination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
next_hop
|
Optional[str]
|
Optional explicit next-hop URI. If |
None
|
on_reply
|
Optional[Callable]
|
Optional callback |
None
|
on_failure
|
Optional[Callable]
|
Optional callback |
None
|
flow
|
Optional[Flow]
|
Optional :class: |
None
|
send_socket
|
Optional[str]
|
Optional egress socket pin
( |
None
|
Example::
request.relay() # default routing
request.relay("sip:proxy@10.0.0.2:5060") # explicit next-hop
request.relay(on_reply=my_reply_handler) # per-relay callback
request.relay(send_socket="udp:10.0.0.1:5060") # pin egress NIC
# Path-token MT routing:
binding = registrar.lookup_by_token(token)
request.relay(flow=binding.flow)
fork
¶
fork(
targets: list[Union[str, Contact]],
strategy: str = "parallel",
send_socket: Optional[str] = None,
) -> None
Fork the request to multiple targets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
targets
|
list[Union[str, Contact]]
|
List of URI strings or :class: |
required |
strategy
|
str
|
|
'parallel'
|
send_socket
|
Optional[str]
|
Optional egress socket pin applied to every branch
(same |
None
|
Example::
contacts = registrar.lookup(request.ruri)
# Pass Contact objects so a WebSocket UE routes over its flow.
request.fork(contacts)
request.fork(["sip:a@host", "sip:b@host"], strategy="sequential")
request.fork(contacts, send_socket="udp:10.0.0.1:5060")
record_route
¶
Insert a Record-Route header so that subsequent in-dialog requests traverse this proxy.
Must be called before relay() or fork().
loose_route
¶
Perform RFC 3261 §16.4 loose routing.
If the topmost Route entry carries an lr parameter, pop it,
record its URI on :attr:consumed_routes, and return True.
If a Route header is present but the topmost entry is a strict
route (no lr), return False and leave it intact.
When no Route header is present, the mock falls back to
returning :attr:in_dialog to preserve compatibility with
scripts that gate loose_route() behind in_dialog checks
without setting up explicit Route headers in the test request.
Example::
if request.in_dialog:
if request.loose_route():
request.relay()
else:
request.reply(404, "Not Here")
get_header
¶
Get the first value of a header by name (case-insensitive).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
Optional[str]
|
Header value string or |
header
¶
Alias for :meth:get_header.
Example::
ua = request.header("User-Agent")
set_header
¶
Set (replace) a header value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name. |
required |
value
|
str
|
New header value. |
required |
Example::
request.set_header("X-Custom", "my-value")
rewrite_identities
¶
rewrite_identities(
policy: Optional[str] = None,
format: Optional[str] = None,
headers: Optional[list[str]] = None,
home: Optional[str] = None,
) -> int
Rewrite dialable identity userparts into a target E.164 shape.
Walks From, To, P-Asserted-Identity, P-Preferred-Identity, the
Request-URI (and any opted-in Referred-By / Remote-Party-ID),
reformatting each dialable number in place. Display names, tags, hosts,
non-numbers and preserved service codes are left untouched.
Pass either a named policy from the number_policies: config
or an inline format ("e164" | "plain" |
"international" | "national") with an optional headers list
and a home country-code override. Returns the number of headers
changed.
Example::
request.rewrite_identities("teams-outbound@2026")
request.rewrite_identities(format="e164")
request.rewrite_identities(format="national", headers=["From", "request-uri"])
set_charging_param
¶
Stash a charging-param for the Rf auto-emit hook.
Recognised names (TS 32.299 IMS-Information AVPs):
"outgoing-trunk-group-id"— BGCF/MGCF settlement"incoming-trunk-group-id""application-server"— MMTel-AS"application-provided-called-party-address"
Unknown names are silently kept on the request so future
siphon versions can recognise them without breaking scripts.
Tests can assert on the captured values via the
charging_params property.
Example::
gw = gateway.select("trunks")
request.set_charging_param(
"outgoing-trunk-group-id", gw.attrs["group"],
)
request.relay(gw.uri)
set_reply_header
¶
Set (replace) a single-value header on the response built by
:meth:reply or :meth:registrar.save.
Use this for single-value headers per RFC 3261 §7.3.1 — To, From, Contact, Expires, Server, Content-Type, Require, Min-Expires. The dispatcher removes any existing header of the same name before inserting this one, so the response will carry exactly one value even if the framework copied a header from the request (e.g. To, From) before the script ran.
For multi-value headers (Via, Route, Service-Route,
P-Associated-URI, Path), use :meth:add_reply_header instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name. |
required |
value
|
str
|
Header value. |
required |
Example::
our_tag = f"scscf-{request.call_id[:8]}"
request.set_reply_header(
"To", f"{request.get_header('To')};tag={our_tag}"
)
request.reply(200, "OK")
add_reply_header
¶
Append a header to the response built by :meth:reply or
:meth:registrar.save.
Use this for multi-value headers — Via, Record-Route, Route, Service-Route, P-Associated-URI, Path. Multiple calls with the same name accumulate in insertion order, and any existing values copied by the framework (e.g. Via) are preserved.
For single-value headers, use :meth:set_reply_header.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name. |
required |
value
|
str
|
Header value. |
required |
Example::
registrar.save(request)
request.add_reply_header(
"P-Associated-URI", "<sip:user@ims.net>"
)
request.add_reply_header(
"Service-Route", "<sip:orig@scscf:6060;lr>"
)
set_reply_to_tag
¶
Attach a To-tag to the response built by :meth:reply.
Required by RFC 3261 §12.1.1.2 / RFC 6665 §4.1.3 on the dialog-establishing 2xx (and any 1xx that establishes early dialog) for INVITE / SUBSCRIBE / REFER from a UAS.
Reads the request's To header, sets or overwrites the
;tag= parameter, and queues the result for replace
semantics. Idempotent: calling twice with different tags
leaves the most recent tag on the response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag
|
str
|
The To-tag value (no |
required |
Example::
request.set_reply_to_tag(f"scscf-{request.call_id[:8]}")
request.reply(200, "OK")
get_reply_header
¶
Return the reply header value set by :meth:set_reply_header
or :meth:add_reply_header, or None if not set.
Test-only convenience — applies replace/add ops in order so the
result matches what the dispatcher would inject into the outgoing
response. Multi-value (add) entries are joined with ,.
set_body
¶
Replace the body of the incoming request message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
Union[str, bytes]
|
|
required |
content_type
|
str | None
|
Optional Content-Type to set alongside the body. |
None
|
Example::
request.set_body(pidf_lo_xml, "application/pidf+xml")
set_reply_body
¶
Attach a body to the response built by :meth:reply.
The dispatcher copies this body and sets Content-Type /
Content-Length on the outgoing response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
Union[str, bytes]
|
|
required |
content_type
|
str
|
|
required |
Example::
request.set_reply_body(pidf_lo_xml, "application/pidf+xml")
request.reply(200, "OK")
remove_header
¶
Remove a header entirely.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name to remove. |
required |
has_header
¶
Check if a header exists (case-insensitive).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
parse_security_client
¶
Parse the Security-Client header (RFC 3329 / 3GPP TS 33.203).
Returns a list of :class:MockSecurityOffer objects (one per
comma-separated offer). Empty list when the header is absent or
no offer parses cleanly.
The mock parser is byte-compatible with the Rust side:
- Splits on top-level commas (respecting quoted strings).
- Splits each offer on
;; the first token is the mechanism, the remainingkey=valuepairs populate the offer. - UE address is taken from :attr:
source_ip.
has_body
¶
Check if the request has a body matching the given content type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
content_type
|
str
|
MIME type to match (e.g. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
ensure_header
¶
Set a header only if it is not already present.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name. |
required |
value
|
str
|
Value to set if header is missing. |
required |
remove_headers_matching
¶
Remove all headers whose name starts with a prefix (case-insensitive).
Mirrors the production request.remove_headers_matching — e.g.
request.remove_headers_matching("X-") strips every custom header.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
Header-name prefix (e.g. |
required |
remove_from_header_list
¶
Remove one value from a comma-separated multi-value header.
If the header has values "A, B, C" and you remove "B",
the result is "A, C".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Header name. |
required |
value
|
str
|
The specific value to remove. |
required |
set_ruri
¶
Replace the entire Request-URI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Union[str, SipUri]
|
New URI as a string or :class: |
required |
set_ruri_user
¶
Set the user part of the Request-URI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Optional[str]
|
New user part, or |
required |
Example::
request.set_ruri_user("bob")
set_ruri_host
¶
Set the host part of the Request-URI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
New host/domain string. |
required |
set_from_display
¶
Rewrite the From header display name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
display_name
|
str
|
New display name (e.g. |
required |
set_to_display
¶
Rewrite the To header display name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
display_name
|
str
|
New display name. |
required |
set_from_uri
¶
Replace the whole From header URI, preserving the display name and From-tag.
The whole-URI form of :meth:set_from_user. The tag survives — unlike a
raw set_header("From", "<sip:...>"), which drops the mandatory
From-tag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
New From URI (e.g. |
required |
set_to_uri
¶
Replace the whole To header URI, preserving the display name and any To-tag.
The whole-URI form of :meth:set_to_user. The tag survives — unlike a
raw set_header("To", "<sip:...>").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
New To URI (e.g. |
required |
add_path
¶
Prepend a Path header (P-CSCF registration path).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
URI to prepend (e.g. |
required |
add_pcscf_path
¶
Insert a Path header (RFC 3327) of the form
<sip:TOKEN@${path_host};lr> where path_host comes from
the configured ipsec.path_host.
On the matching mobile-terminating request, the topmost Route
will be this same URI; loose_route() consumes it,
:attr:consumed_route_user exposes the token, and
registrar.lookup_by_token(token) resolves back to the
stored binding (TS 24.229 §5.2.7.2).
The mock implementation uses a fixed test path host
"pcscf.test" so unit tests can exercise the Path / Route
/ lookup loop without needing the full siphon config.
prepend_route
¶
Prepend a Route header.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
URI to prepend (e.g. |
required |
set_contact_uri
¶
Replace the whole Contact header URI, preserving the display name and any Contact header params (q/expires/…).
In a proxy the Contact is the UA's in-dialog target: rewriting it changes where mid-dialog requests (BYE, re-INVITE) are routed. Fine when the new target is meaningful downstream; a footgun otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
New Contact URI (e.g. |
required |
set_contact_user
¶
Rewrite only the userpart of the Contact header URI, leaving the host/port/params intact. An empty string clears the userpart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
New Contact userpart (e.g. an extension), or |
required |
fix_nated_register
¶
Add received= and rport= to top Via using source IP:port.
Used by edge proxies / P-CSCFs for NAT traversal on REGISTER.
fix_nated_contact
¶
Rewrite Contact URI host:port with source IP:port.
Used for NAT traversal — ensures replies route back through the actual transport address rather than the Contact address the UA advertised.
force_send_via
¶
Override Via header transport and target for outgoing message.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
str
|
Protocol ( |
required |
target
|
str
|
Target address (e.g. |
required |
generate_icid
¶
Generate a unique ICID (IMS Charging ID) for P-Charging-Vector.
Returns:
| Type | Description |
|---|---|
str
|
UUID string suitable for the |
source_ip_in
¶
Check if the source IP is within any of the given CIDR ranges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cidr_list
|
list[str]
|
List of CIDR strings (e.g. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Example::
if request.source_ip_in(["10.0.0.0/8", "172.16.0.0/12"]):
log.info("Trusted network")
from_gateway
¶
Check if the source IP is a member of a gateway group.
Returns True when this request's source IP is one of the resolved
addresses of the gateway group group_name (configured under
gateway.groups in siphon.yaml, or via gateway.add_group).
This is siphon's equivalent of Kamailio ds_is_from_list() /
OpenSIPS ds_is_in_list() — a routing-direction / trust predicate
that replaces hardcoded source CIDRs.
The match is on IP only (the source port is ignored — gateways answer
from varied ports) and against every resolved address in the
group, so a hostname that round-robins across many IPs (e.g. Teams'
sip/sip2/sip3.pstnhub.microsoft.com) matches on any of
them.
Infallible: returns False (never raises) when the group does not
exist, no gateway is configured, 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::
@proxy.on_request("INVITE")
def route(request):
if request.from_gateway("teams"):
# Inbound from Microsoft Teams — trust and forward to the PBX.
request.relay("sip:pbx.internal:5060")
else:
request.reply(403, "Forbidden")