如何解析SIP报文

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,供大家参考

posted @ 2021-09-28 10:29  菩提树下的杨过  阅读(1671)  评论(0编辑  收藏  举报