|
|
|
@ -0,0 +1,583 @@ |
|
|
|
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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + |
|
|
|
"<SIP_XML EventType=\"Push_Resource\">\n" + |
|
|
|
" <Code>1234567890</Code>\n" + |
|
|
|
" <SubList SubNum=\"50\">\n" + |
|
|
|
" <Item Code=\"CAM001 \" Name=\"摄像头1 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.12\" Latitude=\"30.28\" SubNum=\"1 \"/>\n" + |
|
|
|
" <Item Code=\"CAM002 \" Name=\"摄像头2 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"2 \"/>\n" + |
|
|
|
" <Item Code=\"CAM003 \" Name=\"摄像头3 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"3 \"/>\n" + |
|
|
|
" <Item Code=\"CAM004 \" Name=\"摄像头4 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"4 \"/>\n" + |
|
|
|
" <Item Code=\"CAM005 \" Name=\"摄像头5 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"5 \"/>\n" + |
|
|
|
" <Item Code=\"CAM006 \" Name=\"摄像头6 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"6 \"/>\n" + |
|
|
|
" <Item Code=\"CAM007 \" Name=\"摄像头7 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"7 \"/>\n" + |
|
|
|
" <Item Code=\"CAM008 \" Name=\"摄像头8 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"8 \"/>\n" + |
|
|
|
" <Item Code=\"CAM009 \" Name=\"摄像头9 \" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"9 \"/>\n" + |
|
|
|
" <Item Code=\"CAM0010\" Name=\"摄像头10\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"10\"/>\n" + |
|
|
|
" <Item Code=\"CAM0011\" Name=\"摄像头11\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"11\"/>\n" + |
|
|
|
" <Item Code=\"CAM0012\" Name=\"摄像头12\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"12\"/>\n" + |
|
|
|
" <Item Code=\"CAM0013\" Name=\"摄像头13\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"13\"/>\n" + |
|
|
|
" <Item Code=\"CAM0014\" Name=\"摄像头14\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"14\"/>\n" + |
|
|
|
" <Item Code=\"CAM0015\" Name=\"摄像头15\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"15\"/>\n" + |
|
|
|
" <Item Code=\"CAM0016\" Name=\"摄像头16\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"16\"/>\n" + |
|
|
|
" <Item Code=\"CAM0017\" Name=\"摄像头17\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"17\"/>\n" + |
|
|
|
" <Item Code=\"CAM0018\" Name=\"摄像头18\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"18\"/>\n" + |
|
|
|
" <Item Code=\"CAM0019\" Name=\"摄像头19\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"19\"/>\n" + |
|
|
|
" <Item Code=\"CAM0020\" Name=\"摄像头20\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"20\"/>\n" + |
|
|
|
" <Item Code=\"CAM0021\" Name=\"摄像头21\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"21\"/>\n" + |
|
|
|
" <Item Code=\"CAM0022\" Name=\"摄像头22\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"22\"/>\n" + |
|
|
|
" <Item Code=\"CAM0023\" Name=\"摄像头23\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"23\"/>\n" + |
|
|
|
" <Item Code=\"CAM0024\" Name=\"摄像头24\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"24\"/>\n" + |
|
|
|
" <Item Code=\"CAM0025\" Name=\"摄像头25\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"25\"/>\n" + |
|
|
|
" <Item Code=\"CAM0026\" Name=\"摄像头26\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"26\"/>\n" + |
|
|
|
" <Item Code=\"CAM0027\" Name=\"摄像头27\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"27\"/>\n" + |
|
|
|
" <Item Code=\"CAM0028\" Name=\"摄像头28\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"28\"/>\n" + |
|
|
|
" <Item Code=\"CAM0029\" Name=\"摄像头29\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"29\"/>\n" + |
|
|
|
" <Item Code=\"CAM0030\" Name=\"摄像头30\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"30\"/>\n" + |
|
|
|
" <Item Code=\"CAM0031\" Name=\"摄像头31\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"31\"/>\n" + |
|
|
|
" <Item Code=\"CAM0032\" Name=\"摄像头32\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"32\"/>\n" + |
|
|
|
" <Item Code=\"CAM0033\" Name=\"摄像头33\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"33\"/>\n" + |
|
|
|
" <Item Code=\"CAM0034\" Name=\"摄像头34\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"34\"/>\n" + |
|
|
|
" <Item Code=\"CAM0035\" Name=\"摄像头35\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"35\"/>\n" + |
|
|
|
" <Item Code=\"CAM0036\" Name=\"摄像头36\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"36\"/>\n" + |
|
|
|
" <Item Code=\"CAM0037\" Name=\"摄像头37\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"37\"/>\n" + |
|
|
|
" <Item Code=\"CAM0038\" Name=\"摄像头38\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"38\"/>\n" + |
|
|
|
" <Item Code=\"CAM0039\" Name=\"摄像头39\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"39\"/>\n" + |
|
|
|
" <Item Code=\"CAM0040\" Name=\"摄像头40\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"40\"/>\n" + |
|
|
|
" <Item Code=\"CAM0041\" Name=\"摄像头41\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"41\"/>\n" + |
|
|
|
" <Item Code=\"CAM0042\" Name=\"摄像头42\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"42\"/>\n" + |
|
|
|
" <Item Code=\"CAM0043\" Name=\"摄像头43\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"43\"/>\n" + |
|
|
|
" <Item Code=\"CAM0044\" Name=\"摄像头44\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"44\"/>\n" + |
|
|
|
" <Item Code=\"CAM0045\" Name=\"摄像头45\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"45\"/>\n" + |
|
|
|
" <Item Code=\"CAM0046\" Name=\"摄像头46\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"46\"/>\n" + |
|
|
|
" <Item Code=\"CAM0047\" Name=\"摄像头47\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"47\"/>\n" + |
|
|
|
" <Item Code=\"CAM0048\" Name=\"摄像头48\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"48\"/>\n" + |
|
|
|
" <Item Code=\"CAM0049\" Name=\"摄像头49\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"49\"/>\n" + |
|
|
|
" <Item Code=\"CAM0050\" Name=\"摄像头50\" Status=\"1\" DecoderTag=\"H264\" Longitude=\"120.15\" Latitude=\"30.32\" SubNum=\"50\"/>\n" + |
|
|
|
" </SubList>\n" + |
|
|
|
"</SIP_XML>\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<ViaHeader> 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<ViaHeader> 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<ViaHeader> 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<String> splitXml(String xml, int maxBytes) throws UnsupportedEncodingException { |
|
|
|
List<String> 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(); |
|
|
|
} |
|
|
|
} |