Initial LisgloSIPS V2 implementation

This commit is contained in:
hectorzhao
2026-06-22 10:56:38 +08:00
commit 5fa1bd35e9
303 changed files with 35644 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="s21.lisglosips.test"
U1="s21-reg-1001"
U2="s21-reg-1002"
IP1="s21-ip-1001"
IP2="s21-ip-1002"
P1=$(openssl rand -base64 18 | tr -d '=+/' | cut -c1-20)
P2=$(openssl rand -base64 18 | tr -d '=+/' | cut -c1-20)
HA1_1=$(printf "%s:%s:%s" "$U1" "$DOMAIN" "$P1" | md5sum | cut -d " " -f1)
HA1_2=$(printf "%s:%s:%s" "$U2" "$DOMAIN" "$P2" | md5sum | cut -d " " -f1)
unset P1 P2
install -d -m 0755 /opt/lisglosips-s21
install -m 0755 /tmp/lisglosips-s21-sip.py /opt/lisglosips-s21/lisglosips-s21-sip.py
for f in /tmp/s21-*; do
install -m 0755 "$f" "/opt/lisglosips-s21/$(basename "$f")"
done
install -d -m 0750 -o root -g hector /etc/lisglosips-s21
cat > /etc/lisglosips-s21/sip-accounts.env <<EOF
S21_SIP_DOMAIN=$DOMAIN
S21_SIP_PRIMARY_USER=$U1
S21_SIP_SECONDARY_USER=$U2
S21_SIP_PRIMARY_HA1=$HA1_1
S21_SIP_SECONDARY_HA1=$HA1_2
S21_IP_PRIMARY_ID=$IP1
S21_IP_SECONDARY_ID=$IP2
EOF
chown root:hector /etc/lisglosips-s21/sip-accounts.env
chmod 0640 /etc/lisglosips-s21/sip-accounts.env
python3 -m py_compile /opt/lisglosips-s21/lisglosips-s21-sip.py
python3 - <<'PY'
from pathlib import Path
p = Path("/etc/opensips/opensips.cfg")
s = p.read_text()
s = s.replace('modparam("auth_db", "calculate_ha1", 1)', 'modparam("auth_db", "calculate_ha1", 0)')
s = s.replace('modparam("mi_fifo", "fifo_mode", 0666)', 'modparam("mi_fifo", "fifo_mode", 0660)')
p.write_text(s)
PY
find /etc/opensips/tls -type f \( -name "*key*.pem" -o -name "cakey.pem" -o -name "*-privkey.pem" \) -exec chmod 0600 {} + 2>/dev/null || true
mysql --protocol=socket -uroot <<SQL
USE opensips;
INSERT INTO domain (domain, attrs, accept_subdomain, last_modified)
VALUES ('$DOMAIN', 'S21 SIP registration test domain', 0, NOW())
ON DUPLICATE KEY UPDATE attrs=VALUES(attrs), accept_subdomain=VALUES(accept_subdomain), last_modified=NOW();
DELETE FROM subscriber WHERE username IN ('$U1','$U2') AND domain='$DOMAIN';
INSERT INTO subscriber (username, domain, password, ha1, ha1_sha256, ha1_sha512t256)
VALUES
('$U1', '$DOMAIN', '', '$HA1_1', '', ''),
('$U2', '$DOMAIN', '', '$HA1_2', '', '');
DELETE FROM address WHERE context_info IN ('$IP1','$IP2');
INSERT INTO address (grp, ip, mask, port, proto, pattern, context_info)
VALUES
(21, '100.93.185.30', 32, 0, 'any', NULL, '$IP1'),
(22, '100.93.185.30', 32, 0, 'any', NULL, '$IP2');
SQL
opensips -C -f /etc/opensips/opensips.cfg >/run/lisglosips_s21_opensips_check.log 2>&1
systemctl restart opensips
sleep 1
systemctl is-active --quiet opensips
mysql --protocol=socket -uroot -N <<SQL
USE opensips;
SELECT 'domain', domain FROM domain WHERE domain='$DOMAIN';
SELECT 'subscriber', username, domain, LENGTH(password), LENGTH(ha1) FROM subscriber WHERE domain='$DOMAIN' ORDER BY username;
SELECT 'address', grp, ip, mask, proto, context_info FROM address WHERE context_info IN ('$IP1','$IP2') ORDER BY context_info;
SQL
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""LisgloSIPS S21 SIP scenario runner.
Self-contained UDP SIP helper for Server T customer simulation.
It intentionally accepts HA1 values instead of plaintext SIP passwords.
"""
from __future__ import annotations
import argparse
import hashlib
import random
import re
import socket
import string
import sys
import threading
import time
from dataclasses import dataclass
CRLF = "\r\n"
def md5_hex(value: str) -> str:
return hashlib.md5(value.encode("utf-8")).hexdigest()
def rand_token(length: int = 10) -> str:
alphabet = string.ascii_lowercase + string.digits
return "".join(random.choice(alphabet) for _ in range(length))
def call_id(prefix: str = "s21") -> str:
return f"{prefix}-{int(time.time() * 1000)}-{rand_token(8)}@lisglosips-t"
def parse_authenticate(header: str) -> dict[str, str]:
header = header.strip()
if header.lower().startswith("digest "):
header = header[7:]
values: dict[str, str] = {}
for part in re.split(r",\s*", header):
if "=" not in part:
continue
key, raw = part.split("=", 1)
values[key.strip().lower()] = raw.strip().strip('"')
return values
def header_value(message: str, name: str) -> str | None:
pattern = re.compile(rf"^{re.escape(name)}\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE)
match = pattern.search(message)
return match.group(1).strip() if match else None
def status_line(message: str) -> str:
return message.splitlines()[0] if message else "NO RESPONSE"
@dataclass
class SipTarget:
host: str
port: int
def udp_roundtrip(payload: str, target: SipTarget, timeout: float = 3.0, bind_host: str = "0.0.0.0") -> str:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((bind_host, 0))
sock.settimeout(timeout)
sock.sendto(payload.encode("utf-8"), (target.host, target.port))
try:
data, _ = sock.recvfrom(65535)
except socket.timeout:
return ""
return data.decode("utf-8", errors="replace")
def build_register(
username: str,
domain: str,
contact_host: str,
local_port: int,
cseq: int,
cid: str,
branch: str,
authorization: str | None = None,
) -> str:
lines = [
f"REGISTER sip:{domain} SIP/2.0",
f"Via: SIP/2.0/UDP {contact_host}:{local_port};branch=z9hG4bK-{branch};rport",
"Max-Forwards: 70",
f"From: <sip:{username}@{domain}>;tag={rand_token(8)}",
f"To: <sip:{username}@{domain}>",
f"Call-ID: {cid}",
f"CSeq: {cseq} REGISTER",
f"Contact: <sip:{username}@{contact_host}:{local_port}>;expires=120",
"Expires: 120",
"User-Agent: lisglosips-s21",
]
if authorization:
lines.append(f"Authorization: {authorization}")
lines.extend(["Content-Length: 0", "", ""])
return CRLF.join(lines)
def digest_authorization(username: str, realm: str, uri: str, method: str, ha1: str, challenge: dict[str, str]) -> str:
nonce = challenge["nonce"]
qop = challenge.get("qop", "").split(",")[0].strip()
algorithm = challenge.get("algorithm", "MD5")
ha2 = md5_hex(f"{method}:{uri}")
if qop:
nc = "00000001"
cnonce = rand_token(16)
response = md5_hex(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}")
return (
f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{uri}", '
f'response="{response}", algorithm={algorithm}, qop={qop}, nc={nc}, cnonce="{cnonce}"'
)
response = md5_hex(f"{ha1}:{nonce}:{ha2}")
return (
f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{uri}", '
f'response="{response}", algorithm={algorithm}'
)
def cmd_register(args: argparse.Namespace) -> int:
cid = call_id("s21-register")
target = SipTarget(args.host, args.port)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((args.bind_host, 0))
local_host, local_port = sock.getsockname()
if args.contact_host:
local_host = args.contact_host
sock.settimeout(args.timeout)
first = build_register(args.username, args.domain, local_host, local_port, 1, cid, rand_token())
sock.sendto(first.encode("utf-8"), (target.host, target.port))
try:
data, _ = sock.recvfrom(65535)
except socket.timeout:
print(f"call_id={cid} status=NO_RESPONSE")
return 2
response = data.decode("utf-8", errors="replace")
print(f"call_id={cid} first_status={status_line(response)}")
www = header_value(response, "WWW-Authenticate") or header_value(response, "Proxy-Authenticate")
if not www:
return 0 if response.startswith("SIP/2.0 200") else 1
challenge = parse_authenticate(www)
realm = challenge.get("realm", args.domain)
uri = f"sip:{args.domain}"
auth = digest_authorization(args.username, realm, uri, "REGISTER", args.ha1, challenge)
second = build_register(args.username, args.domain, local_host, local_port, 2, cid, rand_token(), auth)
sock.sendto(second.encode("utf-8"), (target.host, target.port))
try:
data, _ = sock.recvfrom(65535)
except socket.timeout:
print(f"call_id={cid} final_status=NO_RESPONSE")
return 2
final = data.decode("utf-8", errors="replace")
print(f"call_id={cid} final_status={status_line(final)}")
return 0 if final.startswith("SIP/2.0 200") else 1
def build_invite(args: argparse.Namespace, cid: str, cseq: int = 1) -> str:
branch = rand_token(10)
from_tag = rand_token(8)
from_uri = f"sip:{args.caller}@{args.from_domain}"
to_uri = f"sip:{args.callee}@{args.to_domain}"
request_uri = f"sip:{args.callee}@{args.to_domain}"
sdp = (
"v=0\r\n"
f"o=lisglosips-s21 0 0 IN IP4 {args.media_host}\r\n"
"s=lisglosips-s21\r\n"
f"c=IN IP4 {args.media_host}\r\n"
"t=0 0\r\n"
f"m=audio {args.media_port} RTP/AVP 0 8 101\r\n"
"a=rtpmap:0 PCMU/8000\r\n"
"a=rtpmap:8 PCMA/8000\r\n"
"a=rtpmap:101 telephone-event/8000\r\n"
)
lines = [
f"INVITE {request_uri} SIP/2.0",
f"Via: SIP/2.0/UDP {args.contact_host}:{args.local_port};branch=z9hG4bK-{branch};rport",
"Max-Forwards: 70",
f"From: <{from_uri}>;tag={from_tag}",
f"To: <{to_uri}>",
f"Call-ID: {cid}",
f"CSeq: {cseq} INVITE",
f"Contact: <sip:{args.caller}@{args.contact_host}:{args.local_port}>",
"User-Agent: lisglosips-s21",
"Content-Type: application/sdp",
f"Content-Length: {len(sdp.encode('utf-8'))}",
"",
sdp,
]
return CRLF.join(lines)
def cmd_invite(args: argparse.Namespace) -> int:
cid = call_id("s21-invite")
target = SipTarget(args.host, args.port)
payload = build_invite(args, cid)
response = udp_roundtrip(payload, target, args.timeout, args.bind_host)
print(f"call_id={cid} status={status_line(response)}")
return 0 if response else 2
def uas_once(host: str, port: int, mode: str, ready: threading.Event) -> None:
code_map = {
"success": ("200", "OK"),
"busy": ("486", "Busy Here"),
"reject": ("603", "Decline"),
}
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((host, port))
sock.settimeout(5)
ready.set()
try:
data, addr = sock.recvfrom(65535)
except socket.timeout:
return
if mode == "timeout":
time.sleep(3)
return
request = data.decode("utf-8", errors="replace")
via = header_value(request, "Via") or ""
from_h = header_value(request, "From") or ""
to_h = header_value(request, "To") or ""
cid = header_value(request, "Call-ID") or call_id("s21-uas")
cseq = header_value(request, "CSeq") or "1 INVITE"
code, reason = code_map[mode]
if "tag=" not in to_h:
to_h = f"{to_h};tag={rand_token(8)}"
response = CRLF.join(
[
f"SIP/2.0 {code} {reason}",
f"Via: {via}",
f"From: {from_h}",
f"To: {to_h}",
f"Call-ID: {cid}",
f"CSeq: {cseq}",
"Server: lisglosips-s21-uas",
"Content-Length: 0",
"",
"",
]
)
sock.sendto(response.encode("utf-8"), addr)
def cmd_local_scenario(args: argparse.Namespace) -> int:
ready = threading.Event()
thread = threading.Thread(target=uas_once, args=(args.listen_host, args.listen_port, args.mode, ready), daemon=True)
thread.start()
ready.wait(2)
args.host = args.listen_host
args.port = args.listen_port
args.to_domain = f"{args.listen_host}:{args.listen_port}"
rc = cmd_invite(args)
thread.join(4)
if args.mode == "timeout":
return 0 if rc == 2 else 1
return rc
def cmd_over_cps(args: argparse.Namespace) -> int:
target = SipTarget(args.host, args.port)
statuses: list[str] = []
for index in range(args.count):
cid = call_id(f"s21-cps-{index + 1}")
args.callee = f"{args.callee_prefix}{index + 1:03d}"
payload = build_invite(args, cid)
response = udp_roundtrip(payload, target, args.timeout, args.bind_host)
line = status_line(response)
statuses.append(line)
print(f"call_id={cid} status={line}")
if args.interval > 0:
time.sleep(args.interval)
print(f"sent={args.count} unique_statuses={sorted(set(statuses))}")
return 0
def add_common_invite_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--host", default="100.90.90.90")
parser.add_argument("--port", type=int, default=15060)
parser.add_argument("--bind-host", default="0.0.0.0")
parser.add_argument("--contact-host", default="100.93.185.30")
parser.add_argument("--local-port", type=int, default=50620)
parser.add_argument("--caller", default="s21-ip-1001")
parser.add_argument("--callee", default="13800138000")
parser.add_argument("--from-domain", default="s21-ip.lisglosips.test")
parser.add_argument("--to-domain", default="100.90.90.90:15060")
parser.add_argument("--media-host", default="100.93.185.30")
parser.add_argument("--media-port", type=int, default=31000)
parser.add_argument("--timeout", type=float, default=3.0)
def main() -> int:
parser = argparse.ArgumentParser(description="LisgloSIPS S21 SIP scenario runner")
sub = parser.add_subparsers(dest="command", required=True)
reg = sub.add_parser("register")
reg.add_argument("--host", default="100.93.185.30")
reg.add_argument("--port", type=int, default=5060)
reg.add_argument("--bind-host", default="0.0.0.0")
reg.add_argument("--contact-host", default="100.93.185.30")
reg.add_argument("--username", required=True)
reg.add_argument("--domain", required=True)
reg.add_argument("--ha1", required=True)
reg.add_argument("--timeout", type=float, default=3.0)
reg.set_defaults(func=cmd_register)
inv = sub.add_parser("invite")
add_common_invite_args(inv)
inv.set_defaults(func=cmd_invite)
local = sub.add_parser("local-scenario")
add_common_invite_args(local)
local.add_argument("--mode", choices=["success", "busy", "reject", "timeout"], required=True)
local.add_argument("--listen-host", default="127.0.0.1")
local.add_argument("--listen-port", type=int, default=15070)
local.set_defaults(func=cmd_local_scenario)
cps = sub.add_parser("over-cps")
add_common_invite_args(cps)
cps.add_argument("--count", type=int, default=20)
cps.add_argument("--interval", type=float, default=0.02)
cps.add_argument("--callee-prefix", default="1390000")
cps.set_defaults(func=cmd_over_cps)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
exec /opt/lisglosips-s21/lisglosips-s21-sip.py invite \
--caller s21-ip-1001 \
--from-domain s21-ip.lisglosips.test \
--callee "${1:-13800138000}"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source /etc/lisglosips-s21/sip-accounts.env
exec /opt/lisglosips-s21/lisglosips-s21-sip.py invite \
--caller "$S21_SIP_PRIMARY_USER" \
--from-domain "$S21_SIP_DOMAIN" \
--callee "${1:-13800138000}"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode busy --callee "${1:-13800138000}"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode reject --callee "${1:-13800138000}"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode success --callee "${1:-13800138000}"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec /opt/lisglosips-s21/lisglosips-s21-sip.py local-scenario --mode timeout --callee "${1:-13800138000}"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec /opt/lisglosips-s21/lisglosips-s21-sip.py over-cps --count "${1:-20}" --interval "${2:-0.02}"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source /etc/lisglosips-s21/sip-accounts.env
exec /opt/lisglosips-s21/lisglosips-s21-sip.py register \
--username "$S21_SIP_PRIMARY_USER" \
--domain "$S21_SIP_DOMAIN" \
--ha1 "$S21_SIP_PRIMARY_HA1"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source /etc/lisglosips-s21/sip-accounts.env
exec /opt/lisglosips-s21/lisglosips-s21-sip.py register \
--username "$S21_SIP_SECONDARY_USER" \
--domain "$S21_SIP_DOMAIN" \
--ha1 "$S21_SIP_SECONDARY_HA1"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source /etc/lisglosips-s21/sip-accounts.env
exec /opt/lisglosips-s21/lisglosips-s21-sip.py register \
--username "$S21_SIP_PRIMARY_USER" \
--domain "$S21_SIP_DOMAIN" \
--ha1 "00000000000000000000000000000000"
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import random
import re
import socket
import struct
import string
import sys
import threading
import time
CRLF = "\r\n"
def token(n: int = 8) -> str:
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(n))
def header(msg: str, name: str) -> str:
m = re.search(rf"^{re.escape(name)}\s*:\s*(.+)$", msg, re.I | re.M)
return m.group(1).strip() if m else ""
def call_id() -> str:
return f"s28-{int(time.time() * 1000)}-{token()}@lisglosips-t"
def status(msg: str) -> str:
return msg.splitlines()[0] if msg else "NO RESPONSE"
def sdp(host: str, port: int) -> str:
return (
"v=0\r\n"
f"o=lisglosips-s28 0 0 IN IP4 {host}\r\n"
"s=lisglosips-s28\r\n"
f"c=IN IP4 {host}\r\n"
"t=0 0\r\n"
f"m=audio {port} RTP/AVP 0 8 101\r\n"
"a=rtpmap:0 PCMU/8000\r\n"
"a=rtpmap:8 PCMA/8000\r\n"
"a=rtpmap:101 telephone-event/8000\r\n"
)
def parse_sdp_media(msg: str) -> tuple[str, int] | None:
body = msg.split("\r\n\r\n", 1)[1] if "\r\n\r\n" in msg else msg.split("\n\n", 1)[1] if "\n\n" in msg else ""
media_host = ""
media_port = 0
for raw in body.splitlines():
line = raw.strip()
if line.startswith("c=IN IP4 "):
media_host = line.split()[-1]
elif line.startswith("m=audio "):
parts = line.split()
if len(parts) >= 2 and parts[1].isdigit():
media_port = int(parts[1])
if media_host and media_port:
return media_host, media_port
return None
def send_pcmu_rtp(target: tuple[str, int], duration: float, bind_host: str = "0.0.0.0") -> None:
payload = b"\xff" * 160
seq = random.randint(0, 65535)
timestamp = random.randint(0, 2**32 - 1)
ssrc = random.randint(1, 2**32 - 1)
deadline = time.time() + duration
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((bind_host, 0))
while time.time() < deadline:
header = struct.pack("!BBHII", 0x80, 0x00, seq, timestamp, ssrc)
sock.sendto(header + payload, target)
seq = (seq + 1) % 65536
timestamp = (timestamp + 160) % 2**32
time.sleep(0.02)
def response(code: int, reason: str, req: str, body: str = "") -> str:
to_h = header(req, "To")
if "tag=" not in to_h:
to_h = f"{to_h};tag=s28uas{token(6)}"
lines = [
f"SIP/2.0 {code} {reason}",
f"Via: {header(req, 'Via')}",
f"From: {header(req, 'From')}",
f"To: {to_h}",
f"Call-ID: {header(req, 'Call-ID')}",
f"CSeq: {header(req, 'CSeq')}",
"Server: lisglosips-s28-uas",
]
if body:
lines.extend(["Content-Type: application/sdp", f"Content-Length: {len(body.encode())}", "", body])
else:
lines.extend(["Content-Length: 0", "", ""])
return CRLF.join(lines)
def uas(args: argparse.Namespace) -> int:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((args.host, args.port))
while True:
data, addr = sock.recvfrom(65535)
req = data.decode(errors="replace")
method = req.split(" ", 1)[0]
if method == "INVITE":
sock.sendto(response(100, "Trying", req).encode(), addr)
body = sdp(args.media_host, args.media_port)
sock.sendto(response(200, "OK", req, body).encode(), addr)
media = parse_sdp_media(req)
if media:
threading.Thread(target=send_pcmu_rtp, args=(media, args.rtp_duration, args.host), daemon=True).start()
elif method == "BYE":
sock.sendto(response(200, "OK", req).encode(), addr)
def invite(args: argparse.Namespace) -> int:
cid = call_id()
from_tag = token()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((args.bind_host, args.local_port))
args.local_port = sock.getsockname()[1]
sock.settimeout(args.timeout)
body = sdp(args.media_host, args.media_port)
req = CRLF.join(
[
f"INVITE sip:{args.callee}@{args.to_domain} SIP/2.0",
f"Via: SIP/2.0/UDP {args.contact_host}:{args.local_port};branch=z9hG4bK-{token(10)};rport",
"Max-Forwards: 70",
f"From: <sip:{args.caller}@s28.customer.local>;tag={from_tag}",
f"To: <sip:{args.callee}@{args.to_domain}>",
f"Call-ID: {cid}",
"CSeq: 1 INVITE",
f"Contact: <sip:{args.caller}@{args.contact_host}:{args.local_port}>",
"User-Agent: lisglosips-s28",
"Content-Type: application/sdp",
f"Content-Length: {len(body.encode())}",
"",
body,
]
)
sock.sendto(req.encode(), (args.host, args.port))
final = ""
route = ""
to_h = ""
while True:
try:
msg = sock.recvfrom(65535)[0].decode(errors="replace")
except socket.timeout:
break
print(f"call_id={cid} status={status(msg)}")
if msg.startswith("SIP/2.0 2"):
final = msg
route = header(msg, "Record-Route") or f"<sip:{args.host}:{args.port};lr>"
to_h = header(msg, "To")
break
if msg.startswith("SIP/2.0 3") or msg.startswith("SIP/2.0 4") or msg.startswith("SIP/2.0 5") or msg.startswith("SIP/2.0 6"):
final = msg
break
if not final.startswith("SIP/2.0 2"):
print(f"call_id={cid} final={status(final)}")
return 1
ack = build_in_dialog("ACK", args, cid, from_tag, to_h, 1, route)
sock.sendto(ack.encode(), (args.host, args.port))
media = parse_sdp_media(final)
if media:
send_pcmu_rtp(media, args.hold, args.contact_host)
time.sleep(args.hold)
bye = build_in_dialog("BYE", args, cid, from_tag, to_h, 2, route)
sock.sendto(bye.encode(), (args.host, args.port))
try:
bye_resp = sock.recvfrom(65535)[0].decode(errors="replace")
except socket.timeout:
bye_resp = ""
print(f"call_id={cid} bye_status={status(bye_resp)}")
return 0 if bye_resp.startswith("SIP/2.0 200") else 1
def build_in_dialog(method: str, args: argparse.Namespace, cid: str, from_tag: str, to_h: str, cseq: int, route: str) -> str:
lines = [
f"{method} sip:{args.callee}@{args.to_domain} SIP/2.0",
f"Via: SIP/2.0/UDP {args.contact_host}:{args.local_port};branch=z9hG4bK-{token(10)};rport",
"Max-Forwards: 70",
f"From: <sip:{args.caller}@s28.customer.local>;tag={from_tag}",
f"To: {to_h}",
f"Call-ID: {cid}",
f"CSeq: {cseq} {method}",
f"Contact: <sip:{args.caller}@{args.contact_host}:{args.local_port}>",
]
if route:
lines.append(f"Route: {route}")
lines.extend(["Content-Length: 0", "", ""])
return CRLF.join(lines)
def main() -> int:
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="cmd", required=True)
u = sub.add_parser("uas")
u.add_argument("--host", default="100.93.185.30")
u.add_argument("--port", type=int, default=50620)
u.add_argument("--media-host", default="100.93.185.30")
u.add_argument("--media-port", type=int, default=32000)
u.add_argument("--rtp-duration", type=float, default=4.0)
i = sub.add_parser("invite")
i.add_argument("--host", default="100.90.90.90")
i.add_argument("--port", type=int, default=15060)
i.add_argument("--bind-host", default="0.0.0.0")
i.add_argument("--contact-host", default="100.93.185.30")
i.add_argument("--local-port", type=int, default=0)
i.add_argument("--caller", default="s28-ip-1001")
i.add_argument("--callee", default="13800138000")
i.add_argument("--to-domain", default="100.90.90.90:15060")
i.add_argument("--media-host", default="100.93.185.30")
i.add_argument("--media-port", type=int, default=31002)
i.add_argument("--timeout", type=float, default=5.0)
i.add_argument("--hold", type=float, default=3.0)
args = p.parse_args()
return uas(args) if args.cmd == "uas" else invite(args)
if __name__ == "__main__":
sys.exit(main())