Skip to content

SIP & SDP manipulation

The classic header-manipulation (HMR) job: rewrite, add, or strip headers and SDP as a message crosses a boundary. SIPhon hands you the parsed message and gets out of the way. You decide what changes, in Python, with the whole standard library (including re) available. There is no manipulation DSL to learn; it is just code.

Headers

Every header operation is a method on the request (or, on the B2BUA, the call). Names are case-insensitive.

from siphon import proxy

@proxy.on_request
def edit(request):
    # read
    ua = request.get_header("User-Agent")        # str | None
    has_pai = request.has_header("P-Asserted-Identity")

    # strip inbound internal headers by name prefix (case-insensitive), e.g. X-*
    request.remove_headers_matching("X-")

    # then stamp our own
    request.set_header("X-Trunk", "wholesale-a")  # replace (or add if absent)
    request.ensure_header("Max-Forwards", "70")   # set only if not already present
    request.remove_header("P-Preferred-Identity")

    # one value out of a multi-value header
    request.remove_from_header_list("Supported", "100rel")

    request.relay()
Call What it does
get_header(name) / header(name) Read a header value (None if absent).
has_header(name) Presence check.
set_header(name, value) Replace the header, or add it if absent.
ensure_header(name, value) Set only if not already present.
remove_header(name) Remove a header by name.
remove_headers_matching(prefix) Remove every header whose name starts with prefix (case-insensitive).
remove_from_header_list(name, value) Drop one value from a comma-separated header.

Regex

remove_headers_matching is a name-prefix strip, not a regex. When you need a real regular expression, reach for Python's re on the value you read. The scripts are ordinary Python, so the whole re module is there:

import re
from siphon import proxy

SCANNERS = re.compile(r"sipvicious|friendly-scanner|sipcli", re.IGNORECASE)

@proxy.on_request
def drop_scanners(request):
    # drop known scanners silently (no response = no fingerprint)
    if SCANNERS.search(request.get_header("User-Agent") or ""):
        return

    # rewrite inside a header value
    diversion = request.get_header("Diversion")
    if diversion:
        request.set_header("Diversion", re.sub(r"sip:(\d+)@", r"tel:\1;", diversion))

    request.relay()

SDP

Parse the body, edit it, apply it back. apply() sets the body, Content-Type, and Content-Length for you.

from siphon import proxy, sdp

@proxy.on_request("INVITE")
def edit(request):
    if not request.has_body("application/sdp"):
        request.relay(); return

    s = sdp.parse(request)

    # keep only G.711
    s.filter_codecs(["PCMU", "PCMA"])

    # strip video
    s.remove_media("video")

    # put audio on hold
    for m in s.media:
        if m.media_type == "audio":
            m.port = 0

    # session- and media-level a= attributes
    s.set_attr("ice-lite")                 # flag attribute
    for m in s.media:
        m.set_attr("ptime", "20")

    s.apply(request)
    request.relay()

The full sdp surface (media sections, codecs, a= attributes, connection lines) is in the SDP reference.

On the B2BUA

At an SBC boundary, the same header and SDP methods exist on the call, but the right tool for which headers cross the A↔B boundary is a header policy, not per-header code. Pick a preset, then apply per-call deltas only for the exceptions:

from siphon import b2bua

@b2bua.on_invite
def on_invite(call):
    call.remove_headers_matching("X-")           # strip internal headers
    call.dial(
        "sip:+15551234567@carrier.example.net",
        header_policy="sip-trunk-edge@2026",     # the boundary hygiene preset
        copy=["X-Account-Id"],                    # let this one through
        strip=["History-Info"],                   # drop this one
    )

Script call.set_header() / call.remove_header() always win over the preset. The presets and precedence are covered in the SBC recipe.

Test it

Exercise the logic without a running server using the siphon-sip mock SDK:

from siphon_sdk import mock_module
from siphon_sdk.request import Request

mock_module.install()

# internal headers are stripped, and the request is relayed
request = Request(method="INVITE", headers={"X-Internal-Trace": "abc"})
edit(request)
assert request.get_header("X-Internal-Trace") is None
assert request.last_action.kind == "relay"

# a scanner is dropped: no routing action was taken
scanner = Request(method="INVITE", headers={"User-Agent": "friendly-scanner"})
drop_scanners(scanner)
assert scanner.actions == []

The line

The manipulation is yours to define; SIPhon just gives you a clean, parsed message to change and re-serializes it correctly (Via, Content-Length, header ordering). What to rewrite, and why, is your policy.

See also