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 17 package com.squareup.okhttp.internal.spdy; 18 19 import com.squareup.okhttp.internal.Util; 20 import java.io.Closeable; 21 import java.io.DataInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.UnsupportedEncodingException; 25 import java.net.ProtocolException; 26 import java.util.ArrayList; 27 import java.util.List; 28 import java.util.logging.Logger; 29 import java.util.zip.DataFormatException; 30 import java.util.zip.Inflater; 31 import java.util.zip.InflaterInputStream; 32 33 /** Read spdy/3 frames. */ 34 final class SpdyReader implements Closeable { 35 static final byte[] DICTIONARY; 36 static { 37 try { 38 DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea" 39 + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele" 40 + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000" 41 + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa" 42 + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000" 43 + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co" 44 + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000" 45 + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000" 46 + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000" 47 + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type" 48 + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe" 49 + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000" 50 + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since" 51 + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000" 52 + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati" 53 + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000" 54 + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000" 55 + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after" 56 + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai" 57 + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000" 58 + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via" 59 + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000" 60 + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000" 61 + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1" 62 + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo" 63 + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300" 64 + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori" 65 + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized" 66 + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un" 67 + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th" 68 + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml" 69 + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate," 70 + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name()); 71 } catch (UnsupportedEncodingException e) { 72 throw new AssertionError(); 73 } 74 } 75 76 private final DataInputStream in; 77 private final DataInputStream nameValueBlockIn; 78 private int compressedLimit; 79 80 SpdyReader(InputStream in) { 81 this.in = new DataInputStream(in); 82 this.nameValueBlockIn = newNameValueBlockStream(); 83 } 84 85 /** 86 * Send the next frame to {@code handler}. Returns true unless there are no 87 * more frames on the stream. 88 */ 89 public boolean nextFrame(Handler handler) throws IOException { 90 int w1; 91 try { 92 w1 = in.readInt(); 93 } catch (IOException e) { 94 return false; // This might be a normal socket close. 95 } 96 int w2 = in.readInt(); 97 98 boolean control = (w1 & 0x80000000) != 0; 99 int flags = (w2 & 0xff000000) >>> 24; 100 int length = (w2 & 0xffffff); 101 102 if (control) { 103 int version = (w1 & 0x7fff0000) >>> 16; 104 int type = (w1 & 0xffff); 105 106 if (version != 3) { 107 throw new ProtocolException("version != 3: " + version); 108 } 109 110 switch (type) { 111 case SpdyConnection.TYPE_SYN_STREAM: 112 readSynStream(handler, flags, length); 113 return true; 114 115 case SpdyConnection.TYPE_SYN_REPLY: 116 readSynReply(handler, flags, length); 117 return true; 118 119 case SpdyConnection.TYPE_RST_STREAM: 120 readRstStream(handler, flags, length); 121 return true; 122 123 case SpdyConnection.TYPE_SETTINGS: 124 readSettings(handler, flags, length); 125 return true; 126 127 case SpdyConnection.TYPE_NOOP: 128 if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length); 129 handler.noop(); 130 return true; 131 132 case SpdyConnection.TYPE_PING: 133 readPing(handler, flags, length); 134 return true; 135 136 case SpdyConnection.TYPE_GOAWAY: 137 readGoAway(handler, flags, length); 138 return true; 139 140 case SpdyConnection.TYPE_HEADERS: 141 readHeaders(handler, flags, length); 142 return true; 143 144 case SpdyConnection.TYPE_WINDOW_UPDATE: 145 readWindowUpdate(handler, flags, length); 146 return true; 147 148 case SpdyConnection.TYPE_CREDENTIAL: 149 Util.skipByReading(in, length); 150 throw new UnsupportedOperationException("TODO"); // TODO: implement 151 152 default: 153 throw new IOException("Unexpected frame"); 154 } 155 } else { 156 int streamId = w1 & 0x7fffffff; 157 handler.data(flags, streamId, in, length); 158 return true; 159 } 160 } 161 162 private void readSynStream(Handler handler, int flags, int length) throws IOException { 163 int w1 = in.readInt(); 164 int w2 = in.readInt(); 165 int s3 = in.readShort(); 166 int streamId = w1 & 0x7fffffff; 167 int associatedStreamId = w2 & 0x7fffffff; 168 int priority = (s3 & 0xe000) >>> 13; 169 int slot = s3 & 0xff; 170 List<String> nameValueBlock = readNameValueBlock(length - 10); 171 handler.synStream(flags, streamId, associatedStreamId, priority, slot, nameValueBlock); 172 } 173 174 private void readSynReply(Handler handler, int flags, int length) throws IOException { 175 int w1 = in.readInt(); 176 int streamId = w1 & 0x7fffffff; 177 List<String> nameValueBlock = readNameValueBlock(length - 4); 178 handler.synReply(flags, streamId, nameValueBlock); 179 } 180 181 private void readRstStream(Handler handler, int flags, int length) throws IOException { 182 if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length); 183 int streamId = in.readInt() & 0x7fffffff; 184 int statusCode = in.readInt(); 185 handler.rstStream(flags, streamId, statusCode); 186 } 187 188 private void readHeaders(Handler handler, int flags, int length) throws IOException { 189 int w1 = in.readInt(); 190 int streamId = w1 & 0x7fffffff; 191 List<String> nameValueBlock = readNameValueBlock(length - 4); 192 handler.headers(flags, streamId, nameValueBlock); 193 } 194 195 private void readWindowUpdate(Handler handler, int flags, int length) throws IOException { 196 if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length); 197 int w1 = in.readInt(); 198 int w2 = in.readInt(); 199 int streamId = w1 & 0x7fffffff; 200 int deltaWindowSize = w2 & 0x7fffffff; 201 handler.windowUpdate(flags, streamId, deltaWindowSize); 202 } 203 204 private DataInputStream newNameValueBlockStream() { 205 // Limit the inflater input stream to only those bytes in the Name/Value block. 206 final InputStream throttleStream = new InputStream() { 207 @Override public int read() throws IOException { 208 return Util.readSingleByte(this); 209 } 210 211 @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException { 212 byteCount = Math.min(byteCount, compressedLimit); 213 int consumed = in.read(buffer, offset, byteCount); 214 compressedLimit -= consumed; 215 return consumed; 216 } 217 218 @Override public void close() throws IOException { 219 in.close(); 220 } 221 }; 222 223 // Subclass inflater to install a dictionary when it's needed. 224 Inflater inflater = new Inflater() { 225 @Override public int inflate(byte[] buffer, int offset, int count) 226 throws DataFormatException { 227 int result = super.inflate(buffer, offset, count); 228 if (result == 0 && needsDictionary()) { 229 setDictionary(DICTIONARY); 230 result = super.inflate(buffer, offset, count); 231 } 232 return result; 233 } 234 }; 235 236 return new DataInputStream(new InflaterInputStream(throttleStream, inflater)); 237 } 238 239 private List<String> readNameValueBlock(int length) throws IOException { 240 this.compressedLimit += length; 241 try { 242 int numberOfPairs = nameValueBlockIn.readInt(); 243 if (numberOfPairs < 0) { 244 Logger.getLogger(getClass().getName()).warning("numberOfPairs < 0: " + numberOfPairs); 245 throw ioException("numberOfPairs < 0"); 246 } 247 List<String> entries = new ArrayList<String>(numberOfPairs * 2); 248 for (int i = 0; i < numberOfPairs; i++) { 249 String name = readString(); 250 String values = readString(); 251 if (name.length() == 0) throw ioException("name.length == 0"); 252 if (values.length() == 0) throw ioException("values.length == 0"); 253 entries.add(name); 254 entries.add(values); 255 } 256 257 if (compressedLimit != 0) { 258 Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit); 259 } 260 261 return entries; 262 } catch (DataFormatException e) { 263 throw new IOException(e.getMessage()); 264 } 265 } 266 267 private String readString() throws DataFormatException, IOException { 268 int length = nameValueBlockIn.readInt(); 269 byte[] bytes = new byte[length]; 270 Util.readFully(nameValueBlockIn, bytes); 271 return new String(bytes, 0, length, "UTF-8"); 272 } 273 274 private void readPing(Handler handler, int flags, int length) throws IOException { 275 if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); 276 int id = in.readInt(); 277 handler.ping(flags, id); 278 } 279 280 private void readGoAway(Handler handler, int flags, int length) throws IOException { 281 if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); 282 int lastGoodStreamId = in.readInt() & 0x7fffffff; 283 int statusCode = in.readInt(); 284 handler.goAway(flags, lastGoodStreamId, statusCode); 285 } 286 287 private void readSettings(Handler handler, int flags, int length) throws IOException { 288 int numberOfEntries = in.readInt(); 289 if (length != 4 + 8 * numberOfEntries) { 290 throw ioException("TYPE_SETTINGS length: %d != 4 + 8 * %d", length, numberOfEntries); 291 } 292 Settings settings = new Settings(); 293 for (int i = 0; i < numberOfEntries; i++) { 294 int w1 = in.readInt(); 295 int value = in.readInt(); 296 int idFlags = (w1 & 0xff000000) >>> 24; 297 int id = w1 & 0xffffff; 298 settings.set(id, idFlags, value); 299 } 300 handler.settings(flags, settings); 301 } 302 303 private static IOException ioException(String message, Object... args) throws IOException { 304 throw new IOException(String.format(message, args)); 305 } 306 307 @Override public void close() throws IOException { 308 Util.closeAll(in, nameValueBlockIn); 309 } 310 311 public interface Handler { 312 void data(int flags, int streamId, InputStream in, int length) throws IOException; 313 314 void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot, 315 List<String> nameValueBlock); 316 317 void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException; 318 void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException; 319 void rstStream(int flags, int streamId, int statusCode); 320 void settings(int flags, Settings settings); 321 void noop(); 322 void ping(int flags, int streamId); 323 void goAway(int flags, int lastGoodStreamId, int statusCode); 324 void windowUpdate(int flags, int streamId, int deltaWindowSize); 325 } 326 } 327