SIP協議是一個文本協議,比如下面是話機注冊的首次REGISTER請求:
REGISTER sip:10.32.26.25:5070;transport=tcp SIP/2.0 Via: SIP/2.0/TCP 10.32.26.25:51696;rport;branch=z9hG4bKPj8d4db68b24754f539dbf3b563a44fe55;alias Max-Forwards: 70 From: jimmy<sip:1000@10.32.26.25>;tag=89aefb1f3fc0413283a453eda5407f60 To: <sip:1000@10.32.26.25> Call-ID: 1e7af0e67a5044658fc7f6716d329642 CSeq: 36850 REGISTER User-Agent: MicroSIP/3.20.3 Supported: outbound, path Contact: <sip:1000@10.32.26.25:51696;transport=TCP;ob>;reg-id=1;+sip.instance="<urn:uuid:00000000-0000-0000-0000-000011058e7e>" Expires: 300 Allow: PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS Content-Length: 0
技術上講,完全可以逐行按String解析,白手起家,拆解出其中的內容,但是這樣做一來有些原始,二來也未必高效,幸好社區里已經類似的開源項目:pkts ,借助這個開源項目,可以很方便的把上述內容快速解析出來,示例代碼如下:
先添加pom依賴(目前最新是3.0.11-SNAPSHOT)
<dependency>
<groupId>io.pkts</groupId>
<artifactId>pkts-sip</artifactId>
<version>3.0.11-SNAPSHOT</version>
</dependency>
然后就可以解析了:
@Test
public void testParseRegister() throws IOException {
StringBuilder register = new StringBuilder("REGISTER sip:10.32.26.25:5070;transport=tcp SIP/2.0\r\n" +
"Via: SIP/2.0/TCP 10.32.26.25:51696;rport;branch=z9hG4bKPj8d4db68b24754f539dbf3b563a44fe55;alias\r\n" +
"Max-Forwards: 70\r\n" +
"From: <sip:1000@10.32.26.25>;tag=89aefb1f3fc0413283a453eda5407f60\r\n" +
"To: jimmy<sip:1000@10.32.26.25>\r\n" +
"Call-ID: 1e7af0e67a5044658fc7f6716d329642\r\n" +
"CSeq: 36850 REGISTER\r\n" +
"User-Agent: MicroSIP/3.20.3\r\n" +
"Supported: outbound, path\r\n" +
"Contact: <sip:1000@10.32.26.25:51696;transport=TCP;ob>;reg-id=1;+sip.instance=\"<urn:uuid:00000000-0000-0000-0000-000011058e7e>\"\r\n" +
"Expires: 300\r\n" +
"Allow: PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS\r\n" +
"Content-Length: 0\r\n");
SipMessage msgMessage = SipParser.frame(Buffers.wrap(register.toString()));
if (msgMessage.isRegisterRequest()) {
System.out.println("This is a REGISTER request");
}
Buffer method = msgMessage.getMethod();
System.out.println("方法:" + method + "\n");
Buffer initialLine = msgMessage.getInitialLine();
System.out.println("第一行:" + initialLine + "\n");
List<ViaHeader> viaHeaders = msgMessage.getViaHeaders();
System.out.println("via:");
for (ViaHeader viaHeader : viaHeaders) {
System.out.println("host:" + viaHeader.getHost() + ",branch:" + viaHeader.getBranch() + ",alias:" + viaHeader.getParameter("alias"));
}
MaxForwardsHeader maxForwards = msgMessage.getMaxForwards();
System.out.println("\nmaxForwards:" + maxForwards.getMaxForwards());
FromHeader fromHeader = msgMessage.getFromHeader();
System.out.println("\nfrom-tag:" + fromHeader.getTag());
ToHeader toHeader = msgMessage.getToHeader();
System.out.println("\nto:" + toHeader.getAddress().getDisplayName());
CallIdHeader callIDHeader = msgMessage.getCallIDHeader();
System.out.println("\ncallId:" + callIDHeader.getCallId());
CSeqHeader cSeqHeader = msgMessage.getCSeqHeader();
System.out.println("\ncSeq:" + cSeqHeader.getSeqNumber());
Optional<SipHeader> userAgentHeader = msgMessage.getHeader("User-Agent");
System.out.println("\nuserAgent value:" + userAgentHeader.get().getValue());
Optional<SipHeader> supported = msgMessage.getHeader("Supported");
System.out.println("\nsupported name:" + supported.get().getName());
ContactHeader contactHeader = msgMessage.getContactHeader();
System.out.println("\ncontact reg-id:" + contactHeader.getParameter("reg-id"));
ExpiresHeader expiresHeader = msgMessage.getExpiresHeader();
System.out.println("\nexpires:" + expiresHeader.getExpires());
Optional<SipHeader> allowHeader = msgMessage.getHeader("Allow");
System.out.println("\nallow:" + allowHeader.get().getValue());
int contentLength = msgMessage.getContentLength();
System.out.println("\ncontentLength:" + contentLength);
}
輸出如下:
This is a REGISTER request 方法:REGISTER 第一行:REGISTER sip:10.32.26.25:5070;transport=tcp SIP/2.0 via: host:10.32.26.25,branch:z9hG4bKPj8d4db68b24754f539dbf3b563a44fe55,alias:null maxForwards:70 from-tag:89aefb1f3fc0413283a453eda5407f60 to:jimmy callId:1e7af0e67a5044658fc7f6716d329642 cSeq:36850 userAgent value:MicroSIP/3.20.3 supported name:Supported contact reg-id:1 expires:300 allow:PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS contentLength:0
pkts-sip的解析非常高效,其主要設計思路借鑒了netty的buffer,自定義類似的buffer結構,內部有 readerIndex、writerIndex、markedReaderIndex、lowerBoundary、upperBoundary幾個標識,可以快速讀取或寫入。

最常用的ByteBuffer內部數據存儲於byte[]數組,值類型的變量直接在堆外內存區分配,無需JVM來GC。
SIP中常見的各種Header解析,pkts-sip已經做了實現,類圖如下:

一個完整的SIP報文,正如最開始的解析示例代碼,最終會被解析成SipMessage,根據該報文是Request還是Response,又派生出2個子類:

SipMessage中的核心部分,就是各種SIpHeader實例。
除了解析,pkts-sip還可以組裝各種SIP報文,仍然以開頭這段REGISTER為例,如果服務端收到這個注冊請求,可以方便的組裝Response進行回應:
@Test
public void testBuildRegisterResponse() throws IOException {
StringBuilder register = new StringBuilder("REGISTER sip:10.32.26.25:5070;transport=tcp SIP/2.0\r\n" +
"Via: SIP/2.0/TCP 10.32.26.25:51696;rport;branch=z9hG4bKPj8d4db68b24754f539dbf3b563a44fe55;alias\r\n" +
"Max-Forwards: 70\r\n" +
"From: <sip:1000@10.32.26.25>;tag=89aefb1f3fc0413283a453eda5407f60\r\n" +
"To: jimmy<sip:1000@10.32.26.25>\r\n" +
"Call-ID: 1e7af0e67a5044658fc7f6716d329642\r\n" +
"CSeq: 36850 REGISTER\r\n" +
"User-Agent: MicroSIP/3.20.3\r\n" +
"Supported: outbound, path\r\n" +
"Contact: <sip:1000@10.32.26.25:51696;transport=TCP;ob>;reg-id=1;+sip.instance=\"<urn:uuid:00000000-0000-0000-0000-000011058e7e>\"\r\n" +
"Expires: 300\r\n" +
"Allow: PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS\r\n" +
"Content-Length: 0\r\n");
SipMessage msgMessage = SipParser.frame(Buffers.wrap(register.toString()));
SipResponse sipResponse = msgMessage.createResponse(401)
.withHeader(SipHeader.create("User-Agent", "FreeSWITCH-mod_sofia/1.6.18+git~20170612T211449Z~6e79667c0a~64bit"))
.withHeader(SipHeader.create("Allow", "INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE"))
.withHeader(SipHeader.create("Supported", "timer, path, replaces"))
.withHeader(SipHeader.create("WWW-Authenticate", "Digest realm=\"10.32.26.25\", nonce=\"bee3366b-cf59-476e-bc5e-334e0d65b386\", algorithm=MD5, qop=\"auth\""))
.withHeader(new ContentLengthHeader.Builder(0).build())
.build();
System.out.println(sipResponse);
}
輸出如下:
SIP/2.0 401 Unauthorized Call-ID: 1e7af0e67a5044658fc7f6716d329642 CSeq: 36850 REGISTER WWW-Authenticate: Digest realm="10.32.26.25", nonce="bee3366b-cf59-476e-bc5e-334e0d65b386", algorithm=MD5, qop="auth" User-Agent: FreeSWITCH-mod_sofia/1.6.18+git~20170612T211449Z~6e79667c0a~64bit To: jimmy<sip:1000@10.32.26.25> From: <sip:1000@10.32.26.25>;tag=89aefb1f3fc0413283a453eda5407f60 Content-Length: 0 Supported: timer, path, replaces Via: SIP/2.0/TCP 10.32.26.25:51696;rport;branch=z9hG4bKPj8d4db68b24754f539dbf3b563a44fe55;alias Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE
可能有細心的同學發現了,最終輸出的報文,每行的出現順序好象有點怪,比如Content-Length:0,是在最后添加進去的,但卻是在中間出現。可以看下io.pkts.packet.sip.impl.SipMessageBuilder#build的源碼:

597行這里,finalHeaders是一個HashMap,眾所周知HashMap是不能保證順序的,對順序十分在意的同學,可以換成LinkedHashMap,另外從代碼可以看出,viaHeaders是放在常規Headers之后組裝的,一般我們習慣於把Via放在最開始,大家可以把這2段代碼的位置互換一下。

改完之后,再跑一下代碼:
SIP/2.0 401 Unauthorized Via: SIP/2.0/TCP 10.32.26.25:51696;rport;branch=z9hG4bKPj8d4db68b24754f539dbf3b563a44fe55;alias From: <sip:1000@10.32.26.25>;tag=89aefb1f3fc0413283a453eda5407f60 To: jimmy<sip:1000@10.32.26.25> CSeq: 36850 REGISTER Call-ID: 1e7af0e67a5044658fc7f6716d329642 User-Agent: FreeSWITCH-mod_sofia/1.6.18+git~20170612T211449Z~6e79667c0a~64bit Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, MESSAGE, INFO, UPDATE, REGISTER, REFER, NOTIFY, PUBLISH, SUBSCRIBE Supported: timer, path, replaces WWW-Authenticate: Digest realm="10.32.26.25", nonce="bee3366b-cf59-476e-bc5e-334e0d65b386", algorithm=MD5, qop="auth" Content-Length: 0
看上去順眼多了,此外從源代碼可以看到,ptks-sip在構造各種Header時,大量使用了Builder設計模式(比如下圖中的FromHeader.Builder),可以方便的用withXXX(...),得到一個XXXBuilder實例,最后調用build()方法生成想要的XXXHeader實例。

最后來談下如何擴展ptks未支持的Header,一般情況下,如果ptks不支持的Header,比如:
WWW-Authenticate: Digest realm="10.32.26.25", nonce="bee3366b-cf59-476e-bc5e-334e0d65b386", algorithm=MD5, qop="auth"
解析后,會生成默認的SipHeaderImpl實例列表,參考下圖:

這樣在使用時,並不方便,最好是希望能看FromHeader類似,只生成1個特定的WWWAuthenticateHeader實例,並且能類似getRealm()、getNonce()...得到相關的屬性值。ptks-sip的readme里,告訴了大家擴展的步驟,我把主要部分列了下:
1、先定義一個XXXHeader的接口,比如:WWWAuthenticateHeader
2、XXXHeader接口里,實現static frame()方法(注:jdk 1.8開始,接口可以添加方法實現)
3、XXXHeader接口里,定義copy()方法
4、SipHeader接口中添加isXXX()以及toXXX()方法
5、XXXHeader接口里,定義ensure()方法,並返回this
6、實現XXXHeader,定義一個XXXHeaderImpl類,核心的解析工作,就放在這個類的frame方法中完成
7、SipParser類中,添加XXXHeader的注冊信息
8、單元測試
按這個步驟,先來定義一個WWWAuthenticateHeader
package io.pkts.packet.sip.header;
import io.pkts.buffer.Buffer;
import io.pkts.buffer.Buffers;
import io.pkts.packet.sip.SipParseException;
import io.pkts.packet.sip.header.impl.WWWAuthenticateHeaderImpl;
public interface WWWAuthenticateHeader extends SipHeader {
Buffer NAME = Buffers.wrap("WWW-Authenticate");
Buffer getRealm();
Buffer getNonce();
Buffer getAlgorithm();
Buffer getQop();
static WWWAuthenticateHeader frame(final Buffer buffer) throws SipParseException {
try {
return new WWWAuthenticateHeader.Builder(buffer).build();
} catch (final Exception e) {
throw new SipParseException(0, "Unable to frame the WWWAuthenticate header due to IOException", e);
}
}
@Override
default WWWAuthenticateHeader toWWWAuthenticateHeader() {
return this;
}
class Builder implements SipHeader.Builder<WWWAuthenticateHeader> {
private Buffer value;
private Buffer realm;
private Buffer nonce;
private Buffer algorithm;
private Buffer qop;
public Builder() {
}
public Builder(Buffer value) {
this.value = value;
}
@Override
public WWWAuthenticateHeader.Builder withValue(Buffer value) {
this.value = value;
return this;
}
public WWWAuthenticateHeader.Builder withRealm(Buffer realm) {
this.realm = realm;
return this;
}
public WWWAuthenticateHeader.Builder withNonce(Buffer nonce) {
this.nonce = nonce;
return this;
}
public WWWAuthenticateHeader.Builder withAlgorithm(Buffer algorithm) {
this.algorithm = algorithm;
return this;
}
public WWWAuthenticateHeader.Builder withQop(Buffer qop) {
this.qop = qop;
return this;
}
@Override
public WWWAuthenticateHeader build() throws SipParseException {
if (value == null &&
(this.realm == null && this.nonce == null)) {
throw new SipParseException("You must specify the [value] or [realm/nonce] of the WWWAuthenticate-Header");
}
if (this.value != null) {
return new WWWAuthenticateHeaderImpl(value);
} else {
return new WWWAuthenticateHeaderImpl(realm, nonce, algorithm, qop);
}
}
}
}
SipHeader里添加
default boolean isWWWAuthenticateHeader() {
//WWW-Authenticate
final Buffer m = getName();
try {
if (m.getReadableBytes() == 16) {
return (m.getByte(0) == 'W' || m.getByte(0) == 'w') &&
(m.getByte(1) == 'W' || m.getByte(1) == 'w') &&
(m.getByte(2) == 'W' || m.getByte(2) == 'w') &&
m.getByte(3) == '-' &&
(m.getByte(4) == 'A' || m.getByte(4) == 'a') &&
(m.getByte(5) == 'U' || m.getByte(5) == 'u') &&
(m.getByte(6) == 'T' || m.getByte(6) == 't') &&
(m.getByte(7) == 'H' || m.getByte(7) == 'h') &&
(m.getByte(8) == 'E' || m.getByte(8) == 'e') &&
(m.getByte(9) == 'N' || m.getByte(9) == 'n') &&
(m.getByte(10) == 'T' || m.getByte(10) == 't') &&
(m.getByte(11) == 'I' || m.getByte(11) == 'i') &&
(m.getByte(12) == 'C' || m.getByte(12) == 'c') &&
(m.getByte(13) == 'A' || m.getByte(13) == 'a') &&
(m.getByte(14) == 'T' || m.getByte(14) == 't') &&
(m.getByte(15) == 'E' || m.getByte(15) == 'e');
}
} catch (final IOException e) {
throw new SipParseException(0, UNABLE_TO_PARSE_OUT_THE_HEADER_NAME_DUE_TO_UNDERLYING_IO_EXCEPTION, e);
}
return false;
}
default WWWAuthenticateHeader toWWWAuthenticateHeader() {
throw new ClassCastException(CANNOT_CAST_HEADER_OF_TYPE + getClass().getName()
+ " to type " + WWWAuthenticateHeader.class.getName());
}
然后再來WWWAuthenticateHeaderImpl
package io.pkts.packet.sip.header.impl;
import io.pkts.buffer.Buffer;
import io.pkts.buffer.Buffers;
import io.pkts.packet.sip.SipParseException;
import io.pkts.packet.sip.header.WWWAuthenticateHeader;
import io.pkts.packet.sip.impl.SipParser;
import java.util.LinkedHashMap;
import java.util.Map;
public class WWWAuthenticateHeaderImpl extends SipHeaderImpl implements WWWAuthenticateHeader {
private Map<Buffer, Buffer> paramMap = new LinkedHashMap<>();
private Buffer realm;
private Buffer nonce;
private Buffer algorithm;
private Buffer qop;
/**
* @param value
*/
public WWWAuthenticateHeaderImpl(Buffer value) {
super(WWWAuthenticateHeader.NAME, value);
Buffer original = value.clone();
Buffer params = null;
if (original.hasReadableBytes()) {
params = original.slice("Digest ".length(), original.getUpperBoundary());
}
final byte[] VALUE_END_1 = Buffers.wrap("\", ").getArray();
final byte[] VALUE_END_2 = Buffers.wrap(", ").getArray();
//WWW-Authenticate: Digest realm="10.32.26.25",
// nonce="bee3366b-cf59-476e-bc5e-334e0d65b386",
// algorithm=MD5,
// qop="auth"
try {
// 思路:
// 1 遇到[=]號是key結束,遇到[,]或[", ]或[\r\n]是value結束
// 2 每次遇"="或”,”標識lastMarkIndex
int lastMarkIndex = params.getReaderIndex();
boolean inKey = true;
Buffer latestKey = Buffers.EMPTY_BUFFER, latestValue;
while (params.hasReadableBytes() && params.getReaderIndex() <= params.getUpperBoundary()) {
if (inKey && SipParser.isNext(params, SipParser.EQ)) {
//遇到[=]認為key結束
latestKey = params.slice(lastMarkIndex, params.getReaderIndex());
params.setReaderIndex(params.getReaderIndex() + 1);
if (SipParser.isNext(params, SipParser.DQUOT)) {
//跳過[="]等號后的第1個雙引號
params.setReaderIndex(params.getReaderIndex() + 1);
inKey = false;
}
lastMarkIndex = params.getReaderIndex();
} else if (params.getReadableBytes() == 1 ||
SipParser.isNext(params, VALUE_END_1) ||
SipParser.isNext(params, VALUE_END_2)) {
//遇到[", ]或[, ]視為value結束
if (params.getReadableBytes() == 1 && params.peekByte() != SipParser.DQUOT) {
latestValue = params.slice(lastMarkIndex, params.getReaderIndex() + 1);
} else {
latestValue = params.slice(lastMarkIndex, params.getReaderIndex());
}
paramMap.put(latestKey, latestValue);
if (params.getReadableBytes() == 1) {
params.setReaderIndex(params.getReaderIndex() + 1);
} else if (SipParser.isNext(params, VALUE_END_1)) {
params.setReaderIndex(params.getReaderIndex() + VALUE_END_1.length);
} else if (SipParser.isNext(params, VALUE_END_2)) {
params.setReaderIndex(params.getReaderIndex() + VALUE_END_2.length);
}
lastMarkIndex = params.getReaderIndex();
inKey = true;
} else {
params.setReaderIndex(params.getReaderIndex() + 1);
}
}
} catch (Exception e) {
throw new SipParseException(NAME + " parse error, " + e.getCause());
}
}
public WWWAuthenticateHeaderImpl(Buffer realm, Buffer nonce, Buffer algorithm, Buffer qop) {
super(WWWAuthenticateHeader.NAME, Buffers.EMPTY_BUFFER);
this.realm = realm;
this.nonce = nonce;
this.algorithm = algorithm;
this.qop = qop;
}
@Override
public Buffer getValue() {
Buffer value = super.getValue();
if (value != null && value != Buffers.EMPTY_BUFFER) {
return value;
}
StringBuilder sb = new StringBuilder("Digest realm=\"" + this.getRealm() + "\", nonce=\"" + this.getNonce() + "\"");
if (this.getAlgorithm() != null) {
sb.append(", algorithm=" + this.getAlgorithm());
}
if (this.getQop() != null) {
sb.append(", qop=\"" + this.getQop() + "\"");
}
value = Buffers.wrap(sb.toString());
return value;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(NAME.toString());
sb.append(": Digest realm=\"" + this.getRealm() + "\", nonce=\"" + this.getNonce() + "\"");
if (this.getAlgorithm() != null) {
sb.append(", algorithm=" + this.getAlgorithm());
}
if (this.getQop() != null) {
sb.append(", qop=\"" + this.getQop() + "\"");
}
return sb.toString();
}
@Override
public WWWAuthenticateHeader.Builder copy() {
return new WWWAuthenticateHeader.Builder(getValue());
}
@Override
public WWWAuthenticateHeader ensure() {
return this;
}
@Override
public WWWAuthenticateHeader clone() {
final Buffer value = getValue();
return new WWWAuthenticateHeaderImpl(value.clone());
}
@Override
public Buffer getRealm() {
if (realm != null) {
return realm;
}
realm = paramMap.get(Buffers.wrap("realm"));
return realm;
}
@Override
public Buffer getNonce() {
if (nonce != null) {
return nonce;
}
nonce = paramMap.get(Buffers.wrap("nonce"));
return nonce;
}
@Override
public Buffer getAlgorithm() {
if (algorithm != null) {
return algorithm;
}
algorithm = paramMap.get(Buffers.wrap("algorithm"));
return algorithm;
}
@Override
public Buffer getQop() {
if (qop != null) {
return qop;
}
qop = paramMap.get(Buffers.wrap("qop"));
return qop;
}
}
SipParser里新增注冊
static {
framers.put(CallIdHeader.NAME, header -> CallIdHeader.frame(header.getValue()));
framers.put(CallIdHeader.COMPACT_NAME, header -> CallIdHeader.frameCompact(header.getValue()));
...
framers.put(ViaHeader.NAME, header -> ViaHeader.frame(header.getValue()));
framers.put(ViaHeader.COMPACT_NAME, header -> ViaHeader.frame(header.getValue()));
//新增WWWAuthenticateHeader注冊
framers.put(WWWAuthenticateHeader.NAME, header -> WWWAuthenticateHeader.frame(header.getValue()));
}
frame方法里,也要新增判斷:
public static SipMessage frame(final Buffer buffer) throws IOException {
...
// Move along as long as we actually can consume an header and
...
SipHeader contactHeader = null;
SipHeader wwwAuthenticateHeader = null;
...
while (consumeCRLF(buffer) != 2 && (headerName = SipParser.nextHeaderName(buffer)) != null) {
final List<Buffer> values = readHeaderValues(headerName, buffer).values;
for (final Buffer value : values) {
header = new SipHeaderImpl(headerName, value);
// The headers that are most commonly used will be fully
// parsed just because no stack can really function without
// looking into these headers.
if (header.isContentLengthHeader()) {
final ContentLengthHeader l = header.ensure().toContentLengthHeader();
contentLength = l.getContentLength();
header = l;
}
...
} else if (recordRouteHeader == null && header.isRecordRouteHeader()) {
header = header.ensure();
recordRouteHeader = header;
} else if (wwwAuthenticateHeader == null && header.isWWWAuthenticateHeader()) {
header = header.ensure();
wwwAuthenticateHeader = header;
}
...
}
另外有1個小坑,readme里沒提到,類似
WWW-Authenticate: Digest realm="10.32.26.25", nonce="bee3366b-cf59-476e-bc5e-334e0d65b386", algorithm=MD5, qop="auth"
這種header解析時,還要修改SipParser里的isHeaderAllowingMultipleValues方法
private static boolean isHeaderAllowingMultipleValues(final Buffer headerName) {
final int size = headerName.getReadableBytes();
if (size == 7) {
return !isSubjectHeader(headerName);
} else if (size == 5) {
return !isAllowHeader(headerName);
} else if (size == 4) {
return !isDateHeader(headerName);
} else if (size == 1) {
return !isAllowEventsHeaderShort(headerName);
} else if (size == 12) {
return !isAllowEventsHeader(headerName);
} else if (size == 16) {
# 新增判斷,防止被解析成多行
return !isWWWAuthenticateHeader(headerName);
}
return true;
}
為了方便判斷Buffer接下來幾個位置是否為指定字符,SipParser里的isNext也做了擴展
public static boolean isNext(final Buffer buffer, final byte[] bytes) throws IOException {
boolean hasReadableBytes = buffer.hasReadableBytes();
if (!hasReadableBytes) {
return false;
}
int readableBytes = buffer.getReadableBytes();
int length = bytes.length;
if (readableBytes < length) {
return false;
}
boolean match = true;
for (int i = 0; i < length; i++) {
int readIndex = buffer.getReaderIndex() + i;
byte aByte = buffer.getByte(readIndex);
if (aByte != bytes[i]) {
match = false;
break;
}
}
return match;
}
還可以在ImmutableSipMessage類中添加以下方法,這樣用起來更順手
@Override
public WWWAuthenticateHeader getWWWAuthenticateHeader() throws SipParseException{
final SipHeader header = findHeader(WWWAuthenticateHeader.NAME.toString());
return header != null ? header.ensure().toWWWAuthenticateHeader() : null;
}
這些做完后,再來跑先前的測試

從上圖可以看到,realm\nonce\algorithm\qop這些屬性已經正確提取出來了,最后可以再測試下Builder
package io.pkts.packet.sip.header.impl;
import io.pkts.buffer.Buffer;
import io.pkts.buffer.Buffers;
import io.pkts.packet.sip.SipParseException;
import io.pkts.packet.sip.header.ViaHeader;
import io.pkts.packet.sip.header.WWWAuthenticateHeader;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.*;
public class WWWAuthenticateHeaderImplTest {
@Test
public void testBuild1() throws Exception {
final WWWAuthenticateHeader wwwAuthenticateHeader = new WWWAuthenticateHeader.Builder()
.withAlgorithm(Buffers.wrap("MD5"))
.withNonce(Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386"))
.withQop(Buffers.wrap("auth"))
.withRealm(Buffers.wrap("10.32.26.25"))
.build();
assertEquals(wwwAuthenticateHeader.getAlgorithm(), Buffers.wrap("MD5"));
assertEquals(wwwAuthenticateHeader.getNonce(), Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386"));
assertEquals(wwwAuthenticateHeader.getQop(), Buffers.wrap("auth"));
assertEquals(wwwAuthenticateHeader.getRealm(), Buffers.wrap("10.32.26.25"));
Buffer value = Buffers.wrap("Digest realm=\"10.32.26.25\", nonce=\"bee3366b-cf59-476e-bc5e-334e0d65b386\", algorithm=MD5, qop=\"auth\"");
assertTrue(wwwAuthenticateHeader.getValue().equalsIgnoreCase(value));
}
@Test
public void testBuild2() throws Exception {
final WWWAuthenticateHeader wwwAuthenticateHeader = new WWWAuthenticateHeader.Builder()
.withNonce(Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386"))
.withRealm(Buffers.wrap("10.32.26.25"))
.build();
assertEquals(wwwAuthenticateHeader.getAlgorithm(), null);
assertEquals(wwwAuthenticateHeader.getNonce(), Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386"));
assertEquals(wwwAuthenticateHeader.getQop(), null);
assertEquals(wwwAuthenticateHeader.getRealm(), Buffers.wrap("10.32.26.25"));
Buffer value = Buffers.wrap("Digest realm=\"10.32.26.25\", nonce=\"bee3366b-cf59-476e-bc5e-334e0d65b386\"");
assertTrue(wwwAuthenticateHeader.getValue().equalsIgnoreCase(value));
}
@Test
public void testFrame1() throws Exception {
Buffer value = Buffers.wrap("Digest realm=\"10.32.26.25\", nonce=\"bee3366b-cf59-476e-bc5e-334e0d65b386\", algorithm=MD5, qop=\"auth\"");
final WWWAuthenticateHeader wwwAuthenticateHeader = new WWWAuthenticateHeaderImpl(value);
assertEquals(wwwAuthenticateHeader.getAlgorithm(), Buffers.wrap("MD5"));
assertEquals(wwwAuthenticateHeader.getNonce(), Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386"));
assertEquals(wwwAuthenticateHeader.getQop(), Buffers.wrap("auth"));
assertEquals(wwwAuthenticateHeader.getRealm(), Buffers.wrap("10.32.26.25"));
}
@Test
public void testFrame2() throws Exception {
Buffer realm = Buffers.wrap("10.32.26.25");
Buffer nonce = Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386");
final WWWAuthenticateHeader wwwAuthenticateHeader = new WWWAuthenticateHeaderImpl(realm, nonce, null, null);
assertEquals(wwwAuthenticateHeader.getAlgorithm(), null);
assertEquals(wwwAuthenticateHeader.getNonce(), Buffers.wrap("bee3366b-cf59-476e-bc5e-334e0d65b386"));
assertEquals(wwwAuthenticateHeader.getQop(), null);
assertEquals(wwwAuthenticateHeader.getRealm(), Buffers.wrap("10.32.26.25"));
Buffer value = Buffers.wrap("Digest realm=\"10.32.26.25\", nonce=\"bee3366b-cf59-476e-bc5e-334e0d65b386\"");
assertTrue(wwwAuthenticateHeader.getValue().equalsIgnoreCase(value));
}
}
以上代碼,均已提交到 https://github.com/yjmyzz/pkts/tree/master/pkts-sip,供大家參考
