siphon-sip 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of siphon-sip might be problematic. Click here for more details.

Files changed (83) hide show
  1. siphon_sip-0.3.0/.gitignore +27 -0
  2. siphon_sip-0.3.0/PKG-INFO +360 -0
  3. siphon_sip-0.3.0/README.md +334 -0
  4. siphon_sip-0.3.0/pyproject.toml +53 -0
  5. siphon_sip-0.3.0/siphon_sdk/__init__.py +36 -0
  6. siphon_sip-0.3.0/siphon_sdk/call.py +1876 -0
  7. siphon_sip-0.3.0/siphon_sdk/http.py +302 -0
  8. siphon_sip-0.3.0/siphon_sdk/http_testing.py +199 -0
  9. siphon_sip-0.3.0/siphon_sdk/lcr.py +386 -0
  10. siphon_sip-0.3.0/siphon_sdk/mock_module.py +9626 -0
  11. siphon_sip-0.3.0/siphon_sdk/numbers.py +390 -0
  12. siphon_sip-0.3.0/siphon_sdk/py.typed +0 -0
  13. siphon_sip-0.3.0/siphon_sdk/reply.py +467 -0
  14. siphon_sip-0.3.0/siphon_sdk/request.py +1366 -0
  15. siphon_sip-0.3.0/siphon_sdk/sdp.py +777 -0
  16. siphon_sip-0.3.0/siphon_sdk/smpp.py +643 -0
  17. siphon_sip-0.3.0/siphon_sdk/smpp_testing.py +255 -0
  18. siphon_sip-0.3.0/siphon_sdk/srs.py +201 -0
  19. siphon_sip-0.3.0/siphon_sdk/testing.py +880 -0
  20. siphon_sip-0.3.0/siphon_sdk/types.py +370 -0
  21. siphon_sip-0.3.0/tests/test_auth_integrity_protected.py +311 -0
  22. siphon_sip-0.3.0/tests/test_auth_nonce.py +92 -0
  23. siphon_sip-0.3.0/tests/test_auth_supplied_credential.py +100 -0
  24. siphon_sip-0.3.0/tests/test_auth_user_normalisation.py +68 -0
  25. siphon_sip-0.3.0/tests/test_b2bua_bridge.py +180 -0
  26. siphon_sip-0.3.0/tests/test_b2bua_default.py +71 -0
  27. siphon_sip-0.3.0/tests/test_b2bua_invite_auth.py +167 -0
  28. siphon_sip-0.3.0/tests/test_b2bua_originate.py +266 -0
  29. siphon_sip-0.3.0/tests/test_b2bua_refer.py +350 -0
  30. siphon_sip-0.3.0/tests/test_b2bua_replace_peer.py +157 -0
  31. siphon_sip-0.3.0/tests/test_b2bua_terminate.py +76 -0
  32. siphon_sip-0.3.0/tests/test_cache_list_len.py +75 -0
  33. siphon_sip-0.3.0/tests/test_call_answer_progress.py +83 -0
  34. siphon_sip-0.3.0/tests/test_call_body_and_local_tag.py +115 -0
  35. siphon_sip-0.3.0/tests/test_call_dial.py +161 -0
  36. siphon_sip-0.3.0/tests/test_call_flow.py +77 -0
  37. siphon_sip-0.3.0/tests/test_call_handover.py +63 -0
  38. siphon_sip-0.3.0/tests/test_call_session_timer.py +32 -0
  39. siphon_sip-0.3.0/tests/test_caller_id_clir.py +155 -0
  40. siphon_sip-0.3.0/tests/test_cdr_write.py +116 -0
  41. siphon_sip-0.3.0/tests/test_diameter_server_mock.py +211 -0
  42. siphon_sip-0.3.0/tests/test_from_gateway.py +149 -0
  43. siphon_sip-0.3.0/tests/test_handler_selection.py +134 -0
  44. siphon_sip-0.3.0/tests/test_http_mock.py +265 -0
  45. siphon_sip-0.3.0/tests/test_ims_scripts.py +588 -0
  46. siphon_sip-0.3.0/tests/test_ims_ue_b2bua.py +88 -0
  47. siphon_sip-0.3.0/tests/test_inline_script.py +192 -0
  48. siphon_sip-0.3.0/tests/test_ipsec_pending_sa.py +127 -0
  49. siphon_sip-0.3.0/tests/test_lcr.py +228 -0
  50. siphon_sip-0.3.0/tests/test_lcr_destination.py +93 -0
  51. siphon_sip-0.3.0/tests/test_li_mock.py +215 -0
  52. siphon_sip-0.3.0/tests/test_multi_value_headers.py +82 -0
  53. siphon_sip-0.3.0/tests/test_numbers.py +159 -0
  54. siphon_sip-0.3.0/tests/test_on_cancel.py +93 -0
  55. siphon_sip-0.3.0/tests/test_play_media_wait.py +60 -0
  56. siphon_sip-0.3.0/tests/test_presence_mock.py +138 -0
  57. siphon_sip-0.3.0/tests/test_proxy_default.py +109 -0
  58. siphon_sip-0.3.0/tests/test_qos_mock.py +172 -0
  59. siphon_sip-0.3.0/tests/test_registrar_alias_chain.py +85 -0
  60. siphon_sip-0.3.0/tests/test_registrar_lookup_contact.py +81 -0
  61. siphon_sip-0.3.0/tests/test_registrar_received.py +193 -0
  62. siphon_sip-0.3.0/tests/test_registrar_refusals.py +244 -0
  63. siphon_sip-0.3.0/tests/test_registration_aka.py +93 -0
  64. siphon_sip-0.3.0/tests/test_remove_headers_matching.py +30 -0
  65. siphon_sip-0.3.0/tests/test_reply_reject.py +72 -0
  66. siphon_sip-0.3.0/tests/test_request_consumed_routes.py +103 -0
  67. siphon_sip-0.3.0/tests/test_request_reply_headers.py +118 -0
  68. siphon_sip-0.3.0/tests/test_rf_charging.py +227 -0
  69. siphon_sip-0.3.0/tests/test_ro_charging.py +138 -0
  70. siphon_sip-0.3.0/tests/test_rtpengine_beep_and_tone.py +368 -0
  71. siphon_sip-0.3.0/tests/test_rtpengine_media.py +861 -0
  72. siphon_sip-0.3.0/tests/test_rx_aar_specific_actions.py +62 -0
  73. siphon_sip-0.3.0/tests/test_save_proxy.py +109 -0
  74. siphon_sip-0.3.0/tests/test_sbi_bsf.py +117 -0
  75. siphon_sip-0.3.0/tests/test_sbi_callbacks.py +27 -0
  76. siphon_sip-0.3.0/tests/test_sbi_event_subscription.py +75 -0
  77. siphon_sip-0.3.0/tests/test_sdp.py +497 -0
  78. siphon_sip-0.3.0/tests/test_send_socket.py +52 -0
  79. siphon_sip-0.3.0/tests/test_smpp_mock.py +390 -0
  80. siphon_sip-0.3.0/tests/test_stir_mock.py +152 -0
  81. siphon_sip-0.3.0/tests/test_timer_api.py +114 -0
  82. siphon_sip-0.3.0/tests/test_uri_setters.py +97 -0
  83. siphon_sip-0.3.0/tests/test_whatsapp_calling.py +136 -0
@@ -0,0 +1,27 @@
1
+ /target
2
+ fuzz/target/
3
+ fuzz/artifacts/
4
+ fuzz/corpus/
5
+ sbom.spdx.json
6
+ sbom.cdx.json
7
+ __pycache__/
8
+ *.pyc
9
+ *.log
10
+ .venv/
11
+ sdk/.venv/
12
+ sdk/dist/
13
+ sdk/*.egg-info/
14
+ .claude/
15
+ CLAUDE.md
16
+
17
+ # Private deployment configs
18
+
19
+ # Private test fixtures
20
+ sipp/ipsec/
21
+
22
+ # mkdocs build output
23
+ /site/
24
+
25
+ # Packet captures from the lawful-intercept capture profile
26
+ # (scripts/validate_li_capture.sh) — build artefacts, never committed.
27
+ sipp/li/captures/
@@ -0,0 +1,360 @@
1
+ Metadata-Version: 2.5
2
+ Name: siphon-sip
3
+ Version: 0.3.0
4
+ Summary: Mock library and type stubs for SIPhon SIP proxy/B2BUA scripts — enables unit testing and LLM-assisted script authoring
5
+ Project-URL: Homepage, https://github.com/siphon-project/siphon-sip
6
+ Project-URL: Documentation, https://siphon-sip.org/reference/
7
+ Project-URL: Repository, https://github.com/siphon-project/siphon-sip
8
+ Author: SIPhon Authors
9
+ License-Expression: MIT
10
+ Keywords: b2bua,ims,mock,proxy,sip,testing,voip
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Telecommunications Industry
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Communications :: Telephony
22
+ Classifier: Topic :: Software Development :: Testing :: Mocking
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+
27
+ <p align="center">
28
+ <img src="https://raw.githubusercontent.com/siphon-project/siphon-sip/main/assets/logo.svg" alt="SIPhon" width="100">
29
+ </p>
30
+
31
+ # siphon-sip
32
+
33
+ Mock library and type stubs for [SIPhon](https://github.com/siphon-project/siphon-sip) scripts — enables unit testing without the Rust binary and provides rich context for LLM-assisted script authoring.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install siphon-sip
39
+ ```
40
+
41
+ > The PyPI distribution is **`siphon-sip`** (matching the `siphon-sip` crate). The import package is still **`siphon_sdk`** — `from siphon_sdk import …`.
42
+
43
+ ## What is SIPhon?
44
+
45
+ SIPhon is a high-performance SIP proxy, B2BUA, and IMS platform written in Rust with Python scripting. Scripts use decorators to handle SIP events:
46
+
47
+ ```python
48
+ from siphon import proxy, registrar, auth, log
49
+
50
+ @proxy.on_request
51
+ def route(request):
52
+ if request.method == "REGISTER":
53
+ if not auth.require_digest(request, realm="example.com"):
54
+ return
55
+ registrar.save(request)
56
+ request.reply(200, "OK")
57
+ return
58
+
59
+ contacts = registrar.lookup(request.ruri)
60
+ if not contacts:
61
+ request.reply(404, "Not Found")
62
+ return
63
+
64
+ request.record_route()
65
+ request.fork([c.uri for c in contacts])
66
+ ```
67
+
68
+ This SDK lets you **test** these scripts with pytest — no Rust binary needed.
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ from siphon_sdk.testing import SipTestHarness
74
+ from siphon_sdk.types import Contact
75
+
76
+ harness = SipTestHarness(local_domains=["example.com"])
77
+ harness.load_script("scripts/proxy_default.py")
78
+
79
+ # Pre-populate the registrar
80
+ harness.registrar.add_contact(
81
+ "sip:alice@example.com",
82
+ Contact(uri="sip:alice@192.168.1.5:5060"),
83
+ )
84
+
85
+ # Test REGISTER challenge
86
+ result = harness.send_request("REGISTER", "sip:alice@example.com",
87
+ from_uri="sip:alice@example.com")
88
+ assert result.status_code == 401 # digest challenge
89
+
90
+ # Test INVITE routing
91
+ result = harness.send_request("INVITE", "sip:alice@example.com")
92
+ assert result.action == "fork"
93
+ assert "sip:alice@192.168.1.5:5060" in result.targets
94
+ ```
95
+
96
+ ## Testing B2BUA scripts
97
+
98
+ ```python
99
+ harness = SipTestHarness()
100
+ harness.load_script("scripts/b2bua_default.py")
101
+
102
+ harness.registrar.add_contact(
103
+ "sip:bob@example.com",
104
+ Contact(uri="sip:bob@10.0.0.2:5060"),
105
+ )
106
+
107
+ result = harness.send_invite(ruri="sip:bob@example.com")
108
+ assert result.action == "fork"
109
+ assert result.targets == ["sip:bob@10.0.0.2:5060"]
110
+
111
+ # Test BYE handling
112
+ result = harness.send_bye(initiator_side="a")
113
+ assert result.was_terminated
114
+ ```
115
+
116
+ ## Testing extension scripts (SMPP, HTTP)
117
+
118
+ The opt-in [siphon extensions](https://siphon-sip.org/extensions/) inject extra
119
+ namespaces (`smpp`, `http`) at runtime. The SDK mocks them too, with dedicated
120
+ harnesses, so extension scripts are testable from the same `pip install
121
+ siphon-sip` — no running SMSC or HTTP listener required:
122
+
123
+ ```python
124
+ from siphon_sdk.smpp_testing import SmppTestHarness
125
+
126
+ harness = SmppTestHarness()
127
+ harness.load_script("scripts/gateway.py")
128
+ assert harness.bind("esme1", password="s3cret")
129
+ reply = harness.submit_sm(source_addr="15550100", destination_addr="15550101",
130
+ short_message=b"hi")
131
+ assert reply.ok
132
+ ```
133
+
134
+ ```python
135
+ from siphon_sdk.http_testing import HttpTestHarness
136
+ from siphon_sdk.http import MockResponse
137
+
138
+ harness = HttpTestHarness()
139
+ harness.add_response(MockResponse(status=200, body=b'{"ok":true}'))
140
+ harness.load_script("scripts/api.py")
141
+
142
+ resp = harness.request("GET", "/users/42")
143
+ assert resp.status == 200
144
+ ```
145
+
146
+ ## Inline scripts
147
+
148
+ Test scripts without separate files:
149
+
150
+ ```python
151
+ harness = SipTestHarness()
152
+ harness.load_source("""
153
+ from siphon import proxy
154
+
155
+ @proxy.on_request
156
+ def route(request):
157
+ if request.source_ip_in(["10.0.0.0/8"]):
158
+ request.relay()
159
+ else:
160
+ request.reply(403, "Forbidden")
161
+ """)
162
+
163
+ result = harness.send_request("INVITE", "sip:bob@host", source_ip="10.1.2.3")
164
+ assert result.was_relayed
165
+
166
+ result = harness.send_request("INVITE", "sip:bob@host", source_ip="8.8.8.8")
167
+ assert result.status_code == 403
168
+ ```
169
+
170
+ ## Async handlers + RTPEngine
171
+
172
+ ```python
173
+ harness = SipTestHarness()
174
+ harness.load_source("""
175
+ from siphon import proxy, rtpengine
176
+
177
+ @proxy.on_request
178
+ async def route(request):
179
+ if request.method == "INVITE" and request.body:
180
+ await rtpengine.offer(request, profile="srtp_to_rtp")
181
+ request.relay()
182
+ """)
183
+
184
+ result = harness.send_request("INVITE", "sip:bob@host",
185
+ body=b"v=0\\r\\n...",
186
+ content_type="application/sdp")
187
+ assert result.was_relayed
188
+ assert harness.rtpengine.operations == [("offer", "srtp_to_rtp")]
189
+ ```
190
+
191
+ ## Controlling mock behavior
192
+
193
+ ```python
194
+ # Auth: allow or deny all
195
+ harness.auth._allow = True # all auth checks pass
196
+
197
+ # Rate limiting
198
+ harness.proxy._utils._rate_limit_allow = False # simulate overload
199
+
200
+ # Cache: pre-populate
201
+ harness.cache.set_data("cnam", {"key": "value"})
202
+
203
+ # Registrar: add contacts directly
204
+ harness.registrar.add_contact("sip:alice@host", Contact(uri="sip:alice@1.2.3.4"))
205
+
206
+ # Log: inspect captured messages
207
+ assert any("error" in msg for level, msg in harness.log.messages)
208
+
209
+ # Reset between tests
210
+ harness.reset()
211
+ ```
212
+
213
+ ## Result assertions
214
+
215
+ `RequestResult` provides convenient properties:
216
+
217
+ | Property | Description |
218
+ |----------|-------------|
219
+ | `.action` | Primary action: `"reply"`, `"relay"`, `"fork"`, `"silent_drop"` |
220
+ | `.status_code` | SIP status code (200, 401, 404, etc.) |
221
+ | `.reason` | Reason phrase |
222
+ | `.targets` | Fork targets list |
223
+ | `.strategy` | Fork strategy (`"parallel"` / `"sequential"`) |
224
+ | `.was_relayed` | `True` if `relay()` was called |
225
+ | `.was_forked` | `True` if `fork()` was called |
226
+ | `.was_dropped` | `True` if handler returned without action (silent drop) |
227
+ | `.record_routed` | `True` if `record_route()` was called |
228
+ | `.request` | The mock `Request` object for header inspection |
229
+
230
+ ## API reference
231
+
232
+ ### Namespaces
233
+
234
+ | Import | Description |
235
+ |--------|-------------|
236
+ | `proxy` | Stateful/stateless proxy decorators and utilities |
237
+ | `registrar` | Address-of-record contact store |
238
+ | `auth` | SIP digest authentication |
239
+ | `b2bua` | Back-to-back user agent call control |
240
+ | `log` | Structured logging |
241
+ | `cache` | Named cache (local LRU + Redis) |
242
+ | `rtpengine` | RTPEngine media proxy operations |
243
+ | `gateway` | Destination groups, load balancing, health probing |
244
+ | `cdr` | Call detail records |
245
+ | `diameter` | Diameter protocol (Cx, Ro, Rx, Rf, Sh) |
246
+ | `presence` | SUBSCRIBE/NOTIFY, PIDF presence |
247
+ | `li` | Lawful intercept (ETSI X1/X2/X3, SIPREC) |
248
+ | `registration` | Outbound REGISTER client (trunk registration) |
249
+
250
+ ### Request properties
251
+
252
+ | Property | Type | Description |
253
+ |----------|------|-------------|
254
+ | `method` | `str` | SIP method (`"INVITE"`, `"REGISTER"`, etc.) |
255
+ | `ruri` | `SipUri` | Request-URI |
256
+ | `from_uri` | `SipUri \| None` | From header URI |
257
+ | `to_uri` | `SipUri \| None` | To header URI |
258
+ | `from_tag` | `str \| None` | From-tag |
259
+ | `to_tag` | `str \| None` | To-tag (`None` for initial requests) |
260
+ | `call_id` | `str \| None` | Call-ID |
261
+ | `cseq` | `(int, str) \| None` | CSeq tuple |
262
+ | `in_dialog` | `bool` | Both tags present |
263
+ | `max_forwards` | `int` | Max-Forwards value |
264
+ | `body` | `bytes \| None` | Message body |
265
+ | `content_type` | `str \| None` | Content-Type |
266
+ | `transport` | `str` | `"udp"`, `"tcp"`, `"tls"`, `"ws"`, `"wss"` |
267
+ | `source_ip` | `str` | Sender IP |
268
+ | `auth_user` | `str \| None` | Authenticated username |
269
+ | `event` | `str \| None` | Event header |
270
+
271
+ ### Request methods
272
+
273
+ | Method | Description |
274
+ |--------|-------------|
275
+ | `reply(code, reason)` | Send SIP response |
276
+ | `relay(next_hop=None)` | Forward to destination |
277
+ | `fork(targets, strategy="parallel")` | Fork to multiple targets |
278
+ | `record_route()` | Insert Record-Route |
279
+ | `loose_route() -> bool` | RFC 3261 loose routing |
280
+ | `get_header(name) -> str \| None` | Get header value |
281
+ | `set_header(name, value)` | Set header |
282
+ | `remove_header(name)` | Remove header |
283
+ | `has_header(name) -> bool` | Check header exists |
284
+ | `has_body(content_type) -> bool` | Check body type |
285
+ | `set_ruri_user(value)` | Set R-URI user part |
286
+ | `set_ruri_host(value)` | Set R-URI host |
287
+ | `source_ip_in(cidrs) -> bool` | CIDR membership check |
288
+ | `generate_icid() -> str` | Generate charging ID |
289
+ | `add_path(uri)` | Prepend Path header |
290
+ | `prepend_route(uri)` | Prepend Route header |
291
+ | `fix_nated_register()` | NAT fixup for REGISTER |
292
+ | `fix_nated_contact()` | NAT fixup for Contact |
293
+
294
+ ### Registrar
295
+
296
+ | Method | Description |
297
+ |--------|-------------|
298
+ | `save(request, force=False)` | Save REGISTER bindings |
299
+ | `lookup(uri) -> list[Contact]` | Look up contacts (sorted by q-value) |
300
+ | `is_registered(uri) -> bool` | Check if URI has contacts |
301
+ | `service_route(uri) -> list[str]` | Get stored service routes (RFC 3608) |
302
+ | `set_service_routes(aor, routes)` | Store service routes for an AoR |
303
+ | `save_pending(request)` | IMS: save binding in pending state |
304
+ | `confirm_pending(uri)` | IMS: promote pending to active after SAR |
305
+ | `asserted_identity(uri) -> str \| None` | IMS: stored P-Asserted-Identity |
306
+ | `reginfo_xml(aor, state, version) -> str` | Generate reginfo XML (RFC 3680) |
307
+ | `on_change` | Decorator: fires on registration state changes |
308
+
309
+ `lookup()` returns `Contact` objects:
310
+
311
+ | Field | Description |
312
+ |-------|-------------|
313
+ | `uri` | Contact URI as advertised by the UE |
314
+ | `received` | Transport source of the REGISTER as a SIP URI (`sip:<ip>:<port>;transport=<proto>`), or `None`. Prefer it over `uri` when routing — the Contact URI may carry a private/NAT address (`contact.received or contact.uri`) |
315
+ | `q` | Quality value (0.0–1.0), higher wins |
316
+ | `expires` | Seconds remaining on the binding |
317
+ | `age_secs` | Seconds since the binding was created or refreshed |
318
+ | `path` | RFC 3327 Path headers stored with the binding |
319
+ | `instance_id` / `instance_epoch` | Which siphon instance/process accepted the REGISTER |
320
+ | `is_local` | `True` when *this* process accepted the binding |
321
+ | `flow_token` / `flow` | Captured inbound flow for RFC 5626 connection reuse |
322
+ | `params` | Contact-header params preserved from the REGISTER (RFC 3840 feature tags) |
323
+ | `kind` | `"ue"` (routable) or `"as"` (AS capability record) |
324
+
325
+ ### Auth
326
+
327
+ | Method | Description |
328
+ |--------|-------------|
329
+ | `require_www_digest(request, realm) -> bool` | 401 challenge |
330
+ | `require_proxy_digest(request, realm) -> bool` | 407 challenge |
331
+ | `require_digest(request, realm) -> bool` | Alias for www_digest |
332
+ | `verify_digest(request, realm) -> bool` | Verify without challenge |
333
+ | `require_ims_digest(request, realm) -> bool` | IMS AKA via Diameter Cx MAR |
334
+ | `require_aka_digest(request, realm) -> bool` | Local Milenage AKA (no HSS) |
335
+
336
+ ### B2BUA call
337
+
338
+ Each B-leg gets a fresh Call-ID and From-tag by default, fully decoupling the two SIP dialogs. Use `keep_call_id()` to opt out of Call-ID regeneration.
339
+
340
+ | Property/Method | Description |
341
+ |----------------|-------------|
342
+ | `call.id` | UUID |
343
+ | `call.state` | `"calling"`, `"ringing"`, `"answered"`, `"terminated"` |
344
+ | `call.from_uri` | A-leg From URI |
345
+ | `call.ruri` | A-leg Request-URI |
346
+ | `call.reject(code, reason)` | Reject call |
347
+ | `call.dial(uri, timeout=30)` | Dial single target |
348
+ | `call.fork(targets, strategy, timeout)` | Fork to multiple |
349
+ | `call.terminate()` | End call (BYE both legs) |
350
+ | `call.keep_call_id()` | Copy A-leg Call-ID to B-leg (From-tag always unique) |
351
+ | `call.set_credentials(user, pass)` | B-leg digest auth credentials (auto 401/407 retry) |
352
+ | `call.media.anchor(engine)` | Anchor media through RTPEngine |
353
+ | `call.media.release()` | Release media anchor |
354
+ | `call.session_timer(expires, min_se, refresher)` | Per-call RFC 4028 session timer |
355
+ | `call.record(srs_uri)` | Start SIPREC recording |
356
+ | `call.stop_recording()` | Stop SIPREC recording |
357
+
358
+ ## License
359
+
360
+ MIT