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.IOException; 22 import java.io.InputStream; 23 import java.io.OutputStream; 24 import java.net.ServerSocket; 25 import java.net.Socket; 26 import java.util.ArrayList; 27 import java.util.Iterator; 28 import java.util.List; 29 import java.util.concurrent.BlockingQueue; 30 import java.util.concurrent.ExecutorService; 31 import java.util.concurrent.Executors; 32 import java.util.concurrent.LinkedBlockingQueue; 33 import okio.BufferedSource; 34 import okio.ByteString; 35 import okio.OkBuffer; 36 import okio.Okio; 37 38 /** Replays prerecorded outgoing frames and records incoming frames. */ 39 public final class MockSpdyPeer implements Closeable { 40 private int frameCount = 0; 41 private boolean client = false; 42 private Variant variant = new Spdy3(); 43 private final OkBuffer bytesOut = new OkBuffer(); 44 private FrameWriter frameWriter = variant.newWriter(bytesOut, client); 45 private final List<OutFrame> outFrames = new ArrayList<OutFrame>(); 46 private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>(); 47 private int port; 48 private final ExecutorService executor = Executors.newSingleThreadExecutor( 49 Util.threadFactory("MockSpdyPeer", false)); 50 private ServerSocket serverSocket; 51 private Socket socket; 52 53 public void setVariantAndClient(Variant variant, boolean client) { 54 if (this.variant.getProtocol() == variant.getProtocol() && this.client == client) { 55 return; 56 } 57 this.client = client; 58 this.variant = variant; 59 this.frameWriter = variant.newWriter(bytesOut, client); 60 } 61 62 public void acceptFrame() { 63 frameCount++; 64 } 65 66 /** Count of frames sent or received. */ 67 public int frameCount() { 68 return frameCount; 69 } 70 71 public FrameWriter sendFrame() { 72 outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE)); 73 return frameWriter; 74 } 75 76 /** 77 * Sends a manually-constructed frame. This is useful to test frames that 78 * won't be generated naturally. 79 */ 80 public void sendFrame(byte[] frame) throws IOException { 81 outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE)); 82 bytesOut.write(frame); 83 } 84 85 /** 86 * Sends a frame, truncated to {@code truncateToLength} bytes. This is only 87 * useful for testing error handling as the truncated frame will be 88 * malformed. 89 */ 90 public FrameWriter sendTruncatedFrame(int truncateToLength) { 91 outFrames.add(new OutFrame(frameCount++, bytesOut.size(), truncateToLength)); 92 return frameWriter; 93 } 94 95 public InFrame takeFrame() throws InterruptedException { 96 return inFrames.take(); 97 } 98 99 public void play() throws IOException { 100 if (serverSocket != null) throw new IllegalStateException(); 101 serverSocket = new ServerSocket(0); 102 serverSocket.setReuseAddress(true); 103 port = serverSocket.getLocalPort(); 104 executor.execute(new Runnable() { 105 @Override public void run() { 106 try { 107 readAndWriteFrames(); 108 } catch (IOException e) { 109 Util.closeQuietly(MockSpdyPeer.this); 110 throw new RuntimeException(e); 111 } 112 } 113 }); 114 } 115 116 private void readAndWriteFrames() throws IOException { 117 if (socket != null) throw new IllegalStateException(); 118 socket = serverSocket.accept(); 119 OutputStream out = socket.getOutputStream(); 120 InputStream in = socket.getInputStream(); 121 FrameReader reader = variant.newReader(Okio.buffer(Okio.source(in)), client); 122 123 Iterator<OutFrame> outFramesIterator = outFrames.iterator(); 124 byte[] outBytes = bytesOut.readByteString(bytesOut.size()).toByteArray(); 125 OutFrame nextOutFrame = null; 126 127 for (int i = 0; i < frameCount; i++) { 128 if (nextOutFrame == null && outFramesIterator.hasNext()) { 129 nextOutFrame = outFramesIterator.next(); 130 } 131 132 if (nextOutFrame != null && nextOutFrame.sequence == i) { 133 long start = nextOutFrame.start; 134 int truncateToLength = nextOutFrame.truncateToLength; 135 long end; 136 if (outFramesIterator.hasNext()) { 137 nextOutFrame = outFramesIterator.next(); 138 end = nextOutFrame.start; 139 } else { 140 end = outBytes.length; 141 } 142 143 // write a frame 144 int length = (int) Math.min(end - start, truncateToLength); 145 out.write(outBytes, (int) start, length); 146 } else { 147 // read a frame 148 InFrame inFrame = new InFrame(i, reader); 149 reader.nextFrame(inFrame); 150 inFrames.add(inFrame); 151 } 152 } 153 Util.closeQuietly(socket); 154 } 155 156 public Socket openSocket() throws IOException { 157 return new Socket("localhost", port); 158 } 159 160 @Override public synchronized void close() throws IOException { 161 executor.shutdown(); 162 Socket socket = this.socket; 163 if (socket != null) { 164 Util.closeQuietly(socket); 165 this.socket = null; 166 } 167 ServerSocket serverSocket = this.serverSocket; 168 if (serverSocket != null) { 169 Util.closeQuietly(serverSocket); 170 this.serverSocket = null; 171 } 172 } 173 174 private static class OutFrame { 175 private final int sequence; 176 private final long start; 177 private final int truncateToLength; 178 179 private OutFrame(int sequence, long start, int truncateToLength) { 180 this.sequence = sequence; 181 this.start = start; 182 this.truncateToLength = truncateToLength; 183 } 184 } 185 186 public static class InFrame implements FrameReader.Handler { 187 public final int sequence; 188 public final FrameReader reader; 189 public int type = -1; 190 public boolean clearPrevious; 191 public boolean outFinished; 192 public boolean inFinished; 193 public int streamId; 194 public int associatedStreamId; 195 public int priority; 196 public ErrorCode errorCode; 197 public long windowSizeIncrement; 198 public List<Header> headerBlock; 199 public byte[] data; 200 public Settings settings; 201 public HeadersMode headersMode; 202 public boolean ack; 203 public int payload1; 204 public int payload2; 205 206 public InFrame(int sequence, FrameReader reader) { 207 this.sequence = sequence; 208 this.reader = reader; 209 } 210 211 @Override public void settings(boolean clearPrevious, Settings settings) { 212 if (this.type != -1) throw new IllegalStateException(); 213 this.type = Spdy3.TYPE_SETTINGS; 214 this.clearPrevious = clearPrevious; 215 this.settings = settings; 216 } 217 218 @Override public void ackSettings() { 219 if (this.type != -1) throw new IllegalStateException(); 220 this.type = Spdy3.TYPE_SETTINGS; 221 this.ack = true; 222 } 223 224 @Override public void headers(boolean outFinished, boolean inFinished, int streamId, 225 int associatedStreamId, int priority, List<Header> headerBlock, 226 HeadersMode headersMode) { 227 if (this.type != -1) throw new IllegalStateException(); 228 this.type = Spdy3.TYPE_HEADERS; 229 this.outFinished = outFinished; 230 this.inFinished = inFinished; 231 this.streamId = streamId; 232 this.associatedStreamId = associatedStreamId; 233 this.priority = priority; 234 this.headerBlock = headerBlock; 235 this.headersMode = headersMode; 236 } 237 238 @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) 239 throws IOException { 240 if (this.type != -1) throw new IllegalStateException(); 241 this.type = Spdy3.TYPE_DATA; 242 this.inFinished = inFinished; 243 this.streamId = streamId; 244 this.data = source.readByteString(length).toByteArray(); 245 } 246 247 @Override public void rstStream(int streamId, ErrorCode errorCode) { 248 if (this.type != -1) throw new IllegalStateException(); 249 this.type = Spdy3.TYPE_RST_STREAM; 250 this.streamId = streamId; 251 this.errorCode = errorCode; 252 } 253 254 @Override public void ping(boolean ack, int payload1, int payload2) { 255 if (this.type != -1) throw new IllegalStateException(); 256 this.type = Spdy3.TYPE_PING; 257 this.ack = ack; 258 this.payload1 = payload1; 259 this.payload2 = payload2; 260 } 261 262 @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { 263 if (this.type != -1) throw new IllegalStateException(); 264 this.type = Spdy3.TYPE_GOAWAY; 265 this.streamId = lastGoodStreamId; 266 this.errorCode = errorCode; 267 this.data = debugData.toByteArray(); 268 } 269 270 @Override public void windowUpdate(int streamId, long windowSizeIncrement) { 271 if (this.type != -1) throw new IllegalStateException(); 272 this.type = Spdy3.TYPE_WINDOW_UPDATE; 273 this.streamId = streamId; 274 this.windowSizeIncrement = windowSizeIncrement; 275 } 276 277 @Override public void priority(int streamId, int priority) { 278 throw new UnsupportedOperationException(); 279 } 280 281 @Override 282 public void pushPromise(int streamId, int associatedStreamId, List<Header> headerBlock) { 283 this.type = Http20Draft09.TYPE_PUSH_PROMISE; 284 this.streamId = streamId; 285 this.associatedStreamId = associatedStreamId; 286 this.headerBlock = headerBlock; 287 } 288 } 289 } 290