diff --git a/tests/rtt/rtt_red/configs/ast1/extensions.conf b/tests/rtt/rtt_red/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_red/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_red/configs/ast1/manager.conf b/tests/rtt/rtt_red/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_red/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_red/configs/ast1/pjsip.conf b/tests/rtt/rtt_red/configs/ast1/pjsip.conf new file mode 100755 index 000000000..51f6827ee --- /dev/null +++ b/tests/rtt/rtt_red/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_red/run-test b/tests/rtt/rtt_red/run-test new file mode 100755 index 000000000..59d173639 --- /dev/null +++ b/tests/rtt/rtt_red/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=2','--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--dis-codec=t140', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if not rx_saw_english and not tx_saw_greek and not rx_saw_german and not tx_saw_chinese and not rx_saw_armenian and not tx_saw_hindi: + LOGGER.info("SUCCESS: No RTT exchange T140 disabled, as expected!") + print("SUCCESS.") + self.passed = True + else: + if rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) saw 'English'") + if x_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) saw 'Greek'") + if rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) saw 'German'") + if tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) saw 'Chinese'") + if rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) saw 'Armenian'") + if _saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_red/test-config.yaml b/tests/rtt/rtt_red/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_red/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_red_audio/configs/ast1/extensions.conf b/tests/rtt/rtt_red_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_red_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_red_audio/configs/ast1/manager.conf b/tests/rtt/rtt_red_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_red_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_red_audio/configs/ast1/pjsip.conf b/tests/rtt/rtt_red_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..51f6827ee --- /dev/null +++ b/tests/rtt/rtt_red_audio/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_red_audio/run-test b/tests/rtt/rtt_red_audio/run-test new file mode 100755 index 000000000..ae2608043 --- /dev/null +++ b/tests/rtt/rtt_red_audio/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=2','--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=ilbc', '--dis-codec=g722', '--dis-codec=t140', + '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if not rx_saw_english and not tx_saw_greek and not rx_saw_german and not tx_saw_chinese and not rx_saw_armenian and not tx_saw_hindi: + LOGGER.info("SUCCESS: No RTT exchange T140 disabled, as expected!") + print("SUCCESS.") + self.passed = True + else: + if rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) saw 'English'") + if tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_red_audio/test-config.yaml b/tests/rtt/rtt_red_audio/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_red_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140/configs/ast1/extensions.conf b/tests/rtt/rtt_red_t140/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_red_t140/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140/configs/ast1/manager.conf b/tests/rtt/rtt_red_t140/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_red_t140/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140/configs/ast1/pjsip.conf b/tests/rtt/rtt_red_t140/configs/ast1/pjsip.conf new file mode 100755 index 000000000..aee440245 --- /dev/null +++ b/tests/rtt/rtt_red_t140/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140/run-test b/tests/rtt/rtt_red_t140/run-test new file mode 100755 index 000000000..92b893ea7 --- /dev/null +++ b/tests/rtt/rtt_red_t140/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=2','--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140/test-config.yaml b/tests/rtt/rtt_red_t140/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_red_t140/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140_audio/configs/ast1/extensions.conf b/tests/rtt/rtt_red_t140_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_red_t140_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140_audio/configs/ast1/manager.conf b/tests/rtt/rtt_red_t140_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_red_t140_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140_audio/configs/ast1/pjsip.conf b/tests/rtt/rtt_red_t140_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..aee440245 --- /dev/null +++ b/tests/rtt/rtt_red_t140_audio/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140_audio/run-test b/tests/rtt/rtt_red_t140_audio/run-test new file mode 100755 index 000000000..3bb780cd7 --- /dev/null +++ b/tests/rtt/rtt_red_t140_audio/run-test @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=2','--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=ilbc', '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_red_t140_audio/test-config.yaml b/tests/rtt/rtt_red_t140_audio/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_red_t140_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_t140/configs/ast1/extensions.conf b/tests/rtt/rtt_t140/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_t140/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_t140/configs/ast1/manager.conf b/tests/rtt/rtt_t140/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_t140/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_t140/configs/ast1/pjsip.conf b/tests/rtt/rtt_t140/configs/ast1/pjsip.conf new file mode 100755 index 000000000..4e08b7900 --- /dev/null +++ b/tests/rtt/rtt_t140/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_t140/run-test b/tests/rtt/rtt_t140/run-test new file mode 100755 index 000000000..d9acb62ae --- /dev/null +++ b/tests/rtt/rtt_t140/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--dis-codec=red', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_t140/test-config.yaml b/tests/rtt/rtt_t140/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_t140/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_t140_audio/configs/ast1/extensions.conf b/tests/rtt/rtt_t140_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_t140_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_t140_audio/configs/ast1/manager.conf b/tests/rtt/rtt_t140_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_t140_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_t140_audio/configs/ast1/pjsip.conf b/tests/rtt/rtt_t140_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..4e08b7900 --- /dev/null +++ b/tests/rtt/rtt_t140_audio/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_t140_audio/run-test b/tests/rtt/rtt_t140_audio/run-test new file mode 100755 index 000000000..2f52ffab2 --- /dev/null +++ b/tests/rtt/rtt_t140_audio/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=ilbc', '--dis-codec=g722', '--dis-codec=red', + '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not rx_saw_chinese: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not rx_saw_hindi: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_t140_audio/test-config.yaml b/tests/rtt/rtt_t140_audio/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_t140_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red/configs/ast1/extensions.conf b/tests/rtt/rtt_t140_red/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_t140_red/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red/configs/ast1/manager.conf b/tests/rtt/rtt_t140_red/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_t140_red/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red/configs/ast1/pjsip.conf b/tests/rtt/rtt_t140_red/configs/ast1/pjsip.conf new file mode 100755 index 000000000..3a50096c1 --- /dev/null +++ b/tests/rtt/rtt_t140_red/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red/run-test b/tests/rtt/rtt_t140_red/run-test new file mode 100755 index 000000000..aaaded820 --- /dev/null +++ b/tests/rtt/rtt_t140_red/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=0','--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red/test-config.yaml b/tests/rtt/rtt_t140_red/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_t140_red/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red_audio/configs/ast1/extensions.conf b/tests/rtt/rtt_t140_red_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/rtt/rtt_t140_red_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red_audio/configs/ast1/manager.conf b/tests/rtt/rtt_t140_red_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/rtt/rtt_t140_red_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red_audio/configs/ast1/pjsip.conf b/tests/rtt/rtt_t140_red_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..3a50096c1 --- /dev/null +++ b/tests/rtt/rtt_t140_red_audio/configs/ast1/pjsip.conf @@ -0,0 +1,91 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red_audio/run-test b/tests/rtt/rtt_t140_red_audio/run-test new file mode 100755 index 000000000..8c29c55db --- /dev/null +++ b/tests/rtt/rtt_t140_red_audio/run-test @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class RTTTest(TestCase): + + def __init__(self): + super(RTTTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(RTTTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=0','--no-vad', + '--log-level=3', '--app-log-level=3', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(RTTTest, self).stop_reactor() + + +def main(): + test = RTTTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/rtt/rtt_t140_red_audio/test-config.yaml b/tests/rtt/rtt_t140_red_audio/test-config.yaml new file mode 100755 index 000000000..52d31dc4b --- /dev/null +++ b/tests/rtt/rtt_t140_red_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102.' +test-modules: + test-object: + config-section: rtt-test + typename: run-test.RTTTest +rtt-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/rtt/tests.yaml b/tests/rtt/tests.yaml new file mode 100755 index 000000000..a0565f4ab --- /dev/null +++ b/tests/rtt/tests.yaml @@ -0,0 +1,10 @@ +# Enter tests here in the order they should be considered for execution: +tests: + - test: 'rtt_red_t140' + - test: 'rtt_red_t140_audio' + - test: 'rtt_t140_red' + - test: 'rtt_t140_red_audio' + - test: 'rtt_red' + - test: 'rtt_red_audio' + - test: 'rtt_t140' + - test: 'rtt_t140_audio' diff --git a/tests/srtp/srtp_red_t140/configs/ast1/extensions.conf b/tests/srtp/srtp_red_t140/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/srtp/srtp_red_t140/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140/configs/ast1/manager.conf b/tests/srtp/srtp_red_t140/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/srtp/srtp_red_t140/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140/configs/ast1/pjsip.conf b/tests/srtp/srtp_red_t140/configs/ast1/pjsip.conf new file mode 100755 index 000000000..3b4b7fd92 --- /dev/null +++ b/tests/srtp/srtp_red_t140/configs/ast1/pjsip.conf @@ -0,0 +1,93 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +media_encryption=sdes +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +media_encryption=sdes +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140/run-test b/tests/srtp/srtp_red_t140/run-test new file mode 100755 index 000000000..3d8edd712 --- /dev/null +++ b/tests/srtp/srtp_red_t140/run-test @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class SRTPTest(TestCase): + + def __init__(self): + super(SRTPTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(SRTPTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=2','--no-vad', + '--log-level=3', '--app-log-level=3', + '--use-srtp=2', '--srtp-secure=0', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(SRTPTest, self).stop_reactor() + + +def main(): + test = SRTPTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140/test-config.yaml b/tests/srtp/srtp_red_t140/test-config.yaml new file mode 100755 index 000000000..a43c0477a --- /dev/null +++ b/tests/srtp/srtp_red_t140/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional SRTP RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102 using SRTP.' +test-modules: + test-object: + config-section: srtp-test + typename: run-test.SRTPTest +srtp-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140_audio/configs/ast1/extensions.conf b/tests/srtp/srtp_red_t140_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/srtp/srtp_red_t140_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140_audio/configs/ast1/manager.conf b/tests/srtp/srtp_red_t140_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/srtp/srtp_red_t140_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140_audio/configs/ast1/pjsip.conf b/tests/srtp/srtp_red_t140_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..3b4b7fd92 --- /dev/null +++ b/tests/srtp/srtp_red_t140_audio/configs/ast1/pjsip.conf @@ -0,0 +1,93 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +media_encryption=sdes +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,red,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +media_encryption=sdes +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140_audio/run-test b/tests/srtp/srtp_red_t140_audio/run-test new file mode 100755 index 000000000..5cde33e21 --- /dev/null +++ b/tests/srtp/srtp_red_t140_audio/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class SRTPTest(TestCase): + + def __init__(self): + super(SRTPTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(SRTPTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=2','--no-vad', + '--log-level=3', '--app-log-level=3', + '--use-srtp=2', '--srtp-secure=0', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=ilbc', '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(SRTPTest, self).stop_reactor() + + +def main(): + test = SRTPTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/srtp/srtp_red_t140_audio/test-config.yaml b/tests/srtp/srtp_red_t140_audio/test-config.yaml new file mode 100755 index 000000000..a43c0477a --- /dev/null +++ b/tests/srtp/srtp_red_t140_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional SRTP RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102 using SRTP.' +test-modules: + test-object: + config-section: srtp-test + typename: run-test.SRTPTest +srtp-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/srtp/srtp_t140/configs/ast1/extensions.conf b/tests/srtp/srtp_t140/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/srtp/srtp_t140/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/srtp/srtp_t140/configs/ast1/manager.conf b/tests/srtp/srtp_t140/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/srtp/srtp_t140/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/srtp/srtp_t140/configs/ast1/pjsip.conf b/tests/srtp/srtp_t140/configs/ast1/pjsip.conf new file mode 100755 index 000000000..fd7ef9622 --- /dev/null +++ b/tests/srtp/srtp_t140/configs/ast1/pjsip.conf @@ -0,0 +1,93 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +media_encryption=sdes +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +media_encryption=sdes +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/srtp/srtp_t140/run-test b/tests/srtp/srtp_t140/run-test new file mode 100755 index 000000000..84b4bbebd --- /dev/null +++ b/tests/srtp/srtp_t140/run-test @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class SRTPTest(TestCase): + + def __init__(self): + super(SRTPTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(SRTPTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--no-vad', + '--log-level=3', '--app-log-level=3', + '--use-srtp=2', '--srtp-secure=0', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--dis-codec=red', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(SRTPTest, self).stop_reactor() + + +def main(): + test = SRTPTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/srtp/srtp_t140/test-config.yaml b/tests/srtp/srtp_t140/test-config.yaml new file mode 100755 index 000000000..a43c0477a --- /dev/null +++ b/tests/srtp/srtp_t140/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional SRTP RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102 using SRTP.' +test-modules: + test-object: + config-section: srtp-test + typename: run-test.SRTPTest +srtp-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/srtp/srtp_t140_audio/configs/ast1/extensions.conf b/tests/srtp/srtp_t140_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/srtp/srtp_t140_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/srtp/srtp_t140_audio/configs/ast1/manager.conf b/tests/srtp/srtp_t140_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/srtp/srtp_t140_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/srtp/srtp_t140_audio/configs/ast1/pjsip.conf b/tests/srtp/srtp_t140_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..fd7ef9622 --- /dev/null +++ b/tests/srtp/srtp_t140_audio/configs/ast1/pjsip.conf @@ -0,0 +1,93 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +media_encryption=sdes +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +media_encryption=sdes +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/srtp/srtp_t140_audio/run-test b/tests/srtp/srtp_t140_audio/run-test new file mode 100755 index 000000000..d18f362f8 --- /dev/null +++ b/tests/srtp/srtp_t140_audio/run-test @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class SRTPTest(TestCase): + + def __init__(self): + super(SRTPTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(SRTPTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--no-vad', + '--log-level=3', '--app-log-level=3', + '--use-srtp=2', '--srtp-secure=0', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=ilbc', '--dis-codec=g722', '--dis-codec=red', + '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not rx_saw_chinese: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not rx_saw_hindi: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(SRTPTest, self).stop_reactor() + + +def main(): + test = SRTPTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/srtp/srtp_t140_audio/test-config.yaml b/tests/srtp/srtp_t140_audio/test-config.yaml new file mode 100755 index 000000000..a43c0477a --- /dev/null +++ b/tests/srtp/srtp_t140_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional SRTP RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102 using SRTP.' +test-modules: + test-object: + config-section: srtp-test + typename: run-test.SRTPTest +srtp-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red/configs/ast1/extensions.conf b/tests/srtp/srtp_t140_red/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/srtp/srtp_t140_red/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red/configs/ast1/manager.conf b/tests/srtp/srtp_t140_red/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/srtp/srtp_t140_red/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red/configs/ast1/pjsip.conf b/tests/srtp/srtp_t140_red/configs/ast1/pjsip.conf new file mode 100755 index 000000000..bf8248830 --- /dev/null +++ b/tests/srtp/srtp_t140_red/configs/ast1/pjsip.conf @@ -0,0 +1,93 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +media_encryption=sdes +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +media_encryption=sdes +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red/run-test b/tests/srtp/srtp_t140_red/run-test new file mode 100755 index 000000000..8993bb0b7 --- /dev/null +++ b/tests/srtp/srtp_t140_red/run-test @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class SRTPTest(TestCase): + + def __init__(self): + super(SRTPTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(SRTPTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=0','--no-vad', + '--log-level=3', '--app-log-level=3', + '--use-srtp=2', '--srtp-secure=0', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=pcmu', '--dis-codec=ilbc', + '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(SRTPTest, self).stop_reactor() + + +def main(): + test = SRTPTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red/test-config.yaml b/tests/srtp/srtp_t140_red/test-config.yaml new file mode 100755 index 000000000..a43c0477a --- /dev/null +++ b/tests/srtp/srtp_t140_red/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional SRTP RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102 using SRTP.' +test-modules: + test-object: + config-section: srtp-test + typename: run-test.SRTPTest +srtp-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red_audio/configs/ast1/extensions.conf b/tests/srtp/srtp_t140_red_audio/configs/ast1/extensions.conf new file mode 100755 index 000000000..074d329af --- /dev/null +++ b/tests/srtp/srtp_t140_red_audio/configs/ast1/extensions.conf @@ -0,0 +1,13 @@ +[general] + +[globals] + +[rtt-test] + +exten => 101,1,NoOp(Calling 101) + same => n,Dial(PJSIP/101) + same => n,Hangup() + +exten => 102,1,NoOp(Calling 102) + same => n,Dial(PJSIP/102) + same => n,Hangup() \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red_audio/configs/ast1/manager.conf b/tests/srtp/srtp_t140_red_audio/configs/ast1/manager.conf new file mode 100755 index 000000000..835676cfa --- /dev/null +++ b/tests/srtp/srtp_t140_red_audio/configs/ast1/manager.conf @@ -0,0 +1,9 @@ +[general] +enabled = yes +port = 5038 +bindaddr = 127.0.0.1 + +[user] +secret = mysecret +read = all +write = all \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red_audio/configs/ast1/pjsip.conf b/tests/srtp/srtp_t140_red_audio/configs/ast1/pjsip.conf new file mode 100755 index 000000000..bf8248830 --- /dev/null +++ b/tests/srtp/srtp_t140_red_audio/configs/ast1/pjsip.conf @@ -0,0 +1,93 @@ + +[global] +type=global +endpoint_identifier_order=ip,username + +[system] +type=system + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 + +;[101] +;type=auth +;auth_type=password +;username=phone101 +;password=phone101pw + + +[101] +type=aor +contact=sip:101@127.0.0.1:5065 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[101] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone101 +;auth=101 +;outbound_auth=101 +aors=101 +media_encryption=sdes +identify_by=ip,username + +[101-identify] +type=identify +endpoint=101 +match=127.0.0.1:5065 + +[102] +type=aor +contact=sip:102@127.0.0.1:5066 +max_contacts=4 +remove_existing=yes +support_path=yes +qualify_frequency=0 + +[102] +type=endpoint +context=rtt-test +transport=transport-udp +allow=!all,t140,red,ulaw,alaw,g722,h264 +max_text_streams=1 +direct_media=no +force_rport=yes +disable_direct_media_on_nat=yes +ice_support=no +allow_transfer=yes +trust_id_inbound=yes +send_diversion=yes +rtp_symmetric=yes +rewrite_contact=yes +tos_text=0 +cos_text=0 +callerid=phone102 +;auth=102 +;outbound_auth=102 +aors=102 +media_encryption=sdes +identify_by=ip,username + +[102-identify] +type=identify +endpoint=102 +match=127.0.0.1:5066 \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red_audio/run-test b/tests/srtp/srtp_t140_red_audio/run-test new file mode 100755 index 000000000..8da368360 --- /dev/null +++ b/tests/srtp/srtp_t140_red_audio/run-test @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +import sys +import os +import logging +import time + +from asterisk.test_case import TestCase +LOGGER = logging.getLogger(__name__) + +import signal +import subprocess +from twisted.internet import reactor + +# pjsua don't support long texts currently +en_txt = "Real-time text (RTT) transmitted" +gr_txt = "πραγματικό κείμενο σε χρόνο" +de_txt = "Echtzeittext übertragen" +cn_txt = "已发送实时文本" +am_txt = "Իրական ժամանակի տեքստ" +hi_txt = "वास्तविक समय का पाठ" + +class SRTPTest(TestCase): + + def __init__(self): + super(SRTPTest, self).__init__() + self.reactor_timeout = 90 + self.create_asterisk(count=1) + self.ast[0].all_out = True + self.pjsua_rx = None + self.pjsua_tx = None + + def run(self): + super(SRTPTest, self).run() + LOGGER.info("Starting Asterisk...") + self.ast[0].cli_exec("rtp set debug on") + self.ast[0].cli_exec("pjsip set logger on") + reactor.callLater(3, self.start_pjsua) + def start_pjsua(self): + print("--- Starting ---") + LOGGER.info("Starting Receiver (102) and Caller (101)...") + + common_params = [ + '--no-tcp', '--text', '--text-red=0','--no-vad', + '--log-level=3', '--app-log-level=3', + '--use-srtp=2', '--srtp-secure=0', + '--dis-codec=speex', '--dis-codec=gsm', '--dis-codec=opus', + '--dis-codec=pcma', '--dis-codec=g722', '--null-audio' + ] + + # Receiver (102) + self.pjsua_rx = subprocess.Popen([ + 'pjsua', + '--id=sip:102@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5066', + '--rtp-port=40000', + '--outbound=sip:127.0.0.1:5060', + '--auto-answer=200' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + # Caller (101) + self.pjsua_tx = subprocess.Popen([ + 'pjsua', + '--id=sip:101@127.0.0.1', + '--ip-addr=127.0.0.1', + '--local-port=5065', + '--rtp-port=30000', + '--outbound=sip:127.0.0.1:5060' + ] + common_params, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + reactor.callLater(3, self.make_call) + + def make_call(self): + LOGGER.info("pjsua_tx: Dialing 102...") + self.pjsua_tx.stdin.write(b"\n") + self.pjsua_tx.stdin.write(b"m\n") + self.pjsua_tx.stdin.write(b"sip:102@127.0.0.1:5060\n") + self.pjsua_tx.stdin.flush() + print("--- 102 calling ---") + reactor.callLater(2, self.send_en) + + def send_en(self): + print("Starting Ping-Pong RTT test...") + LOGGER.info("Starting Ping-Pong RTT test...") + self.pjsua_tx.stdin.write(b"rt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(en_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print("101 wrote 'English'...") + reactor.callLater(2, self.step_gr) + + def step_gr(self): + self.pjsua_rx.stdin.write(b"rt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(gr_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print("102 wrote 'Greek'...") + reactor.callLater(2, self.step_de) + + def step_de(self): + # send Esc-cr-rt, as we probably have some console output + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(de_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'German'...") + reactor.callLater(2, self.step_cn) + + def step_cn(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(cn_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Chinese'...") + reactor.callLater(2, self.step_am) + + def step_am(self): + self.pjsua_tx.stdin.write(b"\x1b\nrt\n") + self.pjsua_tx.stdin.flush() + self.pjsua_tx.stdin.write(am_txt.encode('utf-8') + b"\n") + self.pjsua_tx.stdin.flush() + print(f"101 wrote 'Armenian'...") + reactor.callLater(2, self.step_hi) + + def step_hi(self): + self.pjsua_rx.stdin.write(b"\x1b\nrt\n") + self.pjsua_rx.stdin.flush() + self.pjsua_rx.stdin.write(hi_txt.encode('utf-8') + b"\n") + self.pjsua_rx.stdin.flush() + print(f"102 wrote 'Hindi'...") + reactor.callLater(2, self.finish) + + def finish(self): + LOGGER.info("Verifying message exchange...") + print("Verifying message exchange...") + try: + if self.pjsua_tx and self.pjsua_tx.stdin: + self.pjsua_tx.stdin.write(b"q\n") + self.pjsua_tx.stdin.flush() + if self.pjsua_rx and self.pjsua_rx.stdin: + self.pjsua_rx.stdin.write(b"q\n") + self.pjsua_rx.stdin.flush() + except (BrokenPipeError, OSError): + LOGGER.warning("Could not send 'q' to pjsua; process likely already dead.") + + out_tx, err_tx = b"", b"" + out_rx, err_rx = b"", b"" + + try: + if self.pjsua_tx: + out_tx, err_tx = self.pjsua_tx.communicate(timeout=3) + + if self.pjsua_rx: + out_rx, err_rx = self.pjsua_rx.communicate(timeout=3) + except (subprocess.TimeoutExpired, ValueError, OSError): + self.pjsua_tx.kill() + self.pjsua_rx.kill() + out_tx, err_tx = self.pjsua_tx.communicate() + out_rx, err_rx = self.pjsua_rx.communicate() + + # 1. Handle TX (101) + if out_tx is None: + print("out_tx is none...") + out_tx = "" + elif isinstance(out_tx, bytes): + out_tx = out_tx.decode('utf-8', errors='replace') + print(f"out_tx is==== {out_tx}===") + + # 2. Handle RX (102) + if out_rx is None: + print("out_rx is none...") + out_rx = "" + elif isinstance(out_rx, bytes): + out_rx = out_rx.decode('utf-8', errors='replace') + print(f"out_rx is==== {out_rx}====") + + text_tx = self.print_pjsua_logs("TX (101)", out_tx, err_tx) + text_rx = self.print_pjsua_logs("RX (102)", out_rx, err_rx) + self.stop_reactor() + + rx_saw_english = "Incoming text" in text_rx and en_txt in text_rx + tx_saw_greek = "Incoming text" in text_tx and gr_txt in text_tx + rx_saw_german = "Incoming text" in text_rx and de_txt in text_rx + tx_saw_chinese = "Incoming text" in text_tx and cn_txt in text_tx + rx_saw_armenian = "Incoming text" in text_rx and am_txt in text_rx + tx_saw_hindi = "Incoming text" in text_tx and hi_txt in text_tx + + if rx_saw_english and tx_saw_greek and rx_saw_german and tx_saw_chinese and rx_saw_armenian and tx_saw_hindi: + LOGGER.info("SUCCESS: RTT exchange confirmed!") + print("SUCCESS.") + self.passed = True + else: + if not rx_saw_english: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'English'") + if not tx_saw_greek: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Greek'") + if not rx_saw_german: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'German'") + if not tx_saw_chinese: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Chinese'") + if not rx_saw_armenian: + LOGGER.error("FAILURE: 102 (Receiver) never saw 'Armenian'") + if not tx_saw_hindi: + LOGGER.error("FAILURE: 101 (Caller) never saw 'Hindi'") + if "401" in text_rx or "401" in text_tx: + LOGGER.error("DEBUG: Found '401 Unauthorized' in logs") + self.passed = False + + def print_pjsua_logs(self, label, output, error): + content = "" + + if output is not None: + if isinstance(output, bytes): + + content = output.decode('utf-8', 'ignore') + else: + content = str(output) + + print(f"--- STARTING {label} LOG DUMP ---") + print(content) + print(f"--- END {label} LOG DUMP ---") + + if error: + err_msg = error.decode('utf-8', 'ignore') if isinstance(error, bytes) else str(error) + print(f"{label} reported an error: {err_msg}") + + return content + + def stop_reactor(self): + for p in [self.pjsua_rx, self.pjsua_tx]: + if p: + try: + os.kill(p.pid, signal.SIGKILL) + except OSError: + pass + super(SRTPTest, self).stop_reactor() + + +def main(): + test = SRTPTest() + reactor.run() + return 0 if test.passed else 1 + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/tests/srtp/srtp_t140_red_audio/test-config.yaml b/tests/srtp/srtp_t140_red_audio/test-config.yaml new file mode 100755 index 000000000..a43c0477a --- /dev/null +++ b/tests/srtp/srtp_t140_red_audio/test-config.yaml @@ -0,0 +1,17 @@ +testinfo: + summary: 'Bidirectional SRTP RTT Ping-Pong Test' + description: 'Verifies T.140 RTT media relay between 101 and 102 using SRTP.' +test-modules: + test-object: + config-section: srtp-test + typename: run-test.SRTPTest +srtp-test: + asterisk-instances: 1 + timeout: 70 +properties: + dependencies: + - python : 'twisted' + - app : 'pjsua' + #- asterisk: 'res_pjsip' + tags: + - pjsip \ No newline at end of file diff --git a/tests/srtp/tests.yaml b/tests/srtp/tests.yaml new file mode 100755 index 000000000..599bc6f34 --- /dev/null +++ b/tests/srtp/tests.yaml @@ -0,0 +1,8 @@ +# Enter tests here in the order they should be considered for execution: +tests: + - test: 'srtp_red_t140' + - test: 'srtp_red_t140_audio' + - test: 'srtp_t140' + - test: 'srtp_t140_audio' + - test: 'srtp_t140_red' + - test: 'srtp_t140_red_audio' \ No newline at end of file diff --git a/tests/tests.yaml b/tests/tests.yaml index a5d27f893..f4c813b28 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -37,5 +37,7 @@ tests: - test: 'remote-test' - dir: 'codecs' - dir: 'rtp' + - dir: 'rtt' - dir: 'tenant_id' - dir: 'extra_gates' + - dir: 'srtp' \ No newline at end of file