当前位置: 首页 > news >正文

Python实现轻量级SIP服务器:Digest鉴权与sip.js对接实战

1. 这不是写个“Hello World”就能跑通的SIP服务

很多人第一次听说“用Python搭SIP服务器”,第一反应是:不就是起个UDP监听,收发几条REGISTER、200 OK吗?我当年也是这么想的——直到在凌晨三点对着Wireshark抓出的第17版401 Unauthorized响应发呆,发现sip.js发来的Authorization头里,nonce值明明匹配,realm却总被拒绝;再查Python端生成的HA1,发现MD5计算时把用户名拼错了顺序;最后翻RFC 3261附录A才发现,Digest鉴权里那个“username:realm:password”的拼接,必须严格按冒号分隔、不带空格、大小写敏感,而我写的测试账号是admin,但sip.js默认传的是Admin……就这一个字母,卡了整整两天。

这不是理论题,是实打实的协议对齐工程。SIP注册流程表面只有四步(REGISTER → 401 → REGISTER with Auth → 200 OK),但背后牵扯到状态机管理、时间戳同步、nonce生命周期控制、qop参数协商、HA1/HA2/Response三重哈希链路、以及sip.js与Python服务端在CSeq递增、Via分支ID、Contact URI格式上的隐式约定。稍有偏差,客户端就静默失败,连错误日志都不报——因为sip.js把401当正常挑战,只在最终超时后才抛“Registration failed”。

这篇内容,就是我把从零开始搭建一个可真实对接商用软电话(如MicroSIP、Zoiper)、支持完整Digest鉴权、能通过sip.js Web端稳定注册、且所有环节可调试可验证的SIP服务器全过程,掰开揉碎讲清楚。它不依赖Asterisk或FreeSWITCH,不包装黑盒SDK,全部用标准Python库(asyncio + socket)+ sip.js前端实现,核心逻辑不到500行,但每行都经Wireshark和真实终端反复验证。适合正在做WebRTC通话集成、IoT语音网关、或需要轻量级SIP信令服务的开发者。如果你只需要“能跑”,网上有现成Docker镜像;但如果你需要“知道为什么能跑、哪里会崩、怎么改、怎么查”,那接下来的内容,就是你该逐字读完的实操手册。

2. 协议层拆解:为什么Digest鉴权不能只算MD5?

要让Python服务端正确响应sip.js的REGISTER请求,第一步不是写代码,而是把RFC 3261第22章Digest认证的数据流闭环画清楚。很多人栽在“算出HA1就以为万事大吉”,结果发现Response始终校验失败——问题往往不出在哈希本身,而出在输入参数的构造逻辑上。

2.1 Digest认证的三段式哈希链:HA1 → HA2 → Response

Digest认证不是单次MD5,而是一个三级哈希流水线:

  • HA1 = MD5(username:realm:password)
    这是最常出错的一环。username必须是客户端在REGISTER中To头域里的值(不是From,也不是Contact);realm必须与服务端在401响应中WWW-Authenticate头里声明的完全一致(包括大小写、前后空格);password是明文密码,绝不能提前哈希。我曾把用户密码存为MD5再参与HA1计算,导致整个链路失效——Digest要求原始密码参与,这是协议强制约定。

  • HA2 = MD5(method:digestURI)
    method是大写字符串"REGISTER"digestURI不是完整的Request-URI,而是<sip:domain>部分(例如"sip:192.168.1.100"),且必须与REGISTER请求行中的URI字符级完全相同。注意:如果客户端发的是sip:user@192.168.1.100,digestURI就得是这个全量字符串;如果发的是sip:192.168.1.100,就不能多加user@。sip.js默认使用<sip:your-domain>,所以服务端必须原样复用。

  • Response = MD5(HA1:nonce:nonceCount:cnonce:qop:HA2)
    这是最终校验值,也是最复杂的拼接。其中:

    • nonce:服务端生成的随机字符串,需带时间戳防重放(如base64(time.time())),并在401响应中透出;
    • nonceCount:客户端自增的十六进制计数器(如"00000001"),每次请求+1,服务端需校验是否递增(防重放);
    • cnonce:客户端生成的随机字符串,用于绑定请求;
    • qop:质量保护选项,sip.js固定发"auth",服务端必须严格匹配,不能忽略;
    • 所有字段间用英文冒号:连接,无空格、无换行、无额外引号

提示:Wireshark里看Authorization头时,复制整个value粘贴到文本编辑器,用正则response="([^"]+)"提取Response值,再用Python脚本逐字段还原计算,是定位HA1/HA2/Response哪一环出错的最快方式。

2.2 sip.js的隐式行为清单:不写文档但必须遵守

sip.js作为主流Web SIP栈,其Digest实现虽符合RFC,但有若干未明说但强依赖的默认行为,Python服务端若不主动适配,必然失败:

  • CSeq头域必须严格递增:sip.js对每个dialog维护独立CSeq计数器。首次REGISTER发CSeq: 1 REGISTER,收到200 OK后,下一次REGISTER(如刷新注册)必须是CSeq: 2 REGISTER。服务端若忽略CSeq或简单置1,sip.js会静默丢弃响应。
  • Via头域branch参数必须含z9hG4bK前缀:这是RFC 3261规定的magic cookie,sip.js硬编码检查。若服务端返回的Via头里branch是branch=abc123,sip.js直接拒收;必须是branch=z9hG4bK-abc123
  • Contact头URI必须带expires参数:sip.js在REGISTER中携带Contact: <sip:alice@192.168.1.100>;expires=3600,服务端若在200 OK中返回Contact: <sip:alice@192.168.1.100>(无expires),某些版本会触发重注册失败。
  • Date头域非必需但建议添加:虽然RFC未强制,但sip.js在调试模式下会打印Date缺失警告,添加Date: Wed, 01 Jan 2020 00:00:00 GMT可避免干扰日志。

这些细节在sip.js源码的src/transactions/register-tr.tssrc/core/auth.ts中有体现,但官方文档只字不提。我的做法是:用MicroSIP发标准REGISTER,抓包对比sip.js与MicroSIP的请求差异,逐项对齐。

2.3 Python端必须解决的三个状态难题

协议对齐只是基础,真正让服务端“活”起来的是状态管理。SIP注册不是无状态HTTP,而是有明确生命周期的会话:

  • Nonce生命周期管理:401响应中的nonce不能永久有效。RFC建议有效期300秒,我设为180秒。Python需用dict缓存{nonce: (timestamp, realm)},每次收到Authorization头先查nonce是否存在且未过期。过期nonce必须立即清除,否则攻击者可重放旧nonce。
  • 注册绑定(Binding)存储:每个成功注册的用户,需持久化{aor: contact_uri, expires: timestamp, call_id: str}aor即Address of Record(如sip:alice@mydomain.com),contact_uri是客户端实际地址(如sip:alice@192.168.1.100:5060)。这里不用数据库,用内存dict+后台线程定期清理过期项即可,轻量项目足够。
  • CSeq与Call-ID的关联校验:同一Call-ID的多次REGISTER,CSeq必须严格递增。服务端需为每个Call-ID维护{call_id: last_cseq}映射,收到新请求时比对int(new_cseq) == int(last_cseq) + 1,否则拒绝。

这三个状态点,决定了你的SIP服务器是“玩具”还是“可用”。我见过太多Python实现只处理单次401→200,却没管nonce过期或注册续期,结果上线两小时就被刷爆内存。

3. Python服务端实现:从socket到可调试的异步SIP栈

现在进入实操。我们不用任何SIP框架(如pjsua、sippy),纯用Python标准库asynciosocket,目标是代码透明、逻辑可控、每一行都可断点调试。核心类SIPServer仅3个方法:handle_register()处理注册、generate_nonce()生成挑战、verify_digest()校验凭证。

3.1 基础通信层:UDP Server与消息解析

SIP默认走UDP,所以我们用asyncio.DatagramProtocol构建服务端。关键不是收发数据,而是精准解析SIP消息结构

import asyncio import re from datetime import datetime, timedelta import hashlib import base64 import time import random class SIPServer(asyncio.DatagramProtocol): def __init__(self): self.bindings = {} # {aor: {contact, expires, call_id}} self.nonces = {} # {nonce: (created_time, realm)} self.callid_cseq = {} # {call_id: last_cseq} self.realm = "my-sip-server.local" self.passwords = {"alice": "secret123", "bob": "pass456"} # 真实项目请对接DB def connection_made(self, transport): self.transport = transport def datagram_received(self, data, addr): try: msg = data.decode('utf-8') if msg.startswith("REGISTER"): self.handle_register(msg, addr) except Exception as e: print(f"Parse error from {addr}: {e}")

注意:data.decode('utf-8')是安全的,因为SIP消息体必须是UTF-8(RFC 3261 §25.1)。但生产环境需加try-except,避免非法编码崩溃服务。

解析REGISTER请求的核心是提取关键头域。我们不用正则全文匹配,而是逐行扫描:

def parse_headers(self, msg): headers = {} lines = msg.split("\r\n") for line in lines[1:]: # 跳过首行(METHOD SP Request-URI SP SIP-Version) if not line.strip(): break if ":" in line: key, value = line.split(":", 1) headers[key.strip()] = value.strip() return headers def handle_register(self, msg, addr): headers = self.parse_headers(msg) # 提取必要字段 to_match = re.search(r'To:\s*<([^>]+)>', msg) from_match = re.search(r'From:\s*<([^>]+)>', msg) contact_match = re.search(r'Contact:\s*<([^>]+)>', msg) cseq_match = re.search(r'CSeq:\s*(\d+)\s+(\w+)', msg) call_id = headers.get("Call-ID", "") via = headers.get("Via", "") if not (to_match and from_match and contact_match and cseq_match): self.send_response(addr, "400 Bad Request", msg) return aor = to_match.group(1) # sip:alice@mydomain.com contact_uri = contact_match.group(1) # sip:alice@192.168.1.100:5060 cseq_num = cseq_match.group(1) method = cseq_match.group(2) # 校验CSeq递增 if call_id in self.callid_cseq: last_cseq = self.callid_cseq[call_id] if int(cseq_num) != int(last_cseq) + 1: self.send_response(addr, "400 Bad Request", msg) return self.callid_cseq[call_id] = cseq_num # 检查Authorization头 auth_header = headers.get("Authorization", "") if not auth_header: # 无认证,返回401挑战 nonce = self.generate_nonce() response = ( "SIP/2.0 401 Unauthorized\r\n" f'WWW-Authenticate: Digest realm="{self.realm}", ' f'nonce="{nonce}", algorithm=MD5, qop="auth"\r\n' "Content-Length: 0\r\n\r\n" ) self.transport.sendto(response.encode(), addr) return # 有认证,校验Digest if self.verify_digest(auth_header, aor, method, headers.get("uri", "")): # 校验通过,保存binding expires = 3600 if "expires" in contact_uri: # 解析Contact中的expires参数 exp_match = re.search(r';expires=(\d+)', contact_uri) if exp_match: expires = int(exp_match.group(1)) self.bindings[aor] = { "contact": contact_uri, "expires": time.time() + expires, "call_id": call_id } # 返回200 OK response = ( "SIP/2.0 200 OK\r\n" f"To: {headers['To']}\r\n" f"From: {headers['From']}\r\n" f"Call-ID: {call_id}\r\n" f"CSeq: {cseq_num} {method}\r\n" f"Via: {via}\r\n" f"Contact: <{contact_uri}>\r\n" f"Expires: {expires}\r\n" "Content-Length: 0\r\n\r\n" ) self.transport.sendto(response.encode(), addr) else: self.send_response(addr, "403 Forbidden", msg) def send_response(self, addr, status, original_msg): # 构造最小化响应,复用original_msg中的Via/From/To/Call-ID headers = self.parse_headers(original_msg) via = headers.get("Via", "SIP/2.0/UDP 127.0.0.1:5060;branch=z9hG4bK-12345") to = headers.get("To", "") from_hdr = headers.get("From", "") call_id = headers.get("Call-ID", "") response = ( f"SIP/2.0 {status}\r\n" f"Via: {via}\r\n" f"To: {to}\r\n" f"From: {from_hdr}\r\n" f"Call-ID: {call_id}\r\n" "Content-Length: 0\r\n\r\n" ) self.transport.sendto(response.encode(), addr)

这段代码已能处理基础流程,但verify_digest才是核心。我们继续深挖。

3.2 Digest校验函数:逐字段还原RFC计算逻辑

verify_digest必须严格遵循RFC 3261附录A的公式。重点在于字段提取的鲁棒性——Authorization头格式多变,需用正则安全捕获:

def verify_digest(self, auth_header, aor, method, request_uri): # 解析Authorization头,支持双引号和无引号值 params = {} # 匹配 key="value" 或 key=value pattern = r'(\w+)=(?:"([^"]*)"|(\S+))' for match in re.finditer(pattern, auth_header): key = match.group(1) value = match.group(2) or match.group(3) params[key] = value required = ["username", "realm", "nonce", "uri", "response", "cnonce", "nc", "qop"] if not all(k in params for k in required): return False # 校验realm是否匹配 if params["realm"] != self.realm: return False # 校验nonce是否有效 if params["nonce"] not in self.nonces: return False created, _ = self.nonces[params["nonce"]] if time.time() - created > 180: # 3分钟过期 del self.nonces[params["nonce"]] return False # 校验qop是否为auth if params["qop"] != "auth": return False # 计算HA1: MD5(username:realm:password) username = params["username"] password = self.passwords.get(username, "") if not password: return False ha1 = hashlib.md5(f"{username}:{self.realm}:{password}".encode()).hexdigest() # 计算HA2: MD5(method:digestURI) # digestURI是Authorization头里的uri字段,不是Request-URI ha2 = hashlib.md5(f"{method}:{params['uri']}".encode()).hexdigest() # 计算Response: MD5(HA1:nonce:nc:cnonce:qop:HA2) response_input = f"{ha1}:{params['nonce']}:{params['nc']}:{params['cnonce']}:{params['qop']}:{ha2}" expected_response = hashlib.md5(response_input.encode()).hexdigest() return expected_response == params["response"]

关键细节:params['uri']来自Authorization头,不是原始请求的Request-URI。sip.js在Authorization中发送uri="sip:my-sip-server.local",我们必须用这个值,而非REGISTER行里的sip:192.168.1.100。这是RFC规定,也是多数Python实现踩坑点。

3.3 Nonce生成与状态清理:让服务端真正健壮

generate_nonce看似简单,实则暗藏玄机:

def generate_nonce(self): # 生成含时间戳的nonce,便于后续过期检查 timestamp = int(time.time()) rand_str = base64.b64encode( f"{timestamp}-{random.randint(1000,9999)}".encode() ).decode().replace("+", "").replace("/", "").replace("=", "")[:16] nonce = f"{timestamp}-{rand_str}" self.nonces[nonce] = (time.time(), self.realm) return nonce

同时,必须启动后台任务清理过期状态:

async def cleanup_loop(self): while True: now = time.time() # 清理过期nonce expired_nonces = [ n for n, (t, _) in self.nonces.items() if now - t > 180 ] for n in expired_nonces: del self.nonces[n] # 清理过期binding expired_aors = [ a for a, b in self.bindings.items() if b["expires"] < now ] for a in expired_aors: del self.bindings[a] await asyncio.sleep(30) # 每30秒检查一次 # 启动服务 async def main(): loop = asyncio.get_running_loop() server = SIPServer() transport, protocol = await loop.create_datagram_endpoint( lambda: server, local_addr=("0.0.0.0", 5060) ) # 启动清理协程 asyncio.create_task(server.cleanup_loop()) print("SIP Server listening on :5060") try: await asyncio.Event().wait() # 永久运行 except KeyboardInterrupt: pass finally: transport.close() if __name__ == "__main__": asyncio.run(main())

这套实现,内存占用<5MB,CPU占用近乎为0,可稳定支撑数百终端注册。它不追求功能大全,而追求每个字节都可知、可控、可验证

4. sip.js前端配置:从初始化到注册成功的完整链路

服务端搭好,前端必须精准配合。sip.js v0.20+(推荐v0.22)的配置有诸多陷阱,我们逐项击破。

4.1 最小可行注册配置:去掉所有冗余选项

很多教程堆砌大量配置,反而掩盖核心。以下是最简但100%有效的注册代码:

<!DOCTYPE html> <html> <head> <title>SIP Registration Test</title> <script src="https://unpkg.com/sip.js@0.22.0/dist/umd/sip.min.js"></script> </head> <body> <button id="registerBtn">Register</button> <div id="status">Ready</div> <script> let ua; const config = { uri: 'sip:alice@my-sip-server.local', // 必须与服务端realm一致 authorizationUser: 'alice', password: 'secret123', wsServers: ['ws://localhost:8080'], // 若用WebSocket,此处填WS地址 // 关键:禁用自动STUN/TURN,聚焦信令 hackIpInContact: true, // 强制用本地IP填Contact,方便调试 log: { level: 'debug', filter: 'sip' } // 开启详细日志 }; document.getElementById('registerBtn').onclick = async () => { try { // 创建UA实例 ua = new SIP.UA(config); // 监听注册事件 ua.on('registered', () => { document.getElementById('status').textContent = 'Registered!'; }); ua.on('unregistered', () => { document.getElementById('status').textContent = 'Unregistered'; }); ua.on('registrationFailed', (error) => { console.error('Registration failed:', error); document.getElementById('status').textContent = `Failed: ${error.message}`; }); // 启动注册 await ua.start(); } catch (e) { console.error('UA start error:', e); document.getElementById('status').textContent = `Error: ${e.message}`; } }; </script> </body> </html>

注意:uri字段必须是sip:username@realm,且realm必须与Python服务端self.realm完全一致(包括.local后缀)。我曾因服务端用"myserver"而前端用"myserver.local",导致401后无法完成挑战。

4.2 WebSocket vs UDP:为什么推荐先用UDP调试?

sip.js支持WebSocket(WSS)和UDP两种传输。强烈建议初学者先用UDP,原因有三:

  • 抓包直观:Wireshark可直接看到明文SIP消息,无需SSL解密;
  • 服务端简单:Python UDP Server代码<200行,WebSocket需额外处理HTTP Upgrade、TLS、心跳;
  • 错误定位快:UDP丢包直接表现为超时,而WS可能卡在TCP握手或证书验证。

一旦UDP注册成功,再平滑迁移到WebSocket(只需改wsServers和启动方式)。迁移时唯一要注意的是:WS连接需服务端支持HTTP Upgrade,我们可用aiohttp快速补全:

# 在Python服务端加一个HTTP端点 from aiohttp import web async def websocket_handler(request): ws = web.WebSocketResponse() await ws.prepare(request) # 此处可桥接到SIP逻辑,但初期可先返回405 await ws.close() return ws app = web.Application() app.router.add_get('/ws', websocket_handler) web.run_app(app, host='0.0.0.0', port=8080)

4.3 调试技巧:如何读懂sip.js的console日志

开启log: { level: 'debug' }后,控制台会输出海量日志。关键信息如下:

  • Sending REGISTER:确认请求发出,检查ToFromContact值;
  • Received 401 Unauthorized:说明服务端返回挑战,检查WWW-Authenticate头是否含noncerealm
  • Sending REGISTER with Authorization:确认客户端携带了Authorization头,复制整行到文本编辑器,用正则提取response值;
  • Registration failed: ...:末尾的...是具体错误,常见有Request Timeout(服务端没响应)、Unauthorized(Digest校验失败)、Bad Request(CSeq/Via格式错)。

最高效的调试组合是:浏览器控制台 + Wireshark + Python服务端print日志。三者对照,5分钟内必定位问题。

5. 实战排错:从Wireshark抓包到定位Digest校验失败的完整过程

理论讲完,现在来一场真实的排错演练。假设你已部署Python服务端和sip.js前端,点击注册后,状态一直显示“Failed: Request Timeout”。我们如何系统性排查?

5.1 第一步:确认网络连通性与基础消息流转

打开Wireshark,过滤udp.port == 5060,启动抓包,点击注册按钮。

  • 预期看到
    192.168.1.100 → 192.168.1.100UDP 5060 → REGISTER(客户端发)
    192.168.1.100 → 192.168.1.100UDP 5060 → 401 Unauthorized(服务端回)
    192.168.1.100 → 192.168.1.100UDP 5060 → REGISTER with Authorization(客户端再发)

  • 若只看到第一行:服务端根本没收到请求。检查Python是否监听0.0.0.0:5060(而非127.0.0.1),防火墙是否放行UDP 5060。

  • 若看到第一、二行,无第三行:客户端没收到401,或收到后没重发。检查sip.js日志是否有Received 401,以及WWW-Authenticate头格式是否合法(如realm是否带多余空格)。

5.2 第二步:聚焦401响应,验证挑战参数

双击第二行(401响应),展开Hypertext Transfer Protocol,找到WWW-Authenticate字段:

WWW-Authenticate: Digest realm="my-sip-server.local", nonce="1712345678-abc123", algorithm=MD5, qop="auth"
  • 检查realm是否与sip.js配置的uri后缀一致(sip:alice@my-sip-server.local→ realm必须是my-sip-server.local);
  • 检查nonce是否为有效字符串(无控制字符、长度合理);
  • 检查qop是否为"auth"(带英文双引号)。

提示:Wireshark里右键WWW-Authenticate→ “Copy” → “Value”,粘贴到编辑器,用正则realm="([^"]+)"提取realm,确保与代码完全一致。

5.3 第三步:提取Authorization头,手动验算Response

双击第三行(带Auth的REGISTER),展开Hypertext Transfer Protocol,找到Authorization字段:

Authorization: Digest username="alice", realm="my-sip-server.local", nonce="1712345678-abc123", uri="sip:my-sip-server.local", response="a1b2c3d4e5f67890...", cnonce="xyz789", nc="00000001", qop="auth"

现在,用Python脚本手动验算:

import hashlib # 从抓包中复制的值 username = "alice" realm = "my-sip-server.local" password = "secret123" nonce = "1712345678-abc123" uri = "sip:my-sip-server.local" cnonce = "xyz789" nc = "00000001" qop = "auth" method = "REGISTER" # 计算HA1 ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest() print("HA1:", ha1) # 计算HA2 ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest() print("HA2:", ha2) # 计算Response response_input = f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}" expected = hashlib.md5(response_input.encode()).hexdigest() print("Expected Response:", expected) # 抓包中的response actual = "a1b2c3d4e5f67890..." print("Actual Response:", actual) print("Match:", expected == actual)

运行后,若Match: False,说明服务端verify_digest函数某处出错。此时回到Python代码,逐行打印ha1ha2response_input,与脚本输出对比,必能找到差异点(如uri用了Request-URI而非Authorization中的uri)。

5.4 第四步:检查服务端日志与状态一致性

在Python服务端verify_digest开头加日志:

def verify_digest(self, auth_header, aor, method, request_uri): print(f"[DEBUG] auth_header: {auth_header}") print(f"[DEBUG] parsed params: {params}") print(f"[DEBUG] aor: {aor}, method: {method}, request_uri: {request_uri}") # ...后续计算

启动服务端,观察日志:

  • parsed params为空,说明正则没匹配到Authorization字段,检查header是否被截断(UDP MTU限制);
  • aorusername不一致,说明客户端To头域和Authorizationusername不同(RFC允许,但需服务端支持,我们当前实现要求一致);
  • request_uri为空,说明parse_headers没提取到uri,需检查Authorization头格式。

这套四步法,覆盖了95%的注册失败场景。记住:SIP调试不是猜,是比对。每一个字段,都必须在Wireshark、浏览器日志、服务端日志三处出现且完全一致。

6. 进阶扩展:从注册到通话的最小闭环

注册只是第一步。要让这个SIP服务器真正有用,还需两个关键扩展:状态查询接口基础呼叫路由。它们不增加复杂度,却极大提升实用性。

6.1 添加HTTP状态接口:用curl查看在线用户

aiohttp暴露一个简单的HTTP端点,返回当前注册用户列表:

from aiohttp import web async def status_handler(request): # 返回JSON格式的bindings active = {} now = time.time() for aor, binding in list(server.bindings.items()): if binding["expires"] > now: active[aor] = { "contact": binding["contact"], "expires_in": int(binding["expires"] - now), "call_id": binding["call_id"] } return web.json_response(active) # 在main()中添加 app = web.Application() app.router.add_get('/status', status_handler) # 启动HTTP服务(与SIP UDP并行) web.run_app(app, host='0.0.0.0', port=8081, print=False)

然后执行:

curl http://localhost:8081/status # 返回:{"sip:alice@my-sip-server.local": {"contact": "sip:alice@192.168.1.100:5060", "expires_in": 3592, "call_id": "abc123"}}

这个接口可用于监控、告警,或前端实时显示在线状态。

6.2 实现最简INVITE路由:让两个注册用户能互相呼叫

SIP通话的核心是INVITE请求的路由。我们不做复杂路由策略,只实现根据To头域查找Contact并转发

def handle_invite(self, msg, addr): headers = self.parse_headers(msg) to_match = re.search(r'To:\s*<([^>]+)>', msg) if not to_match: self.send_response(addr, "400 Bad Request", msg) return target_aor = to_match.group(1) if target_aor in self.bindings: binding = self.bindings[target_aor] # 解析Contact URI获取目标IP:PORT contact = binding["contact"] # 简单提取:sip:user@ip:port → ip:port ip_port_match = re.search(r'@([^>:]+):(\d+)', contact) if ip_port_match: target_ip = ip_port_match.group(1) target_port = int(ip_port_match.group(2)) # 修改Via头,添加自己的branch via = headers.get("Via", "") new_via = f"Via: {via};branch=z9hG4bK-{int(time.time())}" # 构造新INVITE,替换To/From/Contact为目标 new_msg = msg.replace(f"To: {headers['To']}", f"To: <{contact}>") new_msg = new_msg.replace(f"From: {headers['From']}", f"From: {headers['From']}") new_msg = new_msg.replace("Via:", f"Via: {new_via}") # 发送给目标 self.transport.sendto(new_msg.encode(), (target_ip, target_port)) return # 目标未注册,返回404 self.send_response(addr, "404 Not Found", msg)

然后在datagram_received中添加:

if msg.startswith("INVITE"): self.handle_invite(msg, addr)

这样,当Alice注册后,Bob用软电话拨打sip:alice@my-sip-server.local,服务端就会把INVITE转发给Alice的Contact地址。配合sip.js的SessionAPI,即可实现端到端音视频通话

http://www.jsqmd.com/news/861643/

相关文章:

  • BurpSuiteCN-Release:面向实战的中文渗透工作流重构
  • 填补 .NET 生态空白:面向工业视觉的高性能 3D 点云/网格处理库
  • 2026Q2机械密封销售厂家选择:强制循环泵、手动补液泵、机械密封供应厂家、机械密封品牌、机械密封工厂、机械密封生产厂家选择指南 - 优质品牌商家
  • PyCharm 2022.3 运行 Python 脚本提示解释器找不到怎么办?
  • 2026年比较好的涂料墨水直喷印染印花助剂/印染印花助剂皂洗剂厂家推荐与选型指南 - 行业平台推荐
  • 题解:洛谷 P3398 仓鼠找 sugar
  • Open MCT性能测试实战:JMeter多协议分层压测方法
  • Chrome多进程沙箱机制原理解析与安全加固实践
  • pytest Code Review skill.md
  • Burp Suite混合加密流量解密实战:JS+Native加解密链路还原
  • AI漫剧创作教程:体验更流畅的创作流程,更好的效果
  • SpaceX启动纳斯达克IPO,1.75万亿美元市值目标能否实现?
  • TensorFlow模型API安全扫描与漏洞修复实战指南
  • edu 域名注册之旅
  • 听劝和辨劝
  • 2026成都租客车:成都租旅游大巴车、成都租旅游车、四川大巴包车、四川大巴租赁、四川大巴车租赁、四川客车租赁、四川旅游大巴车租赁选择指南 - 优质品牌商家
  • 2026年现阶段福州文化墙制作公司深度解析与核心厂商推荐 - 2026年企业推荐榜
  • Midjourney玻璃表现TOP3失败案例(含错误参数截图+修复前后PSD对比),工程师私藏调试日志首次公开
  • 2026年5月兰州装修设计质量排行:兰州装饰公司、兰州本地装修公司、兰州装修公司、兰州装修工作室、兰州装修设计公司选择指南 - 优质品牌商家
  • 题解:洛谷 P1670 [USACO04DEC] Tree Cutting S
  • Unity配置管理实战:Luban实现Excel到C#类型安全配置
  • B站成分检测器:揭秘评论区背后的用户画像,3分钟开启智能社交分析
  • PHP版本升级不是换镜像:漏洞修复中的兼容性实战指南
  • 基于CC2530 ZigBee的智慧农业控制系统:从硬件设计到低功耗组网实战
  • Godot内存泄漏三大根源与自动化防治方案
  • 2025降AI工具测评:10款实测软件附免费方案
  • Chromium沙箱机制与GPU进程安全实践指南
  • 2026耐高温涂料技术解析:户外工程防腐涂料、无毒油漆、无毒饮水舱油漆、无毒饮水舱涂料、无溶剂环氧涂料、机场钢结构防腐涂料选择指南 - 优质品牌商家
  • WebStorm 保存文件时自动格式化失败报错怎么修复?
  • Pandas 核心操作指南:索引、筛选、赋值与函数应用