From 7403746ccd2e723340780cde3a59cf502bc7213c Mon Sep 17 00:00:00 2001 From: Richard Fuchs Date: Mon, 4 May 2026 13:16:24 -0400 Subject: [PATCH] MT#55283 demo scripts/lib Change-Id: Ib28ff8e2d7a0bf0518c8a99103277ae488acc8b7 --- python/.ycm_extra_conf.py | 149 ++++ python/263655_2064400-lq.mp3 | Bin 0 -> 7944 bytes python/Makefile | 11 + python/README.md | 16 + python/demos/1_simple_answer.py | 179 +++++ python/demos/2_simple_invite.py | 267 +++++++ python/demos/3_gui.py | 640 ++++++++++++++++ python/demos/4_b2b.py | 235 ++++++ python/pysip_lite/.gitignore | 2 + python/pysip_lite/Call.py | 127 ++++ python/pysip_lite/Logger.py | 39 + python/pysip_lite/Provider.py | 33 + python/pysip_lite/_UserAgent.py | 111 +++ python/pysip_lite/_Waiter.py | 40 + python/pysip_lite/__init__.py | 7 + python/pysip_lite/_lib.py | 65 ++ python/pysip_lite/_types.py | 33 + python/src/.gitignore | 1 + python/src/belle-wrap.c | 1242 +++++++++++++++++++++++++++++++ 19 files changed, 3197 insertions(+) create mode 100644 python/.ycm_extra_conf.py create mode 100644 python/263655_2064400-lq.mp3 create mode 100644 python/Makefile create mode 100644 python/README.md create mode 100644 python/demos/1_simple_answer.py create mode 100644 python/demos/2_simple_invite.py create mode 100644 python/demos/3_gui.py create mode 100644 python/demos/4_b2b.py create mode 100644 python/pysip_lite/.gitignore create mode 100644 python/pysip_lite/Call.py create mode 100644 python/pysip_lite/Logger.py create mode 100644 python/pysip_lite/Provider.py create mode 100644 python/pysip_lite/_UserAgent.py create mode 100644 python/pysip_lite/_Waiter.py create mode 100644 python/pysip_lite/__init__.py create mode 100644 python/pysip_lite/_lib.py create mode 100644 python/pysip_lite/_types.py create mode 100644 python/src/.gitignore create mode 100644 python/src/belle-wrap.c diff --git a/python/.ycm_extra_conf.py b/python/.ycm_extra_conf.py new file mode 100644 index 000000000..7d77c4666 --- /dev/null +++ b/python/.ycm_extra_conf.py @@ -0,0 +1,149 @@ +# Generated by YCM Generator at 2026-05-08 07:21:11.987806 + +# This file is NOT licensed under the GPLv3, which is the license for the rest +# of YouCompleteMe. +# +# Here's the license text for this file: +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# For more information, please refer to + +import os +import ycm_core + +flags = [ + '-x', + 'c', + '-I/usr/include/glib-2.0', + '-I/usr/include/sysprof-6', + '-I/usr/lib/x86_64-linux-gnu/glib-2.0/include', + '-Wall', +] + + +# Set this to the absolute path to the folder (NOT the file!) containing the +# compile_commands.json file to use that instead of 'flags'. See here for +# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html +# +# You can get CMake to generate this file for you by adding: +# set( CMAKE_EXPORT_COMPILE_COMMANDS 1 ) +# to your CMakeLists.txt file. +# +# Most projects will NOT need to set this to anything; you can just change the +# 'flags' list of compilation flags. Notice that YCM itself uses that approach. +compilation_database_folder = '' + +if os.path.exists( compilation_database_folder ): + database = ycm_core.CompilationDatabase( compilation_database_folder ) +else: + database = None + +SOURCE_EXTENSIONS = [ '.C', '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ] + +def DirectoryOfThisScript(): + return os.path.dirname( os.path.abspath( __file__ ) ) + + +def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): + if not working_directory: + return list( flags ) + new_flags = [] + make_next_absolute = False + path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] + for flag in flags: + new_flag = flag + + if make_next_absolute: + make_next_absolute = False + if not flag.startswith( '/' ): + new_flag = os.path.join( working_directory, flag ) + + for path_flag in path_flags: + if flag == path_flag: + make_next_absolute = True + break + + if flag.startswith( path_flag ): + path = flag[ len( path_flag ): ] + new_flag = path_flag + os.path.join( working_directory, path ) + break + + if new_flag: + new_flags.append( new_flag ) + return new_flags + + +def IsHeaderFile( filename ): + extension = os.path.splitext( filename )[ 1 ] + return extension in [ '.H', '.h', '.hxx', '.hpp', '.hh' ] + + +def GetCompilationInfoForFile( filename ): + # The compilation_commands.json file generated by CMake does not have entries + # for header files. So we do our best by asking the db for flags for a + # corresponding source file, if any. If one exists, the flags for that file + # should be good enough. + if IsHeaderFile( filename ): + basename = os.path.splitext( filename )[ 0 ] + for extension in SOURCE_EXTENSIONS: + replacement_file = basename + extension + if os.path.exists( replacement_file ): + compilation_info = database.GetCompilationInfoForFile( + replacement_file ) + if compilation_info.compiler_flags_: + return compilation_info + return None + return database.GetCompilationInfoForFile( filename ) + + +def FlagsForFile( filename, **kwargs ): + if database: + # Bear in mind that compilation_info.compiler_flags_ does NOT return a + # python list, but a "list-like" StringVec object + compilation_info = GetCompilationInfoForFile( filename ) + if not compilation_info: + return None + + final_flags = MakeRelativePathsInFlagsAbsolute( + compilation_info.compiler_flags_, + compilation_info.compiler_working_dir_ ) + + else: + relative_to = DirectoryOfThisScript() + final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) + + return { + 'flags': final_flags, + 'do_cache': True + } + +def Settings( **kwargs ): + language = kwargs[ 'language' ] + if language == 'cfamily': + return { + 'flags': flags + } + + return {} diff --git a/python/263655_2064400-lq.mp3 b/python/263655_2064400-lq.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..2f09cf26f6897e5f74e12b1bc65c816c61d88589 GIT binary patch literal 7944 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 literal 0 HcmV?d00001 diff --git a/python/Makefile b/python/Makefile new file mode 100644 index 000000000..9475b34ca --- /dev/null +++ b/python/Makefile @@ -0,0 +1,11 @@ +all: pysip_lite/belle-wrap.so + +pysip_lite/belle-wrap.so: src/belle-wrap.o + $(LD) -g -shared src/belle-wrap.o -o pysip_lite/belle-wrap.so $(shell pkg-config --libs belle-sip glib-2.0) -lc + +src/belle-wrap.o: src/belle-wrap.c + gcc -fPIC -O3 -g -Wall $(shell pkg-config --cflags belle-sip glib-2.0) src/belle-wrap.c -c -o src/belle-wrap.o + +.PHONY: clean +clean: + rm -f src/belle-wrap.o pysip_lite/belle-wrap.so diff --git a/python/README.md b/python/README.md new file mode 100644 index 000000000..28d2b6705 --- /dev/null +++ b/python/README.md @@ -0,0 +1,16 @@ +# Python demo scripts + +This directory contains a set of simple Python scripts to demonstrate features +of rtpengine, from basic media proxying to more advanced media manipulation and +playback features. + +Each script acts as a SIP client or server of some sort, and uses rtpengine to +manipulate media and SDP aspects. The scripts are not meant to be comprehensive +feature-wise, but rather are meant to be playgrounds that can easily be +modified and extended. + +## pysip-lite + +A light-weight SIP module for Python is provided, which is required to run the +demo scripts. The module is based on belle-sip (from the Linphone SDK) and +provides a simple, asyncio-based API to make and receive SIP calls. diff --git a/python/demos/1_simple_answer.py b/python/demos/1_simple_answer.py new file mode 100644 index 000000000..95cd0da11 --- /dev/null +++ b/python/demos/1_simple_answer.py @@ -0,0 +1,179 @@ +import asyncio +import aiohttp +import pysip_lite +import argparse +import string +import random + +parser = argparse.ArgumentParser( + description="Registers as a SIP UA and accepts any incoming calls.", + epilog="Example: --uri sip:bench2user000005@guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain", +) + +parser.add_argument( + "--uri", required=True, help="SIP URI to register UA", metavar="SIP-URI" +) +parser.add_argument( + "--pw", + required=True, + help="Password for SIP registration", + metavar="PASSWORD", +) +parser.add_argument( + "--rtpe", required=True, help="HTTP URI for rtpengine", metavar="HTTP-URI" +) +parser.add_argument( + "--ringing", + required=False, + help="Time to wait between ringing and answering", + metavar="SECONDS", + type=float, + default=5, +) +parser.add_argument( + "--teardown", + required=False, + help="Hang up call after this much time", + metavar="SECONDS", + type=float, +) +parser.add_argument( + "--addr", + required=False, + help="Local SIP IP address to bind to (default 0.0.0.0)", + default="0.0.0.0", + metavar="IP", +) +parser.add_argument( + "--port", + required=False, + help="Local SIP port to bind to", + default=15060, + metavar="NUM", + type=int, +) +parser.add_argument( + "--proto", + required=False, + help="SIP transport protocol (default UDP)", + default="UDP", + choices=["UDP", "TCP"], + metavar="PROTO", +) +parser.add_argument( + "--debug", + help="Enable debug output", + action="store_true", +) + +args = parser.parse_args() + + +prov = pysip_lite.Provider(args.addr, args.port, args.proto) +ua = prov.user_agent(args.uri, args.pw) + +rtpe = args.rtpe + + +suffix = "_" + "".join( + random.choices(string.ascii_uppercase + string.digits, k=5) +) + + +async def teardown(call: pysip_lite.Call, s: float) -> None: + await asyncio.sleep(s) + try: + call.stop() + except: + pass + + +async def handle_call(call: pysip_lite.Call) -> None: + print("new call") + sdp: str = call.sdp() + + try: + print("set ringing") + call.ringing() + await asyncio.sleep(args.ringing) + print("wait...") + + req = { + "command": "publish", + "call-id": call.call_id() + suffix, + "from-tag": call.from_tag(), + "sdp": sdp, + "audio player": "force", + "flags": ["bidirectional"], + } + + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + + print("do answer") + call.answer(resp["sdp"]) + print("call running") + + if args.teardown: + asyncio.create_task(teardown(call, args.teardown)) + + await call.finished() + except Exception as e: + print(f"Exception: {e.args}") + + print("call end") + + req = { + "command": "delete", + "call-id": call.call_id() + suffix, + } + + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + await post.json() + + +def log(s: str) -> None: + print(s) + + +running = True + + +async def main() -> None: + global running, ua + + logger = pysip_lite.Logger(log, 1 if args.debug else 8) + + print("registering...") + ok = await ua.register() + print("registered!") + assert ok + + ua.listen() + + while running: + call = await ua.receive() + if call: + asyncio.create_task(handle_call(call)) + + await logger + + +async def close() -> None: + global running, ua + + print("unregistering and closing...") + running = False + await ua.unregister() + + +eventloop = asyncio.new_event_loop() +mt = eventloop.create_task(main()) +try: + eventloop.run_until_complete(mt) +except KeyboardInterrupt as e: + eventloop.run_until_complete(close()) + eventloop.run_until_complete(mt) +eventloop.close() diff --git a/python/demos/2_simple_invite.py b/python/demos/2_simple_invite.py new file mode 100644 index 000000000..a8f870000 --- /dev/null +++ b/python/demos/2_simple_invite.py @@ -0,0 +1,267 @@ +import asyncio +import aiohttp +import pysip_lite +import argparse +import string +import random + +parser = argparse.ArgumentParser( + description="Registers as a SIP UA and repeatedly makes outgoing calls", + epilog="Example: --uri sip:bench2user000005@guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain --to sip:bench2user000000@guest02-snail.lab.sipwise.com", +) + +parser.add_argument( + "--uri", required=True, help="SIP URI to register UA", metavar="SIP-URI" +) +parser.add_argument( + "--pw", + required=True, + help="Password for SIP registration", + metavar="PASSWORD", +) +parser.add_argument( + "--rtpe", required=True, help="HTTP URI for rtpengine", metavar="HTTP-URI" +) +parser.add_argument( + "--to", required=True, help="SIP URI to make calls to", metavar="SIP-URI" +) +parser.add_argument( + "--wait", + required=False, + help="Make new call this long after the previous has closed", + metavar="SECONDS", + type=float, + default=5, +) +parser.add_argument( + "--multi", + help="Create multiple calls in parallel", + action="store_true", +) +parser.add_argument( + "--teardown", + required=False, + help="Hang up call after this much time", + metavar="SECONDS", + type=float, +) +parser.add_argument( + "--num", + required=False, + help="Stop after making this many calls", + metavar="NUM", + type=int, + default=0, +) +parser.add_argument( + "--codecs", + required=False, + help="Codecs to offer", + metavar="CODEC,CODEC,CODEC,...", + default="opus,AMR-WB,G722,AMR,PCMA,PCMU", +) +parser.add_argument( + "--addr", + required=False, + help="Local SIP IP address to bind to (default 0.0.0.0)", + default="0.0.0.0", + metavar="IP", +) +parser.add_argument( + "--port", + required=False, + help="Local SIP port to bind to", + default=15061, + metavar="NUM", + type=int, +) +parser.add_argument( + "--proto", + required=False, + help="SIP transport protocol (default UDP)", + default="UDP", + choices=["UDP", "TCP"], + metavar="PROTO", +) +parser.add_argument( + "--debug", + help="Enable debug output", + action="store_true", +) + +args = parser.parse_args() + + +prov = pysip_lite.Provider(args.addr, args.port, args.proto) +ua = prov.user_agent(args.uri, args.pw) + +to = args.to + +rtpe = args.rtpe + + +running = True + + +async def teardown(call: pysip_lite.Call, s: float, seq: int) -> None: + await asyncio.sleep(s) + print(f"[{seq}] call stop") + try: + call.stop() + except: + print(f"[{seq}] exception") + + +seqn = 0 + + +async def make_call() -> None: + global seqn + + seq = seqn + seqn += 1 + + print(f"[{seq}] create req rtpengine...") + + req = { + "command": "create", + "codecs": { + "offer": args.codecs.split(","), + }, + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + except: + print(f"[{seq}] exception") + return + + print(f"[{seq}] send invite...") + call = ua.create(to, resp["call-id"], resp["from-tag"], resp["sdp"]) + + if not call: + print(f"[{seq}] failed to create call") + return + + print(f"[{seq}] wait for answer...") + + code: int = await call.wait(180) + if not code or code >= 400: + print(f"[{seq}] call rejected or error") + return + + if code == 180: + print(f"[{seq}] ringing!") + + code: int = await call.wait() + if not code or code >= 400: + print(f"[{seq}] call rejected or error") + return + + if code == 183: + print(f"[{seq}] early media!") + + code: int = await call.wait() + if not code or code >= 400: + print(f"[{seq}] call rejected or error") + return + + print(f"[{seq}] call answered ({code})!") + print(f"[{seq}] answer req to rtpengine...") + + req = { + "command": "create answer", + "call-id": call.call_id(), + "from-tag": call.from_tag(), + "sdp": call.sdp(), + "audio player": "force", + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + except: + print(f"[{seq}] exception") + return + + print(f"[{seq}] running!") + print(f"[{seq}] wait for finish...") + + if args.teardown: + asyncio.create_task(teardown(call, args.teardown, seq)) + + await call.finished() + + print(f"[{seq}] delete req to rtpengine...") + + req = { + "command": "delete", + "call-id": call.call_id(), + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + except: + print(f"[{seq}] exception") + return + + print(f"[{seq}] call finished") + + +def log(s: str) -> None: + print(s) + + +async def main() -> None: + global running, ua + + logger = pysip_lite.Logger(log, 1 if args.debug else 8) + + print("registering...") + ok = await ua.register() + print("registered!") + assert ok + + num_calls = 0 + tasks = [] + + while running and (not args.num or num_calls < args.num): + print("interval wait...") + await asyncio.sleep(args.wait) + + if not running: + break + + num_calls += 1 + + if not args.multi: + await make_call() + else: + t = asyncio.create_task(make_call()) + tasks.append(t) + + await asyncio.gather(*tasks) + await logger + + +async def close() -> None: + global running, ua + + print("unregistering and closing...") + running = False + await ua.unregister() + + +eventloop = asyncio.new_event_loop() +mt = eventloop.create_task(main()) +try: + eventloop.run_until_complete(mt) +except KeyboardInterrupt as e: + eventloop.run_until_complete(close()) + eventloop.run_until_complete(mt) +eventloop.close() diff --git a/python/demos/3_gui.py b/python/demos/3_gui.py new file mode 100644 index 000000000..c58a531ec --- /dev/null +++ b/python/demos/3_gui.py @@ -0,0 +1,640 @@ +import asyncio +import aiohttp +import aiofiles +import base64 +from guizero import * +import pysip_lite +import typing +import argparse +import string +import random + +parser = argparse.ArgumentParser( + description="Register as a SIP UA and interactively manipulate calls.", + epilog="Example: --user bench2user000005 --domain guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain", +) + +parser.add_argument( + "--user", + required=True, + help="SIP user name to register UA", + metavar="USER", +) +parser.add_argument( + "--domain", + required=True, + help="SIP user name to register UA", + metavar="USER", +) +parser.add_argument( + "--pw", + required=True, + help="Password for SIP registration", + metavar="PASSWORD", +) +parser.add_argument( + "--rtpe", required=True, help="HTTP URI for rtpengine", metavar="HTTP-URI" +) +parser.add_argument( + "--addr", + required=False, + help="Local SIP IP address to bind to (default 0.0.0.0)", + default="0.0.0.0", + metavar="IP", +) +parser.add_argument( + "--port", + required=False, + help="Local SIP port to bind to", + default=15062, + metavar="NUM", + type=int, +) +parser.add_argument( + "--proto", + required=False, + help="SIP transport protocol (default UDP)", + default="UDP", + choices=["UDP", "TCP"], + metavar="PROTO", +) +parser.add_argument( + "--debug", + help="Enable debug output", + action="store_true", +) + +args = parser.parse_args() + + +prov = pysip_lite.Provider(args.addr, args.port, args.proto) +ua = prov.user_agent(f"sip:{args.user}@{args.domain}", args.pw) + +rtpe = args.rtpe + + +class call_entry: + _value: str = None + _idx: int = None + state: str = None + call: pysip_lite.Call = None + + def __init__(self, list_val: str, state: str): + global call_list, call_cnnct + + self._idx = len(call_list.items) + self._value = f"{self._idx}: {list_val}" + call_list.append(self._value) + call_cnnct.append(self._value) + self.state = state + + def update(self, list_val: str, state: str) -> None: + global call_list, call_cnnct + + call_list.remove(self._value) + call_cnnct.remove(self._value) + self._value = f"{self._idx}: {list_val}" + call_list.insert(self._idx, self._value) + call_cnnct.insert(self._idx, self._value) + self.state = state + + +calls: typing.List[call_entry] = [] + +suffix = "_" + "".join( + random.choices(string.ascii_uppercase + string.digits, k=5) +) + +running = True + + +def do_shutdown(): + global running + running = False + + +app = App(title="SIP/rtpengine demo", height=800, width=1000) + +app.when_closed = do_shutdown + + +def resolve(f: asyncio.Future, val: typing.Any) -> None: + f.set_result(val) + + +async def msgbox(text) -> None: + w = Window(app, height=200, width=400) + text = Text(w, text=text) + f = asyncio.get_running_loop().create_future() + ok_btn = PushButton(w, command=resolve, args=[f, True], text="OK") + await f + w.destroy() + + +async def rtpe_req(req: dict) -> dict: + print("Request to rtpengine:") + print(req) + try: + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + except Exception as e: + await msgbox("Exception during JSON request to rtpengine: " + str(e)) + return None + + print("Response from rtpengine:") + print(resp) + + return resp + + +async def do_call(uri: str) -> None: + global call_to_txt, codecs_txt + + if not uri: + await msgbox("missing destination URI") + return + + if not uri.startswith("sip:"): + uri = "sip:" + uri + + if not "@" in uri: + uri = uri + f"@{args.domain}" + + req = { + "command": "create", + } + + c: typing.List[str] = codecs_txt.value.split() + + if c: + req["codec"] = {"offer": c} + + resp = await rtpe_req(req) + if not resp: + return + + call = ua.create(uri, resp["call-id"], resp["from-tag"], resp["sdp"]) + c = call_entry(f"pending call (to {uri})", "n") + c.call = call + calls.append(c) + + code: int = await call.wait(180) + + if code == 180: + c.update(f"ringing (to {uri})", "or") + res = await call.wait() + + if res == 200: + c.update(f"running (to {uri})", "r") + + req = { + "command": "create answer", + "call-id": call.call_id(), + "from-tag": call.from_tag(), + "sdp": call.sdp(), + "audio player": "force", + } + + resp = await rtpe_req(req) + if not resp: + call.stop() + + await call.finished() + else: + print("rejected") + + c.update("terminated", "t") + + req = { + "command": "delete", + "call-id": call.call_id(), + } + + await rtpe_req(req) + + c.call = None + + +def do_call_btn() -> None: + asyncio.create_task(do_call(call_to_txt.value)) + + +call_to_box = Box(app, layout="grid") +call_to_btn = PushButton( + call_to_box, + command=do_call_btn, + text="Call to:", + grid=[0, 0], + enabled=False, +) +call_to_txt = TextBox(call_to_box, width=80, grid=[1, 0]) + +Text(call_to_box, text="Codecs:", grid=[0, 1]) +codecs_txt = TextBox( + call_to_box, width=80, grid=[1, 1], text="opus AMR-WB G722 AMR PCMA PCMU" +) + + +call_list = ListBox(app, width="fill", multiselect=True) + + +def get_call(v: str) -> call_entry: + if v is None: + return None + va = v.split(" ") + i = va[0] + ia = i.split(":") + n = int(ia[0]) + return calls[n] + + +def get_calls(box: ListBox) -> typing.List[call_entry]: + if not box.value: + return [] + return [get_call(i) for i in box.value] + + +async def rtpe_answer(c: call_entry) -> None: + if c.state != "ir": + await msgbox("call is not in ringing state") + return + + call = c.call + + f = call.from_addr() + c.update(f"answering (from {f})", "ira") + + sdp = call.sdp() + + req = { + "command": "publish", + "call-id": call.call_id(), + "from-tag": call.from_tag(), + "sdp": sdp, + "audio player": "force", + "flags": ["bidirectional"], + } + + resp = await rtpe_req(req) + if not resp: + call.reject(500) + return + + try: + call.answer(resp["sdp"]) + except: + await msgbox("error") + return + + c.update(f"running (from {f})", "r") + + +def do_answer() -> None: + cs = get_calls(call_list) + if not cs: + asyncio.create_task(msgbox("no call selected")) + return + + for c in cs: + asyncio.create_task(rtpe_answer(c)) + + +def do_close() -> None: + cs = get_calls(call_list) + if not cs: + asyncio.create_task(msgbox("no call selected")) + return + + for c in cs: + if c.state == "r": + c.call.stop() + elif c.state == "ir": + c.call.reject(480) + else: + asyncio.create_task(msgbox(f"call '{c._value}' is in wrong state")) + + +ar_buttons = Box(app, layout="grid") +answer_btn = PushButton( + ar_buttons, command=do_answer, text="Answer", grid=[0, 0] +) +reject_btn = PushButton( + ar_buttons, command=do_close, text="Close", grid=[1, 0] +) + + +async def do_play(fn: str) -> None: + cs = get_calls(call_list) + if not cs: + await msgbox("no call selected") + return + + try: + async with aiofiles.open(fn, "rb") as f: + b: bytes = await f.read() + except: + await msgbox("error reading file") + return + + for c in cs: + call = c.call + + if call is None or c.state != "r": + await msgbox("call is not running") + return + + req = { + "command": "play media", + "call-id": call.call_id(), + "from-tag": call.from_tag(), + "blob64": base64.encodebytes(b).decode(), + } + + await rtpe_req(req) + + +def do_play_btn() -> None: + global play_txt + asyncio.create_task(do_play(play_txt.value)) + + +play_grid = Box(app, layout="grid") +play_btn = PushButton( + play_grid, command=do_play_btn, text="Play:", grid=[0, 0] +) +play_txt = TextBox( + play_grid, + text="263655_2064400-lq.mp3", + grid=[1, 0], + width=80, +) + + +async def do_transfer(c1: call_entry, c2: call_entry) -> None: + global cnnct_info, transfer_chk, bidirect_chk + + flags = [] + + if not transfer_chk.value: + flags.append("directional") + if bidirect_chk.value: + flags.append("bidirectional") + + info = "Using 'connect' method" + info = info + " with flags: " + ", ".join(flags) + cnnct_info.value = info + else: + cnnct_info.value = "Info: using simple 'connect' method" + + req = { + "command": "connect", + "call-id": c1.call.call_id(), + "from-tag": c1.call.from_tag(), + "to-call-id": c2.call.call_id(), + "to-tag": c2.call.from_tag(), + "flags": flags, + } + + await rtpe_req(req) + + +async def do_mesh( + from_calls: typing.List[call_entry], to_calls: typing.List[call_entry] +) -> None: + global transfer_chk, cnnct_info, bidirect_chk + + flags = [] + + if transfer_chk.value: + flags.append("unsubscribe") + if bidirect_chk.value: + flags.append("bidirectional") + + calls = set() + tags = [] + + for c in from_calls: + calls.add(c.call.call_id()) + tag = {"from": c.call.from_tag(), "to": []} + for d in to_calls: + if c is d: + continue + calls.add(d.call.call_id()) + tag["to"].append(d.call.from_tag()) + tags.append(tag) + + req = { + "command": "mesh", + "calls": list(calls), + "tags": tags, + "flags": flags, + } + + info = "Using 'mesh' method" + if flags: + info = info + " with flags: " + ", ".join(flags) + + cnnct_info.value = info + + await rtpe_req(req) + + +async def do_connect() -> None: + global call_list, call_cnnct, transfer_chk, cnnct_info + + to_calls = get_calls(call_list) + if not to_calls: + await msgbox("no to-call selected") + return + + from_calls = get_calls(call_cnnct) + if not from_calls: + await msgbox("no from-call selected") + return + + if len(to_calls) > 1 or len(from_calls) > 1: + await do_mesh(from_calls, to_calls) + return + + await do_transfer(from_calls[0], to_calls[0]) + + +async def do_disconnect() -> None: + global call_list, call_cnnct, transfer_chk, bidirect_chk + + to_calls = get_calls(call_list) + if not to_calls: + await msgbox("no to-call selected") + return + + from_calls = get_calls(call_cnnct) + if not from_calls: + await msgbox("no from-call selected") + return + + if len(to_calls) > 1 or len(from_calls) > 1: + await msgbox("can only disconnect from/to a single call") + return + + c1 = from_calls[0] + c2 = to_calls[0] + + flags = ["directional"] + + if bidirect_chk: + cnnct_info.value = ( + "Info: using 'unsubscribe' method with 'bidirectional' flag" + ) + flags.append("bidirectional") + else: + cnnct_info.value = "Info: using 'unsubscribe' method" + + await rtpe_req( + { + "command": "unsubscribe", + "call-id": c1.call.call_id(), + "from-tag": c1.call.from_tag(), + "to-tag": c2.call.from_tag(), + "flags": flags, + } + ) + + +def do_cnct_btn() -> None: + asyncio.create_task(do_connect()) + + +def do_dcnt_btn() -> None: + asyncio.create_task(do_disconnect()) + + +cd_grid = Box(app, layout="grid") +cnct_btn = PushButton( + cd_grid, command=do_cnct_btn, text="Connect to:", grid=[0, 0] +) +transfer_chk = CheckBox(cd_grid, text="Full transfer", grid=[1, 0]) +dcnt_btn = PushButton( + cd_grid, command=do_dcnt_btn, text="Disconnect from:", grid=[2, 0] +) +bidirect_chk = CheckBox(cd_grid, text="Both ways", grid=[3, 0]) + +call_cnnct = ListBox(app, width="fill", multiselect=True) +cnnct_info = Text(app, text="Info:", width="fill") + + +async def do_speak(txt: str) -> None: + cs = get_calls(call_list) + if not cs: + await msgbox("no call selected") + return + + try: + proc = await asyncio.create_subprocess_exec( + "espeak", + "--stdout", + txt, + stdin=None, + stdout=asyncio.subprocess.PIPE, + ) + b, _ = await proc.communicate() + except Exception as e: + await msgbox(f"error exexuting espeak: {e}") + return + + for c in cs: + call = c.call + + req = { + "command": "play media", + "call-id": call.call_id(), + "from-tag": call.from_tag(), + "blob64": base64.encodebytes(b).decode(), + } + + await rtpe_req(req) + + +def do_speak_btn() -> None: + global speak_txt + asyncio.create_task(do_speak(speak_txt.value)) + + +speak_btn = PushButton( + play_grid, command=do_speak_btn, text="Speak:", grid=[0, 1] +) +speak_txt = TextBox(play_grid, grid=[1, 1], width=80) + + +async def handle_call(call: pysip_lite.Call) -> None: + c = call_entry("incoming call", "n") + c.call = call + calls.append(c) + + try: + call.ringing() + f = call.from_addr() + c.update(f"ringing (from {f})", "ir") + + await call.finished() + except: + print("some error") + + c.update("terminated", "t") + + await rtpe_req( + { + "command": "delete", + "call-id": call.call_id(), + } + ) + + c.call = None + + +async def ua_main() -> None: + global ua, running + + ok = await ua.register() + assert ok + + call_to_btn.enable() + + ua.listen() + + while running: + call = await ua.receive() + if call: + asyncio.create_task(handle_call(call)) + + +def log(s: str) -> None: + print(s) + + +async def app_main(): + global running, app + + logger = pysip_lite.Logger(log, 1 if args.debug else 8) + + r = eventloop.create_task(ua_main()) + + while running: + app.update() + await asyncio.sleep(0.05) + + call_to_btn.disable() + + await ua.unregister() + await r + await logger + + +eventloop = asyncio.new_event_loop() +mt = eventloop.create_task(app_main()) +try: + eventloop.run_until_complete(mt) +except KeyboardInterrupt as e: + running = False + eventloop.run_until_complete(mt) +eventloop.close() diff --git a/python/demos/4_b2b.py b/python/demos/4_b2b.py new file mode 100644 index 000000000..0780516a8 --- /dev/null +++ b/python/demos/4_b2b.py @@ -0,0 +1,235 @@ +import asyncio +import aiohttp +import pysip_lite +import argparse +import string +import random + +parser = argparse.ArgumentParser( + description="Register as a SIP UA and act as a proxy to another UA.", + epilog="Example: --uri sip:bench2user000005@guest02-snail.lab.sipwise.com --pw testuser --rtpe http://localhost:9911/ng-plain --to sip:bench2user000001@guest02-snail.lab.sipwise.com", +) + +parser.add_argument( + "--uri", required=True, help="SIP URI to register UA", metavar="SIP-URI" +) +parser.add_argument( + "--pw", + required=True, + help="Password for SIP registration", + metavar="PASSWORD", +) +parser.add_argument( + "--rtpe", required=True, help="HTTP URI for rtpengine", metavar="HTTP-URI" +) +parser.add_argument( + "--to", required=True, help="SIP URI to make calls to", metavar="SIP-URI" +) +parser.add_argument( + "--codecs", + required=False, + help="Additional odecs to offer", + metavar="CODEC,CODEC,CODEC,...", + default="", +) +parser.add_argument( + "--audio-player", + help="Force use of audio player", + action="store_true", +) +parser.add_argument( + "--addr", + required=False, + help="Local SIP IP address to bind to (default 0.0.0.0)", + default="0.0.0.0", + metavar="IP", +) +parser.add_argument( + "--port", + required=False, + help="Local SIP port to bind to", + default=15063, + metavar="NUM", + type=int, +) +parser.add_argument( + "--proto", + required=False, + help="SIP transport protocol (default UDP)", + default="UDP", + choices=["UDP", "TCP"], + metavar="PROTO", +) +parser.add_argument( + "--debug", + help="Enable debug output", + action="store_true", +) + +args = parser.parse_args() + + +prov = pysip_lite.Provider(args.addr, args.port, args.proto) +ua = prov.user_agent(args.uri, args.pw) + +to = args.to + +rtpe = args.rtpe + +suffix = "_" + "".join( + random.choices(string.ascii_uppercase + string.digits, k=5) +) + + +async def wait_finish(A: pysip_lite.Call, B: pysip_lite.Call) -> None: + await B.finished() + try: + A.stop() + except: + pass + + +async def answer_with( + code: int, A: pysip_lite.Call, B: pysip_lite.Call +) -> None: + req = { + "command": "answer", + "call-id": A.call_id() + suffix, + "from-tag": A.from_tag(), + "to-tag": B.to_tag(), + "sdp": B.sdp(), + } + + if args.audio_player: + req["audio player"] = "force" + + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + + A.answer(resp["sdp"], code) + + +async def handle_call(A: pysip_lite.Call) -> None: + print("new call") + sdp: str = A.sdp() + + try: + print("offer to rtpengine") + + req = { + "command": "offer", + "call-id": A.call_id() + suffix, + "from-tag": A.from_tag(), + "sdp": sdp, + "codec": {"transcode": args.codecs.split(",")}, + } + + if args.audio_player: + req["audio player"] = "force" + + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + resp: dict = await post.json() + + print("invite to B leg") + + B = ua.create(to, A.call_id() + "_b2b", A.from_tag(), resp["sdp"]) + + if not B: + print("failed to create call") + raise RuntimeError("error") + + asyncio.create_task(wait_finish(B, A)) + asyncio.create_task(wait_finish(A, B)) + + code: int = await B.wait(180) + while code == 180: + print("ringing") + A.ringing() + code: int = await B.wait(180) + + while code == 183: + if B.sdp: + print("early media") + await answer_with(183, A, B) + + code: int = await B.wait(183) + + if code != 200: + print("rejected or error") + raise RuntimeError("rejected") + + print("answered!") + + await answer_with(200, A, B) + + print("call running") + + await A.finished() + B.stop() + except Exception as e: + print(e) + try: + A.stop() + B.stop() + except: + pass + + print("call end") + + req = { + "command": "delete", + "call-id": A.call_id() + suffix, + } + + async with aiohttp.ClientSession() as session: + async with session.post(rtpe, json=req) as post: + await post.json() + + +def log(s: str) -> None: + print(s) + + +running = True + + +async def main() -> None: + global running, ua + + logger = pysip_lite.Logger(log, 1 if args.debug else 8) + + print("registering...") + ok = await ua.register() + if not ok: + print("failed to register!") + return + print("registered!") + + ua.listen() + + while running: + call = await ua.receive() + if call: + asyncio.create_task(handle_call(call)) + + await logger + + +async def close() -> None: + global running, ua + + print("unregistering and closing...") + running = False + await ua.unregister() + + +eventloop = asyncio.new_event_loop() +mt = eventloop.create_task(main()) +try: + eventloop.run_until_complete(mt) +except KeyboardInterrupt as e: + eventloop.run_until_complete(close()) + eventloop.run_until_complete(mt) +eventloop.close() diff --git a/python/pysip_lite/.gitignore b/python/pysip_lite/.gitignore new file mode 100644 index 000000000..5ddc4ed4e --- /dev/null +++ b/python/pysip_lite/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.so diff --git a/python/pysip_lite/Call.py b/python/pysip_lite/Call.py new file mode 100644 index 000000000..f580f2fc9 --- /dev/null +++ b/python/pysip_lite/Call.py @@ -0,0 +1,127 @@ +from ctypes import * +import typing +from ._types import * +from ._lib import * +from ._Waiter import * + + +class Call: + """Represents a SIP call.""" + + _ci: call_info = None + _aw: Waiter = None + + def __init__(self, ci: call_info, answer_waiter: Waiter = None): + self._ci = ci + self._aw = answer_waiter + + def __del__(self): + lib.bsw_call_destroy(self._ci.call) + + def sdp(self) -> str: + """Returns the (remote) SDP body""" + return self._ci.body.decode() + + def call_id(self) -> str: + """Returns the call ID""" + return self._ci.call_id.decode() + + def from_addr(self) -> str: + """Returns the SIP "from" address""" + return self._ci.from_addr.decode() + + def from_tag(self) -> str: + """Returns the SIP "from" tag""" + return self._ci.from_tag.decode() + + def to_tag(self) -> str: + """Returns the SIP "to" tag""" + return self._ci.to_tag.decode() + + def ringing(self, sdp: str = None) -> None: + """Sets a pending incoming call to "ringing" state by sending a 180 + response""" + ok: bool = lib.bsw_call_answer( + self._ci.call, 180, sdp.encode() if sdp else None + ) + if not ok: + raise RuntimeError("wrong call state") + + def reject(self, code: int) -> None: + """Rejects a pending incoming call with an error code. + + :param code: integer SIP error code (>= 400) + """ + lib.bsw_call_answer(self._ci.call, code, None) + + def answer(self, sdp: str = None, code: int = 200) -> None: + """Answers a pending incoming call. + + :param sdp: SDP body to answer with + :param code: SIP response code (defaults to 200, or 183) + """ + ok: bool = lib.bsw_call_answer( + self._ci.call, code, sdp.encode() if sdp else None + ) + if not ok: + raise RuntimeError("wrong call state") + + async def finished(self) -> None: + """Wait for an established or pending call to terminate""" + w = Waiter() + done: bool = lib.bsw_call_finished(self._ci.call, w.fd()) + if not done: + await w.wait() + + def stop(self) -> None: + """Stop an existing call. + + Sends a BYE for an established call, or CANCEL for a pending outgoing + call. For a pending incoming call, sends an appropriate response code. + """ + ok: bool = lib.bsw_call_terminate(self._ci.call) + if not ok: + raise RuntimeError("call not established") + + async def wait(self, state: int = 200) -> int: + """Wait for a state change on an outgoing call. + + Returns the new state as a SIP response code. + + The state to wait for defaults to 200, which means wait for the call to + be fully established. To wait for a "ringing" state, wait for code + 180, and wait for 183 to catch early media. + + The returned state can be different from what was waited for (e.g. >= + 400 for a failed call when waiting for 200, or 200 when waiting for + 18x). + + :param state: state to wait for as integer SIP code + """ + assert self._aw + + while True: + res: str = await self._aw.wait_val() + if res == "0": + return False + + ci = call_info() + code: int = lib.bsw_call_wait(self._ci.call, byref(ci)) + + if code == 183 or code == 200: + if code == 183 and state >= 200: + continue + + # copy remote data + self._ci.body = ci.body + self._ci.content_type = ci.content_type + self._ci.to_tag = ci.to_tag + + return code + if code >= 400: + return 400 + if code == 180: + if state >= 200: + continue + return 180 + return code # ? diff --git a/python/pysip_lite/Logger.py b/python/pysip_lite/Logger.py new file mode 100644 index 000000000..01c3d7291 --- /dev/null +++ b/python/pysip_lite/Logger.py @@ -0,0 +1,39 @@ +import asyncio +import typing +from ._Waiter import Waiter +from ._lib import * + + +class Logger: + """Registers a callback to receive log messages and sets the log level. + + The object must be "await"ed for to close the logging context. + + :param fn: callable (taking one string argument) to pass log messages to + :param log_level: belle-sip log level (1 for full debug) + """ + + _fn: typing.Callable[[str], None] = None + _w: Waiter = None + _t: asyncio.Task = None + + def __init__(self, fn: typing.Callable[[str], None], log_level: int = 4): + self._fn = fn + self._w = Waiter() + self._t = asyncio.create_task(self._wait()) + + lib.bsw_set_logger(log_level, self._w.fd()) + + async def _wait(self): + while self._w: + ok: bool = await self._w.wait() + if not ok: + break + s = c_char_p(b"\0" * 8192) + ok = lib.bsw_get_log(s, 8192) + if ok: + self._fn(s.value.decode()) + + def __await__(self) -> None: + self._w.close() # wakes up _wait() + yield from self._t diff --git a/python/pysip_lite/Provider.py b/python/pysip_lite/Provider.py new file mode 100644 index 000000000..6f22baf8d --- /dev/null +++ b/python/pysip_lite/Provider.py @@ -0,0 +1,33 @@ +from ._UserAgent import _UserAgent +from ctypes import * +from ._types import * +from ._lib import * + + +class Provider: + """Base class required for all SIP communications. + + Opens and binds a socket to a local interface address and port. + + :param addr: Local IP address (default 0.0.0.0) + :param port: Local port to bind to + :param proto: Protocol (UDP or TCP) + """ + + _prov: provider_ptr = None + + def __init__( + self, addr: str = "0.0.0.0", port: int = 0, proto: str = "UDP" + ) -> None: + self._prov = lib.bsw_provider(addr.encode(), port, proto.encode()) + + if not self._prov: + raise RuntimeError("failed to create SIP provider") + + def user_agent(self, uri: str, pw: str) -> _UserAgent: + """Create a user agent. + + :param uri: SIP uri for the UA + :param pw: Password + """ + return _UserAgent(self, uri, pw) diff --git a/python/pysip_lite/_UserAgent.py b/python/pysip_lite/_UserAgent.py new file mode 100644 index 000000000..4dab6a806 --- /dev/null +++ b/python/pysip_lite/_UserAgent.py @@ -0,0 +1,111 @@ +import typing +from ._Waiter import Waiter +from ._types import * +from ._lib import * +from .Call import * + +Provider = typing.NewType("Provider", None) + + +class _UserAgent: + """Represents a SIP user agent which can make and receive calls.""" + + _prov: Provider = None + _uri: str = None + _pw: str = None + _w: Waiter = None + + def __init__(self, prov: Provider, uri: str, pw: str): + self._prov = prov + self._uri = uri + self._pw = pw + self._reg = False + + async def register(self, dur=600) -> bool: + """Register the user agent with the given password. + + :param dur: Duration of the registration in seconds + """ + w = Waiter() + lib.bsw_register( + self._prov._prov, + w.fd(), + self._uri.encode(), + self._pw.encode(), + dur, + ) + res: bool = await w.wait() + if dur: + self._reg = res + return res + + async def unregister(self) -> None: + """Remove an existing registration.""" + res: bool = await self.register(dur=0) + self._reg = False + self.unlisten() + + def listen(self) -> None: + """Listen for incoming calls.""" + assert self._w is None + self._w = Waiter() + lib.bsw_listen(self._prov._prov, self._w.fd(), self._uri.encode()) + + def unlisten(self) -> None: + """Undo the .listen() operation.""" + if not self._w: + return + # wake up the listener (self.receive()) + lib.bsw_listen(self._prov._prov, -1, self._uri.encode()) + self._w = None + + async def receive(self) -> Call | None: + """Wait for an incoming call and returns a new call object. + + Returns None when .unlisten() is called. + + .listen() must have been called before.""" + while self._w is not None: + ok: bool = await self._w.wait() + if not ok: + continue + ci = call_info() + res: bool = lib.bsw_receive( + self._prov._prov, self._uri.encode(), byref(ci) + ) + if not res: + continue + return Call(ci) + + def create( + self, to_addr: str, call_id: str, from_tag: str, sdp: str + ) -> Call: + """Create a new outgoing call and sends an INVITE. + + Returns a new call object. + + :param to_addr: SIP uri to make the call to + :param call_id: call ID for the new call + :param from_tag: "From" header tag for the INVITE + :param sdp: complete SDP body + """ + w = Waiter() + ci = call_info( + call_id=call_id.encode(), + from_addr=self._uri.encode(), + from_tag=from_tag.encode(), + to_addr=to_addr.encode(), + ) + if sdp is not None: + ci.body = sdp.encode() + # XXX allow 200/ACK SDP exchange + ci.call = lib.bsw_call_create(self._prov._prov, byref(ci), w.fd()) + # ok: bool = await w.wait() # XXX allow time-out handling, safe with byref(ci) + + # clear for remote SDP + ci.body = b"" + ci.content_type = b"" + + c = Call(ci, w) + + return c diff --git a/python/pysip_lite/_Waiter.py b/python/pysip_lite/_Waiter.py new file mode 100644 index 000000000..28a81fd5a --- /dev/null +++ b/python/pysip_lite/_Waiter.py @@ -0,0 +1,40 @@ +import asyncio +import os + + +class Waiter: + _r: int = None + _w: int = None + + def __init__(self): + self._r, self._w = os.pipe() + + def __del__(self): + self.close() + if self._r: + os.close(self._r) + self._r = None + + def close(self) -> None: + if self._w: + os.close(self._w) + self._w = None + + def fd(self) -> int: + return self._w + + def _reader(self, r: int, f: asyncio.Future) -> None: + b = os.read(r, 1) + f.set_result(b.decode()) + + async def wait_val(self) -> str: + f = asyncio.get_event_loop().create_future() + asyncio.get_event_loop().add_reader( + self._r, lambda: self._reader(self._r, f) + ) + res: str = await f + asyncio.get_event_loop().remove_reader(self._r) + return res + + async def wait(self) -> bool: + return await self.wait_val() == "1" diff --git a/python/pysip_lite/__init__.py b/python/pysip_lite/__init__.py new file mode 100644 index 000000000..59e3a7b2a --- /dev/null +++ b/python/pysip_lite/__init__.py @@ -0,0 +1,7 @@ +__name__ = "pysip_lite" +__package__ = "pysip_lite" +__version__ = "1.0.0" +__author__ = "Richard Fuchs" + +from .Provider import Provider +from .Logger import Logger diff --git a/python/pysip_lite/_lib.py b/python/pysip_lite/_lib.py new file mode 100644 index 000000000..36c34492a --- /dev/null +++ b/python/pysip_lite/_lib.py @@ -0,0 +1,65 @@ +from ctypes import * +from ._types import * +import os + +lib: CDLL = CDLL( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "belle-wrap.so") +) +assert lib + + +# primary lib init +lib.bsw_init.argtypes = [] +lib.bsw_init.restype = c_bool + +# set up logging callback +lib.bsw_set_logger.argtypes = [c_int, c_int] +lib.bsw_set_logger.restype = None + +# receive one log message +lib.bsw_get_log.argtypes = [c_char_p, c_size_t] +lib.bsw_get_log.restype = c_bool + +# create a provider +lib.bsw_provider.argtypes = [c_char_p, c_int, c_char_p] +lib.bsw_provider.restype = provider_ptr + +# register a UA +lib.bsw_register.argtypes = [provider_ptr, c_int, c_char_p, c_char_p, c_int] +lib.bsw_register.restype = None + +# listen for incoming calls +lib.bsw_listen.argtypes = [provider_ptr, c_int, c_char_p] +lib.bsw_listen.restype = None + +# wait for one incoming call +lib.bsw_receive.argtypes = [provider_ptr, c_char_p, call_info_ptr] +lib.bsw_receive.restype = c_bool + +# destroy a call object +lib.bsw_call_destroy.argtypes = [call_ptr] +lib.bsw_call_destroy.restype = None + +# answer one incoming call +lib.bsw_call_answer.argtypes = [call_ptr, c_int, c_char_p] +lib.bsw_call_answer.restype = bool + +# wait for end of call +lib.bsw_call_finished.argtypes = [call_ptr, c_int] +lib.bsw_call_finished.restype = bool + +# close a running call +lib.bsw_call_terminate.argtypes = [call_ptr] +lib.bsw_call_terminate.restype = bool + +# initiate an outgoing call +lib.bsw_call_create.argtypes = [provider_ptr, call_info_ptr, c_int] +lib.bsw_call_create.restype = call_ptr + +# return state of outgoing call answer +lib.bsw_call_wait.argtypes = [call_ptr, call_info_ptr] +lib.bsw_call_wait.restype = c_int + + +if not lib.bsw_init(): + raise RuntimeError("lib init failed") diff --git a/python/pysip_lite/_types.py b/python/pysip_lite/_types.py new file mode 100644 index 000000000..6ba893a0c --- /dev/null +++ b/python/pysip_lite/_types.py @@ -0,0 +1,33 @@ +from ctypes import * + +# opaque C types + + +class call(Structure): + pass + + +class provider_t(Structure): + pass + + +provider_ptr = POINTER(provider_t) + + +call_ptr = POINTER(call) + + +class call_info(Structure): + _fields_ = [ + ("call_id", c_char * 256), + ("body", c_char * 8192), + ("content_type", c_char * 256), + ("from_addr", c_char * 256), + ("to_addr", c_char * 256), + ("from_tag", c_char * 256), + ("to_tag", c_char * 256), + ("call", call_ptr), + ] + + +call_info_ptr = POINTER(call_info) diff --git a/python/src/.gitignore b/python/src/.gitignore new file mode 100644 index 000000000..5761abcfd --- /dev/null +++ b/python/src/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/python/src/belle-wrap.c b/python/src/belle-wrap.c new file mode 100644 index 000000000..6040c7f6a --- /dev/null +++ b/python/src/belle-wrap.c @@ -0,0 +1,1242 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +// XXX replace char* with char[] below + +struct call_info; + +struct auth_info { + char *username; + char *domain; + char *passwd; +}; + +struct transaction { + int response_waker; + + // handler: + void (*response)(struct transaction *, int code, const belle_sip_response_event_t *, + belle_sip_response_t *, belle_sip_client_transaction_t *, belle_sip_provider_t *); + + // for register & invite: + bool auth; + bool done; + + // for invite: + struct call *call; +}; + +struct listener { + char *username; + char *domain; + int incoming_waker; + GQueue calls; // struct call +}; + +// corresponds to a sip_dialog_t +struct call { + belle_sip_provider_t *sip_provider; + belle_sip_dialog_t *dlg; + belle_sip_server_transaction_t *strans; // pending received invite + belle_sip_client_transaction_t *ctrans; // pending outgoing invite + enum { + CS_INC_PENDING = 0, + CS_INC_RINGING, // 180 sent + CS_INC_MEDIA, // 183 sent + CS_OUT_PENDING, // INVITE sent + CS_OUT_RINGING, // 180 received + CS_OUT_MEDIA, // 183 received + CS_ESTABLISHED, + CS_DEAD, + } state; + int terminate_waker; +}; + +struct call_info { + char call_id[256]; + char body[8192]; // SDP etc + char content_type[256]; + char from_addr[256]; + char to_addr[256]; + char from_tag[256]; + char to_tag[256]; + struct call *call; +}; + + +static belle_sip_stack_t *sipstack; +static belle_sip_main_loop_t *main_loop; +static pthread_mutex_t main_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t run_mutex = PTHREAD_MUTEX_INITIALIZER; +static int wake_fds[2]; + +static pthread_mutex_t auth_mutex = PTHREAD_MUTEX_INITIALIZER; +static GHashTable *auth_info; + +static pthread_mutex_t listen_mutex = PTHREAD_MUTEX_INITIALIZER; +static GHashTable *listeners; + +static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; +static GQueue logs = G_QUEUE_INIT; +static int log_fd = -1; + + + +#define FD_TO_PTR(x) ((void *) ((long) (x))) +#define PTR_TO_FD(x) ((int) ((long) (x))) + + + +static struct auth_info *get_auth_info(const char *un, const char *dom) { + if (!un || !dom) + return NULL; + + const struct auth_info req = {.username = (char *) un, .domain = (char *) dom}; + return g_hash_table_lookup(auth_info, &req); +} + + +#define CONCAT2(a, b) a ## b +#define CONCAT(a, b) CONCAT2(a, b) + +typedef struct { + pthread_mutex_t *m; +} lock_t; +static void auto_unlock(lock_t *l) { + pthread_mutex_unlock(l->m); +} +static lock_t auto_lock(pthread_mutex_t *l) { + pthread_mutex_lock(l); + return (lock_t) {l}; +} +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(lock_t, auto_unlock); +#define LOCK(m) g_auto(lock_t) CONCAT(__l, __COUNTER__) __attribute__((unused)) = auto_lock(m) + + +static void set_auth_info(const char *un, const char *dom, const char *pw) { + if (!un || !dom || !pw) + return; // XXX fail? + + LOCK(&auth_mutex); + + struct auth_info *ai = get_auth_info(un, dom); + if (!ai) { + ai = g_new0(struct auth_info, 1); + ai->username = g_strdup(un); + ai->domain = g_strdup(dom); + g_hash_table_insert(auth_info, ai, ai); + } + + g_free(ai->passwd); + ai->passwd = g_strdup(pw); +} + + +static void log_handler(const char *domain, BctbxLogLevel lev, const char *fmt, va_list args) +{ + char *s = g_strdup_vprintf(fmt, args); + char *p = g_strdup_printf("[%d] [%s] %s", lev, domain, s); + g_free(s); + + LOCK(&log_mutex); + g_queue_push_tail(&logs, p); + + if (log_fd != -1) + (void) write(log_fd, "1", 1); +} + + +static void log_int(BctbxLogLevel lev, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + log_handler("internal", lev, fmt, ap); + va_end(ap); +} + + +#define ASSERT_RET_VAL(a, v, m, ...) do { \ + if (G_UNLIKELY(!(a))) { \ + log_int(BCTBX_LOG_TRACE, "[%s:%d]" m, __FUNCTION__, __LINE__, ##__VA_ARGS__); \ + return v; \ + } \ +} while (0) + +#define ASSERT_RET(a, m, ...) ASSERT_RET_VAL(a, (void)0, m, ##__VA_ARGS__) +#define ASSERT_RET_NULL(a, m, ...) ASSERT_RET_VAL(a, NULL, m, ##__VA_ARGS__) +#define ASSERT_RET_FALSE(a, m, ...) ASSERT_RET_VAL(a, false, m, ##__VA_ARGS__) + + +static void auth_resp(struct transaction *t, const belle_sip_response_event_t *event, + belle_sip_client_transaction_t *trans, + belle_sip_provider_t *sip_provider) +{ + struct call *c = NULL; + + belle_sip_dialog_t *dlg = belle_sip_response_event_get_dialog(event); + if (dlg) { + c = belle_sip_dialog_get_application_data(dlg); + ASSERT_RET(c, "auth response without dialog"); + belle_sip_dialog_set_application_data(dlg, NULL); + } + + belle_sip_request_t *req = belle_sip_client_transaction_create_authenticated_request(trans, + NULL, NULL); + ASSERT_RET(req, "failed to cast transaction to request"); + + // create new transaction and move transaction & call objects + belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), NULL); + + trans = belle_sip_provider_create_client_transaction(sip_provider, req); + ASSERT_RET(trans, "failed to create client transaction"); + + belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), t); + t->auth = true; + + if (dlg) { + belle_sip_dialog_set_application_data(dlg, NULL); + + dlg = belle_sip_provider_create_dialog(sip_provider, BELLE_SIP_TRANSACTION(trans)); + belle_sip_dialog_set_application_data(dlg, c); + + if (c->dlg) + belle_sip_object_unref(c->dlg); + + c->dlg = dlg; + belle_sip_object_ref(c->dlg); + } + + belle_sip_client_transaction_send_request_to(trans, NULL); +} + + +static void reg_response(struct transaction *t, int code, + const belle_sip_response_event_t *event, + belle_sip_response_t *res, + belle_sip_client_transaction_t *trans, belle_sip_provider_t *sip_provider) +{ + if (code == 401 && !t->auth) + auth_resp(t, event, trans, sip_provider); + else if (code == 200) { + if (!t->done) { + t->done = true; + write(t->response_waker, "1", 1); + + belle_sip_refresher_t *rfr = belle_sip_client_transaction_create_refresher(trans); + ASSERT_RET(rfr, "failed to create refresher"); + // XXX remove refresher on unreg + } + } + else if (code >= 400) { + if (!t->done) { + t->done = true; + write(t->response_waker, "0", 1); + } + } + else { + // XXX ? + } +} + + +static void transaction_free(struct transaction *t) { + g_free(t); +} + + +static void res_event(void *user_ctx, const belle_sip_response_event_t *event) +{ + ASSERT_RET(event, "empty event"); + + belle_sip_provider_t *sip_provider = user_ctx; + + belle_sip_response_t *res = belle_sip_response_event_get_response(event); + ASSERT_RET(res, "no response from event"); + + int code = belle_sip_response_get_status_code(res); + + belle_sip_client_transaction_t *trans = belle_sip_response_event_get_client_transaction(event); + ASSERT_RET(trans, "no transaction from event"); + + struct transaction *t = belle_sip_transaction_get_application_data(BELLE_SIP_TRANSACTION(trans)); + + ASSERT_RET(t, "no user transaction object"); + + t->response(t, code, event, res, trans, sip_provider); +} + + +static void ua_unlisten(const char *un, const char *dom) { + const struct listener lx = {.username = (char *) un, .domain = (char *) dom}; + + LOCK(&listen_mutex); + + struct listener *l = NULL; + g_hash_table_steal_extended(listeners, &lx, (void **) &l, NULL); + + if (!l) + return; + + write(l->incoming_waker, "0", 1); + g_free(l); +} + + +static void ua_listen(const char *un, const char *dom, int fd) { + if (fd == -1) { + ua_unlisten(un, dom); + return; + } + + struct listener *l = g_new0(struct listener, 1); + l->username = g_strdup(un); + l->domain = g_strdup(dom); + l->incoming_waker = fd; + + LOCK(&listen_mutex); + ASSERT_RET(g_hash_table_lookup(listeners, l) == NULL, "duplicate listener"); + g_hash_table_insert(listeners, l, l); +} + + +static struct listener *get_listener(const char *un, const char *dom) { + const struct listener l = {.username = (char *) un, .domain = (char *) dom}; + return g_hash_table_lookup(listeners, &l); +} + + +static void req_invite(belle_sip_provider_t *sip_provider, belle_sip_request_t *req, + const belle_sip_request_event_t *event) +{ + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req); + + belle_sip_server_transaction_t *trans = belle_sip_request_event_get_server_transaction(event); + ASSERT_RET(trans == NULL, "server transaction already exists"); + + trans = belle_sip_provider_create_server_transaction(sip_provider, req); + ASSERT_RET(trans, "failed to create server transaction"); + + belle_sip_header_to_t *to = belle_sip_message_get_header_by_type(msg, belle_sip_header_to_t); + ASSERT_RET(to, "failed to create TO header"); + + belle_sip_header_address_t *toa = BELLE_SIP_HEADER_ADDRESS(to); + ASSERT_RET(toa, "failed to cast to header address"); + + belle_sip_uri_t *to_uri = belle_sip_header_address_get_uri(toa); + ASSERT_RET(to_uri, "failed to get URI"); + + belle_sip_dialog_t *dlg = belle_sip_request_event_get_dialog(event); + ASSERT_RET(!dlg, "dialog already exists"); + + LOCK(&listen_mutex); + struct listener *l = get_listener(belle_sip_uri_get_user(to_uri), belle_sip_uri_get_host(to_uri)); + if (!l) { + belle_sip_response_t *res = belle_sip_response_create_from_request(req, 410); + ASSERT_RET(res, "failed to create response"); + + belle_sip_provider_send_response(sip_provider, res); + + return; + } + + dlg = belle_sip_provider_create_dialog(sip_provider, BELLE_SIP_TRANSACTION(trans)); + ASSERT_RET(dlg, "failed to create dialog"); + + struct call *c = g_new0(struct call, 1); + c->terminate_waker = -1; + + c->sip_provider = sip_provider; + belle_sip_object_ref(sip_provider); + + c->dlg = dlg; + belle_sip_object_ref(dlg); + + c->strans = trans; + belle_sip_object_ref(trans); + + belle_sip_dialog_set_application_data(dlg, c); + + // call belongs to listener + g_queue_push_tail(&l->calls, c); + + write(l->incoming_waker, "1", 1); +} + + +static void req_cancel(belle_sip_provider_t *sip_provider, belle_sip_request_t *req, + const belle_sip_request_event_t *event) +{ + belle_sip_dialog_t *dlg = belle_sip_request_event_get_dialog(event); + ASSERT_RET(dlg, "no dialog"); + + struct call *c = belle_sip_dialog_get_application_data(dlg); + ASSERT_RET(c, "no call object"); + + if (c->state != CS_DEAD) { + if (c->terminate_waker != -1) + write(c->terminate_waker, "1", 1); + + c->state = CS_DEAD; + } + + // send "cancelled" + belle_sip_request_t *ireq = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(c->strans)); + ASSERT_RET(ireq, "no request from transaction"); + + belle_sip_response_t *res = belle_sip_response_create_from_request(ireq, 487); + ASSERT_RET(res, "failed to create response"); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res); + ASSERT_RET(msg, "failed to cast to message"); + + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new())); + + belle_sip_server_transaction_send_response(c->strans, res); + + // ok to cancel + res = belle_sip_response_create_from_request(req, 200); + ASSERT_RET(res, "failed to create response"); + + belle_sip_provider_send_response(c->sip_provider, res); +} + + +static void req_bye(belle_sip_provider_t *sip_provider, belle_sip_request_t *req, + const belle_sip_request_event_t *event) +{ + belle_sip_dialog_t *dlg = belle_sip_request_event_get_dialog(event); + if (!dlg) { + // XXX send resp? + return; + } + + struct call *c = belle_sip_dialog_get_application_data(dlg); + ASSERT_RET(c, "no application call object"); + + if (c->state != CS_DEAD) { + if (c->terminate_waker != -1) + write(c->terminate_waker, "1", 1); + + c->state = CS_DEAD; + } + + belle_sip_server_transaction_t *trans = belle_sip_request_event_get_server_transaction(event); + ASSERT_RET(!trans, "server transaction already exists"); + + trans = belle_sip_provider_create_server_transaction(sip_provider, req); + ASSERT_RET(trans, "failed to create transaction"); + + belle_sip_response_t *res = belle_sip_response_create_from_request(req, 200); + ASSERT_RET(res, "failed to create response"); + + belle_sip_server_transaction_send_response(trans, res); +} + + +static void req_event(void *user_ctx, const belle_sip_request_event_t *event) +{ + ASSERT_RET(event, "empty event"); + + belle_sip_provider_t *sip_provider = user_ctx; + + belle_sip_request_t *req = belle_sip_request_event_get_request(event); + ASSERT_RET(req, "no request from event"); + + const char *method = belle_sip_request_get_method(req); + ASSERT_RET(method, "empty method"); + + if (!strcmp(method, "OPTIONS")) { + belle_sip_response_t *res = belle_sip_response_create_from_request(req, 200); + ASSERT_RET(res, "failed to create response"); + + belle_sip_provider_send_response(sip_provider, res); + } + else if (!strcmp(method, "INVITE")) + req_invite(sip_provider, req, event); + else if (!strcmp(method, "CANCEL")) + req_cancel(sip_provider, req, event); + else if (!strcmp(method, "BYE")) + req_bye(sip_provider, req, event); + else if (!strcmp(method, "ACK")) + {} + else { + belle_sip_response_t *res = belle_sip_response_create_from_request(req, 501); + ASSERT_RET(res, "failed to create response"); + + belle_sip_provider_send_response(sip_provider, res); + } +} + + +static void timeout(void *user_ctx, const belle_sip_timeout_event_t *event) +{ + printf("%s\n", __FUNCTION__); +} + + +static void trans_terminated(void *user_ctx, const belle_sip_transaction_terminated_event_t *event) +{ + belle_sip_transaction_t *trans = NULL; + + belle_sip_client_transaction_t *ctrans = belle_sip_transaction_terminated_event_get_client_transaction(event); + + if (ctrans) + trans = BELLE_SIP_TRANSACTION(ctrans); + else { + belle_sip_server_transaction_t *strans = belle_sip_transaction_terminated_event_get_server_transaction(event); + if (strans) + trans = BELLE_SIP_TRANSACTION(strans); + } + + ASSERT_RET(trans, "no transaction"); + + struct transaction *t = belle_sip_transaction_get_application_data(BELLE_SIP_TRANSACTION(trans)); + + if (t) + transaction_free(t); +} + + +static void dlg_terminated(void *user_ctx, const belle_sip_dialog_terminated_event_t *event) +{ + belle_sip_dialog_t *dlg = belle_sip_dialog_terminated_event_get_dialog(event); + if (!dlg) + return; + + struct call *c = belle_sip_dialog_get_application_data(dlg); + if (!c) + return; + + if (c->state == CS_DEAD) + return; + + if (c->terminate_waker != -1) + write(c->terminate_waker, "1", 1); + + c->state = CS_DEAD; +} + + +static void auth_request(void *user_ctx, belle_sip_auth_event_t *auth_event) +{ + LOCK(&auth_mutex); + + struct auth_info *ai = get_auth_info(belle_sip_auth_event_get_username(auth_event), + belle_sip_auth_event_get_domain(auth_event)); + + if (ai) + belle_sip_auth_event_set_passwd(auth_event, ai->passwd); + // XXX else error +} + + +static void lst_destroy(void *user_ctx) +{ + printf("%s\n", __FUNCTION__); +} + +static void io_error(void *user_ctx, const belle_sip_io_error_event_t *error) +{ + printf("%s\n", __FUNCTION__); +} + +static const belle_sip_listener_callbacks_t callbacks = { + .process_dialog_terminated = dlg_terminated, + .process_io_error = io_error, + .process_request_event = req_event, + .process_response_event = res_event, + .process_timeout = timeout, + .process_transaction_terminated = trans_terminated, + .process_auth_requested = auth_request, + .listener_destroyed = lst_destroy, +}; + + +static void *bg_loop(void *p) { + ASSERT_RET_NULL(main_loop, "library not initialised"); + + LOCK(&run_mutex); + + while (true) { + pthread_mutex_unlock(&run_mutex); + + pthread_mutex_lock(&main_mutex); + + belle_sip_main_loop_run(main_loop); + + pthread_mutex_unlock(&main_mutex); + + pthread_mutex_lock(&run_mutex); + } + + return NULL; +} + + +typedef struct { +} sip_stack_lock_t; + + +static sip_stack_lock_t lock_sip_stack(void) { + assert(main_loop); + + pthread_mutex_lock(&run_mutex); + + (void) write(wake_fds[1], "x", 1); + + pthread_mutex_lock(&main_mutex); + + return (sip_stack_lock_t) {}; +} + + +static void unlock_sip_stack(void) { + pthread_mutex_unlock(&run_mutex); + pthread_mutex_unlock(&main_mutex); +} + + +static void auto_unlock_sip_stack(sip_stack_lock_t *s) { + unlock_sip_stack(); +} +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(sip_stack_lock_t, auto_unlock_sip_stack); +#define LOCK_SIP_STACK() g_auto(sip_stack_lock_t) CONCAT(__l, __COUNTER__) __attribute__((unused)) = lock_sip_stack() + + +static int pipe_read(void *p, unsigned int ev) { + char b; + (void) read(wake_fds[0], &b, 1); + + belle_sip_main_loop_quit(main_loop); + + return BELLE_SIP_CONTINUE; +} + + +static guint auth_info_hash(const void *p) { + const struct auth_info *h = p; + return g_str_hash(h->username) ^ g_str_hash(h->domain); +} + + +static gboolean auth_info_eq(const void *p, const void *q) { + const struct auth_info *a = p; + const struct auth_info *b = q; + return g_str_equal(a->username, b->username) && g_str_equal(a->domain, b->domain); +} + + +static guint listener_hash(const void *p) { + const struct listener *h = p; + return g_str_hash(h->username) ^ g_str_hash(h->domain); +} + + +static gboolean listener_eq(const void *p, const void *q) { + const struct listener *a = p; + const struct listener *b = q; + return g_str_equal(a->username, b->username) && g_str_equal(a->domain, b->domain); +} + + +static void auth_info_free(void *p) { + struct auth_info *h = p; + g_free(h->username); + g_free(h->domain); + g_free(h->passwd); + g_free(h); +} + + +bool bsw_init(void) { + ASSERT_RET_FALSE(!sipstack, "library already initialised"); + + auth_info = g_hash_table_new_full(auth_info_hash, auth_info_eq, auth_info_free, NULL); + listeners = g_hash_table_new(listener_hash, listener_eq); + + bctbx_set_log_handler(log_handler); + + sipstack = belle_sip_stack_new(NULL); + ASSERT_RET_FALSE(sipstack, "failed to initialise library"); + + int ret = pipe2(wake_fds, O_NONBLOCK); // XXX close at exit + ASSERT_RET_FALSE(ret == 0, "failed to create pipe"); + + main_loop = belle_sip_stack_get_main_loop(sipstack); + ASSERT_RET_FALSE(main_loop, "failed to create main loop"); + + belle_sip_source_t *src = belle_sip_socket_source_new(pipe_read, NULL, + wake_fds[0], BELLE_SIP_EVENT_READ, -1); + belle_sip_main_loop_add_source(main_loop, src); + + pthread_t thr; + pthread_create(&thr, NULL, bg_loop, NULL); + + return true; +} + + +void bsw_set_logger(int ll, int fd) { + ASSERT_RET(fd != -1, "invalid logging fd"); + + lock_sip_stack(); + + belle_sip_set_log_level(ll); + + unlock_sip_stack(); + + LOCK(&log_mutex); + + ASSERT_RET(log_fd == -1, "logger already set"); + log_fd = fd; + + for (unsigned int i = 0; i < logs.length; i++) + (void) write(fd, "1", 1); +} + + +bool bsw_get_log(char *o, size_t len) { + LOCK(&log_mutex); + + char *s = g_queue_pop_head(&logs); + + if (!s) { + pthread_mutex_unlock(&log_mutex); + return false; + } + + g_strlcpy(o, s, len); + + g_free(s); + + return true; +} + + +belle_sip_provider_t *bsw_provider(const char *addr, int port, const char *proto) { + LOCK_SIP_STACK(); + + belle_sip_listening_point_t *lp = + belle_sip_stack_create_listening_point(sipstack, addr, port, proto); + ASSERT_RET_NULL(lp, "failed to create listening point"); + belle_sip_provider_t *sip_provider = belle_sip_stack_create_provider(sipstack, lp); + ASSERT_RET_NULL(sip_provider, "failed to create provider"); + belle_sip_listener_t *lstn = belle_sip_listener_create_from_callbacks(&callbacks, sip_provider); + ASSERT_RET_NULL(lstn, "failed to create listener"); + belle_sip_provider_add_sip_listener(sip_provider, lstn); + + return sip_provider; +} + + +void bsw_register(belle_sip_provider_t *sip_provider, int fd, const char *uri, const char *pw, int dur) { + LOCK_SIP_STACK(); + + belle_sip_uri_t *u = belle_sip_uri_parse(uri); // "sip:bench2user000005@guest02-snail.lab.sipwise.com" + ASSERT_RET(u, "failed to parse URI"); + + set_auth_info(belle_sip_uri_get_user(u), belle_sip_uri_get_host(u), pw); + + belle_sip_uri_t *uo = BELLE_SIP_URI(belle_sip_object_clone(BELLE_SIP_OBJECT(u))); + ASSERT_RET(uo, "failed to clone URI"); + belle_sip_uri_set_user(uo, NULL); // "sip:guest02-snail.lab.sipwise.com" + + belle_sip_request_t *req = belle_sip_request_create( + uo, + "REGISTER", + belle_sip_provider_create_call_id(sip_provider), + belle_sip_header_cseq_create(1, "REGISTER"), + belle_sip_header_from_create2(uri, BELLE_SIP_RANDOM_TAG), + belle_sip_header_to_create2(uri, NULL), + belle_sip_header_via_new(), + 70); + ASSERT_RET(req, "failed to create request"); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req); + ASSERT_RET(msg, "failed to cast to message"); + + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_expires_create(dur))); + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new())); + + belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(sip_provider, req); + ASSERT_RET(trans, "failed to create transaction"); + + struct transaction *t = g_new0(struct transaction, 1); + t->response = reg_response; + t->response_waker = fd; + + belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), t); + + int res = belle_sip_client_transaction_send_request_to(trans, NULL); + ASSERT_RET(res == 0, "failed to send request"); +} + + +void bsw_listen(belle_sip_provider_t *sip_provider, int fd, const char *uri) +{ + LOCK_SIP_STACK(); + + belle_sip_uri_t *u = belle_sip_uri_parse(uri); // "sip:bench2user000005@guest02-snail.lab.sipwise.com" + ASSERT_RET(u, "failed to parse URI"); + + ua_listen(belle_sip_uri_get_user(u), belle_sip_uri_get_host(u), fd); +} + + +bool bsw_receive(belle_sip_provider_t *sip_provider, const char *uri, struct call_info *info) +{ + LOCK_SIP_STACK(); + + belle_sip_uri_t *u = belle_sip_uri_parse(uri); // "sip:bench2user000005@guest02-snail.lab.sipwise.com" + ASSERT_RET_FALSE(u, "failed to parse URI"); + + LOCK(&listen_mutex); + + struct listener *l = get_listener(belle_sip_uri_get_user(u), belle_sip_uri_get_host(u)); + if (!l) + return false; + + if (l->calls.length == 0) + return false; + + // transfer ownership to the external object + struct call *c = g_queue_pop_head(&l->calls); + info->call = c; + + belle_sip_request_t *req = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(c->strans)); + ASSERT_RET_FALSE(req, "no request from transaction"); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req); + ASSERT_RET_FALSE(msg, "failed to cast to message"); + + const char *s = belle_sip_message_get_body(msg); + if (s) + g_strlcpy(info->body, s, sizeof(info->body)); + + belle_sip_header_content_type_t *ct = belle_sip_message_get_header_by_type(msg, + belle_sip_header_content_type_t); + if (ct) { + s = belle_sip_header_content_type_get_type(ct); + ASSERT_RET_FALSE(s, "failed to get content-type"); + g_strlcpy(info->content_type, s, sizeof(info->content_type)); + } + + belle_sip_header_from_t *from = belle_sip_message_get_header_by_type(msg, belle_sip_header_from_t); + ASSERT_RET_FALSE(from, "no FROM header"); + + belle_sip_header_address_t *addr = BELLE_SIP_HEADER_ADDRESS(from); + ASSERT_RET_FALSE(addr, "no address in FROM header"); + + belle_sip_uri_t *uh = belle_sip_header_address_get_uri(addr); + ASSERT_RET_FALSE(uh, "no URI in FROM address"); + + const char *un = belle_sip_uri_get_user(uh); + const char *dm = belle_sip_uri_get_host(uh); + const char *tg = belle_sip_header_from_get_tag(from); + + snprintf(info->from_addr, sizeof(info->from_addr), "%s@%s", un, dm); + if (tg) + g_strlcpy(info->from_tag, tg, sizeof(info->from_tag)); + + belle_sip_header_to_t *to = belle_sip_message_get_header_by_type(msg, belle_sip_header_to_t); + ASSERT_RET_FALSE(to, "no TO header"); + + addr = BELLE_SIP_HEADER_ADDRESS(to); + ASSERT_RET_FALSE(addr, "no address in TO header"); + + uh = belle_sip_header_address_get_uri(addr); + ASSERT_RET_FALSE(uh, "no URI in TO address"); + + un = belle_sip_uri_get_user(uh); + dm = belle_sip_uri_get_host(uh); + tg = belle_sip_header_to_get_tag(to); + + snprintf(info->to_addr, sizeof(info->to_addr), "%s@%s", un, dm); + if (tg) + g_strlcpy(info->to_tag, tg, sizeof(info->to_tag)); + + belle_sip_header_call_id_t *cidh = belle_sip_message_get_header_by_type(msg, belle_sip_header_call_id_t); + ASSERT_RET_FALSE(cidh, "no CALL ID header"); + const char *cid = belle_sip_header_call_id_get_call_id(cidh); + ASSERT_RET_FALSE(cid, "empty CALL ID header"); + + g_strlcpy(info->call_id, cid, sizeof(info->call_id)); + + return true; +} + +void bsw_call_destroy(struct call *c) { + LOCK_SIP_STACK(); + + ASSERT_RET(c->dlg, "no dialog associated with call"); + + belle_sip_dialog_set_application_data(c->dlg, NULL); + if (c->strans) + belle_sip_object_unref(c->strans); + if (c->ctrans) + belle_sip_object_unref(c->ctrans); + belle_sip_object_unref(c->dlg); + + // XXX make sure info/pointers from pending invite transaction is removed + + g_free(c); +} + + +static bool bsw_call_reply(struct call *c, int code, const char *sdp) { + if (code == 180) { + if (c->state != CS_INC_PENDING) + return false; + c->state = CS_INC_RINGING; + } + else if (code == 183) { + if (c->state != CS_INC_PENDING && c->state != CS_INC_RINGING) + return false; + c->state = CS_INC_MEDIA; + } + else if (code == 200) { + if (c->state != CS_INC_PENDING && c->state != CS_INC_RINGING && c->state != CS_INC_MEDIA) + return false; + c->state = CS_ESTABLISHED; + } + else if (code >= 400) { + if (c->state != CS_INC_PENDING && c->state != CS_INC_RINGING && c->state != CS_INC_MEDIA) + return false; + + if (c->terminate_waker != -1) + write(c->terminate_waker, "1", 1); + + c->state = CS_DEAD; + } + else + return false; + + belle_sip_request_t *req = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(c->strans)); + ASSERT_RET_FALSE(req, "no request from transaction"); + + belle_sip_response_t *res = belle_sip_response_create_from_request(req, code); + ASSERT_RET_FALSE(res, "failed to create response"); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res); + ASSERT_RET_FALSE(msg, "failed to cast to message"); + + if (sdp) { + belle_sip_message_set_body(msg, sdp, strlen(sdp)); + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_content_type_create( + "application", "sdp"))); + } + + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new())); + + belle_sip_server_transaction_send_response(c->strans, res); + + return true; +} + + +bool bsw_call_answer(struct call *c, int code, const char *sdp) { + LOCK_SIP_STACK(); + + ASSERT_RET_FALSE(c->sip_provider, "provider link missing"); + ASSERT_RET_FALSE(c->dlg, "no dialog associated with call"); + ASSERT_RET_FALSE(c->strans, "no transaction associated with call"); + + return bsw_call_reply(c, code, sdp); +} + + +bool bsw_call_finished(struct call *c, int fd) { + ASSERT_RET_FALSE(fd != -1, "invalid fd"); + + LOCK_SIP_STACK(); + + if (c->state == CS_DEAD) + return true; + + c->terminate_waker = fd; + + return false; +} + + +static bool bsw_call_bye(struct call *c) { + belle_sip_request_t *req = belle_sip_dialog_create_request(c->dlg, "BYE"); + ASSERT_RET_FALSE(req, "failed to create request"); + + belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(c->sip_provider, + req); + ASSERT_RET_FALSE(trans, "failed to create transaction"); + + belle_sip_client_transaction_send_request(trans); + + if (c->terminate_waker != -1) + write(c->terminate_waker, "1", 1); + + c->state = CS_DEAD; + + return true; +} + + +static bool bsw_call_cancel(struct call *c) { + belle_sip_request_t *req = belle_sip_dialog_create_request(c->dlg, "BYE"); + ASSERT_RET_FALSE(req, "failed to create request"); + + belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(c->sip_provider, + req); + ASSERT_RET_FALSE(trans, "failed to create transaction"); + + belle_sip_client_transaction_send_request(trans); + + if (c->terminate_waker != -1) + write(c->terminate_waker, "1", 1); + + c->state = CS_DEAD; + + return true; +} + + +bool bsw_call_terminate(struct call *c) { + LOCK_SIP_STACK(); + + ASSERT_RET_FALSE(c->sip_provider, "missing provider link"); + ASSERT_RET_FALSE(c->dlg, "no dialog associated with call"); + + if (c->state == CS_DEAD) + return false; + + switch (c->state) { + case CS_DEAD: + return false; + + case CS_ESTABLISHED: + return bsw_call_bye(c); + + case CS_OUT_PENDING: + case CS_OUT_RINGING: + case CS_OUT_MEDIA: + return bsw_call_cancel(c); + + case CS_INC_PENDING: + case CS_INC_RINGING: + case CS_INC_MEDIA: + return bsw_call_reply(c, 407, NULL); + } + + return false; +} + + +static void inv_response(struct transaction *t, int code, + const belle_sip_response_event_t *event, + belle_sip_response_t *res, + belle_sip_client_transaction_t *trans, belle_sip_provider_t *sip_provider) +{ + belle_sip_dialog_t *dlg = belle_sip_response_event_get_dialog(event); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res); + ASSERT_RET(msg, "failed to cast to message"); + + struct call *c = t->call; + + if (code == 407 && !t->auth) + auth_resp(t, event, trans, sip_provider); + else if (code == 180) { + if (c && c->state == CS_OUT_PENDING) { + c->state = CS_OUT_RINGING; + write(t->response_waker, "1", 1); + } + + if (c->ctrans) + belle_sip_object_unref(c->ctrans); + c->ctrans = trans; + belle_sip_object_ref(trans); + } + else if (code == 183) { + if (c && (c->state == CS_OUT_PENDING || c->state == CS_OUT_RINGING)) { + c->state = CS_OUT_MEDIA; + write(t->response_waker, "1", 1); + } + + if (c->ctrans) + belle_sip_object_unref(c->ctrans); + c->ctrans = trans; + belle_sip_object_ref(trans); + } + else if (code == 200) { + if (!t->done) { + t->done = true; + write(t->response_waker, "1", 1); + + if (c) + c->state = CS_ESTABLISHED; + } + + + if (c->ctrans) + belle_sip_object_unref(c->ctrans); + c->ctrans = trans; + belle_sip_object_ref(trans); + + ASSERT_RET(dlg, "no dialog"); + + belle_sip_request_t *ack = belle_sip_dialog_create_ack(dlg, + belle_sip_dialog_get_local_seq_number(dlg)); + ASSERT_RET(ack, "failed to create ACK"); + + belle_sip_message_remove_header(BELLE_SIP_MESSAGE(ack), "Proxy-Authorization"); + + belle_sip_dialog_send_ack(dlg, ack); + + t->call = NULL; + } + else if (code >= 400) { + if (!t->done) { + t->done = true; + write(t->response_waker, "0", 1); + + if (c) + c->state = CS_DEAD; + } + + //t->info = NULL; + t->call = NULL; + } + else { + // XXX ? + } +} + + +struct call *bsw_call_create(belle_sip_provider_t *sip_provider, struct call_info *info, int fd) +{ + ASSERT_RET_NULL(fd != -1, "invalid fd"); + + struct call *c = g_new0(struct call, 1); + c->terminate_waker = -1; + c->state = CS_OUT_PENDING; + + struct transaction *t = g_new0(struct transaction, 1); + t->response = inv_response; + t->response_waker = fd; + t->call = c; + + LOCK_SIP_STACK(); + + c->sip_provider = sip_provider; + belle_sip_object_ref(sip_provider); + + belle_sip_uri_t *tu = belle_sip_uri_parse(info->to_addr); + ASSERT_RET_NULL(tu, "failed to parse URI"); + + belle_sip_header_call_id_t *cidh = belle_sip_header_call_id_new(); + ASSERT_RET_NULL(cidh, "failed to create CALL ID header"); + belle_sip_header_call_id_set_call_id(cidh, info->call_id); + + belle_sip_header_from_t *fh = belle_sip_header_from_create2(info->from_addr, info->from_tag); // "sip:...@..." + ASSERT_RET_NULL(fh, "failed to create FROM header"); + + belle_sip_header_address_t *ta = belle_sip_header_address_create(NULL, tu); + ASSERT_RET_NULL(ta, "failed to create TO address"); + + belle_sip_header_to_t *th = belle_sip_header_to_create(ta, NULL); + ASSERT_RET_NULL(th, "failed to create TO header"); + + belle_sip_request_t *req = belle_sip_request_create(tu, "INVITE", + cidh, belle_sip_header_cseq_create(20, "INVITE"), + fh, th, belle_sip_header_via_new(), 70); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(req); + + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_contact_new())); + + size_t body_len = strlen(info->body); + if (body_len) { + belle_sip_message_set_body(msg, info->body, strlen(info->body)); + belle_sip_message_add_header(msg, BELLE_SIP_HEADER(belle_sip_header_content_type_create( + "application", "sdp"))); + } + + belle_sip_client_transaction_t *trans = belle_sip_provider_create_client_transaction(sip_provider, req); + + belle_sip_dialog_t *dlg = belle_sip_provider_create_dialog(sip_provider, BELLE_SIP_TRANSACTION(trans)); + ASSERT_RET_NULL(dlg, "failed to create dialog"); + + c->dlg = dlg; + belle_sip_object_ref(dlg); + + belle_sip_transaction_set_application_data(BELLE_SIP_TRANSACTION(trans), t); + belle_sip_dialog_set_application_data(dlg, c); + + c->ctrans = trans; + belle_sip_object_ref(trans); + + belle_sip_client_transaction_send_request(trans); + + return c; +} + + +int bsw_call_wait(struct call *call, struct call_info *info) { + int ret; + + LOCK_SIP_STACK(); + + ASSERT_RET_VAL(call->ctrans, -1, "no client transaction"); + + belle_sip_response_t *res = belle_sip_transaction_get_response(BELLE_SIP_TRANSACTION(call->ctrans)); + ASSERT_RET_VAL(res, -1, "no response from transaction"); + + belle_sip_message_t *msg = BELLE_SIP_MESSAGE(res); + ASSERT_RET_VAL(msg, -1, "failed to cast to message"); + + const char *s = belle_sip_message_get_body(msg); + if (s) + g_strlcpy(info->body, s, sizeof(info->body)); + + belle_sip_header_content_type_t *ct = belle_sip_message_get_header_by_type(msg, + belle_sip_header_content_type_t); + if (ct) { + s = belle_sip_header_content_type_get_type(ct); + ASSERT_RET_VAL(s, -1, "failed to get content-type"); + g_strlcpy(info->content_type, s, sizeof(info->content_type)); + } + + switch (call->state) { + case CS_DEAD: + ret = 400; + break; + + case CS_OUT_PENDING: + ret = 100; + break; + + case CS_OUT_RINGING: + ret = 180; + break; + + case CS_OUT_MEDIA: + ret = 183; + break; + + case CS_ESTABLISHED: + ret = 200; + break; + + default: + ret = 500; + break; + } + + return ret; +}