1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package libcore.net.spdy;
18
19import java.io.DataInputStream;
20import java.io.EOFException;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.UnsupportedEncodingException;
24import java.nio.charset.Charset;
25import java.util.ArrayList;
26import java.util.List;
27import java.util.logging.Logger;
28import java.util.zip.DataFormatException;
29import java.util.zip.Inflater;
30import java.util.zip.InflaterInputStream;
31import libcore.io.Streams;
32
33/**
34 * Read version 2 SPDY frames.
35 */
36final class SpdyReader {
37    public static final Charset UTF_8 = Charset.forName("UTF-8");
38    private static final String DICTIONARY_STRING = ""
39            + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-"
40            + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi"
41            + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser"
42            + "-agent10010120020120220320420520630030130230330430530630740040140240340440"
43            + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta"
44            + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic"
45            + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran"
46            + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati"
47            + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo"
48            + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe"
49            + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic"
50            + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1"
51            + ".1statusversionurl\0";
52    public static final byte[] DICTIONARY;
53    static {
54        try {
55            DICTIONARY = DICTIONARY_STRING.getBytes("UTF-8");
56        } catch (UnsupportedEncodingException e) {
57            throw new AssertionError(e);
58        }
59    }
60
61    public final DataInputStream in;
62    public int flags;
63    public int length;
64    public int streamId;
65    public int associatedStreamId;
66    public int version;
67    public int type;
68    public int priority;
69    public int statusCode;
70
71    public List<String> nameValueBlock;
72    private final DataInputStream nameValueBlockIn;
73    private int compressedLimit;
74
75    SpdyReader(InputStream in) {
76        this.in = new DataInputStream(in);
77        this.nameValueBlockIn = newNameValueBlockStream();
78    }
79
80    /**
81     * Advance to the next frame in the source data. If the frame is of
82     * TYPE_DATA, it's the caller's responsibility to read length bytes from
83     * the input stream before the next call to nextFrame().
84     */
85    public int nextFrame() throws IOException {
86        int w1;
87        try {
88            w1 = in.readInt();
89        } catch (EOFException e) {
90            return SpdyConnection.TYPE_EOF;
91        }
92        int w2 = in.readInt();
93
94        boolean control = (w1 & 0x80000000) != 0;
95        flags = (w2 & 0xff000000) >>> 24;
96        length = (w2 & 0xffffff);
97
98        if (control) {
99            version = (w1 & 0x7fff0000) >>> 16;
100            type = (w1 & 0xffff);
101
102            switch (type) {
103            case SpdyConnection.TYPE_SYN_STREAM:
104                readSynStream();
105                return SpdyConnection.TYPE_SYN_STREAM;
106
107            case SpdyConnection.TYPE_SYN_REPLY:
108                readSynReply();
109                return SpdyConnection.TYPE_SYN_REPLY;
110
111            case SpdyConnection.TYPE_RST_STREAM:
112                readSynReset();
113                return SpdyConnection.TYPE_RST_STREAM;
114
115            default:
116                readControlFrame();
117                return type;
118            }
119        } else {
120            streamId = w1 & 0x7fffffff;
121            return SpdyConnection.TYPE_DATA;
122        }
123    }
124
125    private void readSynStream() throws IOException {
126        int w1 = in.readInt();
127        int w2 = in.readInt();
128        int s3 = in.readShort();
129        streamId = w1 & 0x7fffffff;
130        associatedStreamId = w2 & 0x7fffffff;
131        priority = s3 & 0xc000 >> 14;
132        // int unused = s3 & 0x3fff;
133        nameValueBlock = readNameValueBlock(length - 10);
134    }
135
136    private void readSynReply() throws IOException {
137        int w1 = in.readInt();
138        in.readShort(); // unused
139        streamId = w1 & 0x7fffffff;
140        nameValueBlock = readNameValueBlock(length - 6);
141    }
142
143    private void readSynReset() throws IOException {
144        streamId = in.readInt() & 0x7fffffff;
145        statusCode = in.readInt();
146    }
147
148    private void readControlFrame() throws IOException {
149        Streams.skipByReading(in, length);
150    }
151
152    private DataInputStream newNameValueBlockStream() {
153        // Limit the inflater input stream to only those bytes in the Name/Value block.
154        final InputStream throttleStream = new InputStream() {
155            @Override public int read() throws IOException {
156                return Streams.readSingleByte(this);
157            }
158
159            @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
160                byteCount = Math.min(byteCount, compressedLimit);
161                int consumed = in.read(buffer, offset, byteCount);
162                compressedLimit -= consumed;
163                return consumed;
164            }
165
166            @Override public void close() throws IOException {
167                in.close();
168            }
169        };
170
171        // Subclass inflater to install a dictionary when it's needed.
172        Inflater inflater = new Inflater() {
173            @Override
174            public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
175                int result = super.inflate(buffer, offset, count);
176                if (result == 0 && needsDictionary()) {
177                    setDictionary(DICTIONARY);
178                    result = super.inflate(buffer, offset, count);
179                }
180                return result;
181            }
182        };
183
184        return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
185    }
186
187    private List<String> readNameValueBlock(int length) throws IOException {
188        this.compressedLimit += length;
189        try {
190            List<String> entries = new ArrayList<String>();
191
192            int numberOfPairs = nameValueBlockIn.readShort();
193            for (int i = 0; i < numberOfPairs; i++) {
194                String name = readString();
195                String values = readString();
196                if (name.length() == 0 || values.length() == 0) {
197                    throw new IOException(); // TODO: PROTOCOL ERROR
198                }
199                entries.add(name);
200                entries.add(values);
201            }
202
203            if (compressedLimit != 0) {
204                Logger.getLogger(getClass().getName())
205                        .warning("compressedLimit > 0" + compressedLimit);
206            }
207
208            return entries;
209        } catch (DataFormatException e) {
210            throw new IOException(e);
211        }
212    }
213
214    private String readString() throws DataFormatException, IOException {
215        int length = nameValueBlockIn.readShort();
216        byte[] bytes = new byte[length];
217        Streams.readFully(nameValueBlockIn, bytes);
218        return new String(bytes, 0, length, "UTF-8");
219    }
220}
221