package com.inspect.tcpserver.sip.service; import com.inspect.tcpserver.sip.items.SipEventRegistry; import com.inspect.tcpserver.sip.utils.DigestUtil; import com.inspect.tcpserver.sip.utils.SipXmlEnvelope; import com.inspect.tcpserver.sip.utils.SipXmlParser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.sip.*; import javax.sip.address.Address; import javax.sip.address.AddressFactory; import javax.sip.address.SipURI; import javax.sip.header.*; import javax.sip.message.MessageFactory; import javax.sip.message.Request; import javax.sip.message.Response; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Service @ConditionalOnProperty(name = "sip.enable", havingValue = "true") public class SipClientService implements SipListener { private SipFactory sipFactory; private SipStack sipStack; private SipProvider sipProvider; private MessageFactory messageFactory; private HeaderFactory headerFactory; private AddressFactory addressFactory; // 保存上次请求信息,重发用 private String lastDeviceCode; private String lastServerIp; private int lastServerPort; private String lastXmlBody; // 保存 WWW-Authenticate 参数 private String lastNonce; private String lastRealm; private String lastOpaque; private String lastQop; @Value("${sip.username}") private String username; @Value("${sip.password}") private String password; @Value("${sip.domain}") private String domain; @Value("${sip.port}") private int port; @Value("${sip.transport}") private String transport; @Value("${sip.local-ip}") private String localIp; @Value("${sip.local-port}") private int localPort; @Value("${sip.expires}") private int expires; private ClientTransaction lastRegisterTransaction; private Timer refreshTimer = new Timer(true); private String fromTag = Long.toHexString(System.currentTimeMillis()); private AtomicInteger cSeqCounter = new AtomicInteger(1); String testXml = "\n" + "\n" + " 1234567890\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; @PostConstruct public void init() throws Exception { sipFactory = SipFactory.getInstance(); sipFactory.setPathName("gov.nist"); headerFactory = sipFactory.createHeaderFactory(); addressFactory = sipFactory.createAddressFactory(); messageFactory = sipFactory.createMessageFactory(); Properties properties = new Properties(); properties.setProperty("javax.sip.STACK_NAME", "spring-boot-sip-stack"); properties.setProperty("gov.nist.javax.sip.IP_ADDRESS", localIp); properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sipdebug.txt"); properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sipserverlog.txt"); sipStack = sipFactory.createSipStack(properties); ListeningPoint lp = sipStack.createListeningPoint(localIp, localPort, transport); sipProvider = sipStack.createSipProvider(lp); sipProvider.addSipListener(this); sendRegister(null); } public void sendRegister(AuthorizationHeader authHeader) throws Exception { SipURI requestUri = addressFactory.createSipURI(null, domain); requestUri.setTransportParam(transport.toUpperCase()); requestUri.setPort(port); Address fromAddress = addressFactory.createAddress(username, addressFactory.createSipURI(username, domain)); FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, fromTag); Address toAddress = addressFactory.createAddress(username, addressFactory.createSipURI(username, domain)); ToHeader toHeader = headerFactory.createToHeader(toAddress, null); CallIdHeader callId = sipProvider.getNewCallId(); CSeqHeader cSeq = headerFactory.createCSeqHeader(1L, Request.REGISTER); MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70); // 创建带自定义deviceid参数的Contact URI SipURI contactUri = addressFactory.createSipURI(username, localIp); contactUri.setPort(localPort); contactUri.setTransportParam(transport.toUpperCase()); contactUri.setParameter("deviceid", "123456"); Address contactAddress = addressFactory.createAddress(contactUri); ContactHeader contactHeader = headerFactory.createContactHeader(contactAddress); ExpiresHeader expiresHeader = headerFactory.createExpiresHeader(expires); List viaHeaders = new ArrayList<>(); ViaHeader viaHeader = headerFactory.createViaHeader(localIp, localPort, transport.toUpperCase(), null); viaHeaders.add(viaHeader); Request request = messageFactory.createRequest( requestUri, Request.REGISTER, callId, cSeq, fromHeader, toHeader, viaHeaders, maxForwards ); request.addHeader(contactHeader); request.addHeader(expiresHeader); // 添加自定义头,携带设备编号 Header deviceIdHeader = headerFactory.createHeader("X-Device-ID", "123456"); // 设备编号 request.addHeader(deviceIdHeader); if (authHeader != null) { request.addHeader(authHeader); } lastRegisterTransaction = sipProvider.getNewClientTransaction(request); lastRegisterTransaction.sendRequest(); } public void sendXmlResource(String targetSipUri, String xml) throws Exception { log.info("Sending XML resource to {}", targetSipUri); SipURI fromUri = addressFactory.createSipURI(username, domain + ":" + port); Address fromAddress = addressFactory.createAddress(fromUri); FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, fromTag); javax.sip.address.URI toUri = addressFactory.createURI(targetSipUri); Address toAddress = addressFactory.createAddress(toUri); ToHeader toHeader = headerFactory.createToHeader(toAddress, null); // 创建空的Via头列表 List viaHeaders = new ArrayList<>(); Request message = messageFactory.createRequest( toUri, Request.MESSAGE, sipProvider.getNewCallId(), headerFactory.createCSeqHeader(1L, Request.MESSAGE), fromHeader, toHeader, viaHeaders, headerFactory.createMaxForwardsHeader(70) ); // 手动创建Via头并添加 ViaHeader viaHeader = headerFactory.createViaHeader(localIp, localPort, "UDP", null); message.addHeader(viaHeader); ContentTypeHeader ct = headerFactory.createContentTypeHeader("Application", "MANSCDP+xml"); message.setContent(xml, ct); ClientTransaction tx = sipProvider.getNewClientTransaction(message); tx.sendRequest(); } @Override public void processResponse(ResponseEvent responseEvent) { Response response = responseEvent.getResponse(); int status = response.getStatusCode(); log.info("Received response: {}", status); CSeqHeader cSeqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME); if (cSeqHeader == null) return; String method = cSeqHeader.getMethod(); try { if (Request.REGISTER.equalsIgnoreCase(method)) { if (status == 401 || status == 407) { log.info("Received 401/407, attempt auth..."); WWWAuthenticateHeader wwwAuth = (WWWAuthenticateHeader) response.getHeader(WWWAuthenticateHeader.NAME); if (wwwAuth == null) { log.error("No WWW-Authenticate header found, cannot authenticate"); return; } String realm = wwwAuth.getRealm(); String nonce = wwwAuth.getNonce(); String qop = wwwAuth.getQop(); String uri = "sip:" + domain + ":" + port; String nc = "00000001"; String cNonce = generateCNonce(); String responseDigest = DigestUtil.computeResponse( username, password, realm, nonce, "REGISTER", uri, nc, cNonce, qop ); AuthorizationHeader authHeader = headerFactory.createAuthorizationHeader("Digest"); authHeader.setUsername(username); authHeader.setRealm(realm); authHeader.setNonce(nonce); authHeader.setURI(addressFactory.createURI(uri)); authHeader.setResponse(responseDigest); if (qop != null) { authHeader.setQop(qop); authHeader.setCNonce(cNonce); authHeader.setNonceCount(Integer.parseInt(nc, 16)); } sendRegister(authHeader); } else if (status >= 200 && status < 300) { log.info("REGISTER success: {}", status); int delay = Math.max(5, expires - 60); refreshTimer.schedule(new TimerTask() { @Override public void run() { try { sendRegister(null); } catch (Exception e) { log.error("Failed to refresh register", e); } } }, delay * 1000L); sendNotify(username, domain, port, testXml); } else { log.warn("REGISTER response: {}", status); } } else if (Request.MESSAGE.equalsIgnoreCase(method)) { log.info("MESSAGE response: {}", status); } else if (Request.NOTIFY.equalsIgnoreCase(method)) { log.info("NOTIFY response: {}", status); if (status == 401) { WWWAuthenticateHeader wwwAuth = (WWWAuthenticateHeader) response.getHeader(WWWAuthenticateHeader.NAME); if (wwwAuth != null) { lastNonce = wwwAuth.getNonce(); lastRealm = wwwAuth.getRealm(); lastOpaque = wwwAuth.getOpaque(); lastQop = wwwAuth.getQop(); try { resendNotifyWithAuth(); } catch (Exception e) { e.printStackTrace(); } } } } } catch (Exception ex) { log.error("processResponse error", ex); } } private void resendNotifyWithAuth() throws Exception { String method = Request.NOTIFY; String uriStr = lastDeviceCode + "@" + lastServerIp; String nc = "00000001"; String cNonce = md5Hex(Long.toString(System.currentTimeMillis())); String responseDigest = computeResponse( username, lastRealm, password, method, "sip:" + uriStr, lastNonce, nc, cNonce, lastQop != null ? lastQop : "auth" ); AuthorizationHeader authHeader = headerFactory.createAuthorizationHeader("Digest"); authHeader.setUsername(username); authHeader.setRealm(lastRealm); authHeader.setNonce(lastNonce); authHeader.setURI(addressFactory.createURI("sip:" + uriStr)); authHeader.setResponse(responseDigest); authHeader.setAlgorithm("MD5"); authHeader.setCNonce(cNonce); if (lastOpaque != null) authHeader.setOpaque(lastOpaque); authHeader.setQop(lastQop != null ? lastQop : "auth"); authHeader.setNonceCount(1); Request requestWithAuth = createNotifyRequest(lastDeviceCode, lastServerIp, lastServerPort, lastXmlBody, authHeader); ClientTransaction transaction = sipProvider.getNewClientTransaction(requestWithAuth); transaction.sendRequest(); } // 构造NOTIFY请求 private Request createNotifyRequest(String deviceCode, String serverIp, int serverPort, String xmlBody, AuthorizationHeader authHeader) throws Exception { String notifierUser = "290010021201070000"; String localHost = this.localIp; int localPort = this.localPort; SipURI requestUri = addressFactory.createSipURI(deviceCode, serverIp); requestUri.setTransportParam("tcp"); requestUri.setPort(serverPort); Address fromAddress = addressFactory.createAddress("sip:" + username + "@" + localHost); FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, UUID.randomUUID().toString().substring(0, 8)); Address toAddress = addressFactory.createAddress("sip:" + deviceCode + "@" + serverIp); ToHeader toHeader = headerFactory.createToHeader(toAddress, null); CallIdHeader callId = sipProvider.getNewCallId(); CSeqHeader cSeq = headerFactory.createCSeqHeader(cSeqCounter.getAndIncrement(), Request.NOTIFY); MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70); List viaHeaders = new ArrayList<>(); ViaHeader viaHeader = headerFactory.createViaHeader(localHost, localPort, transport, null); viaHeaders.add(viaHeader); Address contactAddr = addressFactory.createAddress("sip:" + username + "@" + localHost + ":" + localPort); ContactHeader contactHeader = headerFactory.createContactHeader(contactAddr); EventHeader eventHeader = headerFactory.createEventHeader("Push_Resource"); SubscriptionStateHeader subscriptionStateHeader = headerFactory.createSubscriptionStateHeader(SubscriptionStateHeader.TERMINATED); ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "xml"); Request request = messageFactory.createRequest( requestUri, Request.NOTIFY, callId, cSeq, fromHeader, toHeader, viaHeaders, maxForwards ); request.addHeader(contactHeader); request.addHeader(eventHeader); request.addHeader(subscriptionStateHeader); request.addHeader(contentTypeHeader); request.setContent(xmlBody, contentTypeHeader); if (authHeader != null) { request.addHeader(authHeader); } return request; } private String generateCNonce() { return Long.toHexString(System.currentTimeMillis() & 0xffffffffL); } @Override public void processRequest(RequestEvent requestEvent) { Request request = requestEvent.getRequest(); String method = request.getMethod(); log.info("Received SIP request: {}", method); try { if (Request.MESSAGE.equalsIgnoreCase(method)) { log.info("Processing MESSAGE request from {}", ((FromHeader) request.getHeader(FromHeader.NAME)).getAddress()); ContentTypeHeader contentTypeHeader = (ContentTypeHeader) request.getHeader(ContentTypeHeader.NAME); if (contentTypeHeader != null && "application".equalsIgnoreCase(contentTypeHeader.getContentType()) && "xml".equalsIgnoreCase(contentTypeHeader.getContentSubType())) { String xml = new String(request.getRawContent(), StandardCharsets.UTF_8); xml = xml.replace("”", "\"").replace("“", "\""); log.info("MESSAGE body:\n{}", xml); String eventType = SipXmlParser.peekEventType(xml); log.info("RECOGNIZE EventType = {}", eventType); Class itemClass = SipEventRegistry.getItemClass(eventType); if (itemClass == null) { log.warn("UNREGISTERED EventType: {}", eventType); return; } SipXmlEnvelope envelope = SipXmlParser.parse(xml, itemClass); //handleSipEvent(envelope); } } else { Response notImpl = messageFactory.createResponse(Response.NOT_IMPLEMENTED, request); requestEvent.getServerTransaction().sendResponse(notImpl); log.warn("Method {} not implemented, replied 501", method); } } catch (Exception ex) { log.error("Error processing request: {}", ex.getMessage(), ex); try { Response errorResponse = messageFactory.createResponse(Response.SERVER_INTERNAL_ERROR, request); requestEvent.getServerTransaction().sendResponse(errorResponse); } catch (Exception e) { log.error("Failed to send error response", e); } } } @Override public void processTimeout(TimeoutEvent timeoutEvent) { log.warn("SIP timeout: {}", timeoutEvent); } @Override public void processIOException(IOExceptionEvent exceptionEvent) { log.error("SIP IO Exception: {}", exceptionEvent); } @Override public void processTransactionTerminated(TransactionTerminatedEvent tte) { } @Override public void processDialogTerminated(DialogTerminatedEvent dte) { } @PreDestroy public void shutdown() { log.info("Shutting down SIP stack..."); refreshTimer.cancel(); if (sipProvider != null) sipProvider.removeSipListener(this); if (sipStack != null) sipStack.stop(); } /** * 发送 SIP NOTIFY 消息 * * @param deviceCode 客户端编码(也是 SIP 用户名) * @param serverIp 服务器 IP * @param serverPort 服务器 平台端口 * @param xmlBody XML 消息体 * @throws Exception */ // 公开调用,发送NOTIFY(首次不带认证) public void sendNotify(String deviceCode, String serverIp, int serverPort, String xmlBody) throws Exception { lastDeviceCode = deviceCode; lastServerIp = serverIp; lastServerPort = serverPort; lastXmlBody = xmlBody; Request request = createNotifyRequest(deviceCode, serverIp, serverPort, xmlBody, null); ClientTransaction transaction = sipProvider.getNewClientTransaction(request); transaction.sendRequest(); } /** * 分片 XML(保证每块 <= maxBytes) */ private List splitXml(String xml, int maxBytes) throws UnsupportedEncodingException { List parts = new ArrayList<>(); byte[] data = xml.getBytes("UTF-8"); int offset = 0; while (offset < data.length) { int len = Math.min(maxBytes, data.length - offset); parts.add(new String(data, offset, len, "UTF-8")); offset += len; } return parts; } // 计算Digest响应值 private String computeResponse(String username, String realm, String password, String method, String uri, String nonce, String nc, String cNonce, String qop) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); String ha1 = md5Hex(md, username + ":" + realm + ":" + password); String ha2 = md5Hex(md, method + ":" + uri); return md5Hex(md, ha1 + ":" + nonce + ":" + nc + ":" + cNonce + ":" + qop + ":" + ha2); } private String md5Hex(String data) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); return md5Hex(md, data); } private String md5Hex(MessageDigest md, String data) { byte[] digest = md.digest(data.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b & 0xff)); } return sb.toString(); } }