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.framed; 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 java.util.logging.Logger; 34 import okio.Buffer; 35 import okio.BufferedSource; 36 import okio.ByteString; 37 import okio.Okio; 38 39 /** Replays prerecorded outgoing frames and records incoming frames. */ 40 public final class MockSpdyPeer implements Closeable { 41 private static final Logger logger = Logger.getLogger(MockSpdyPeer.class.getName()); 42 43 private int frameCount = 0; 44 private boolean client = false; 45 private Variant variant = new Spdy3(); 46 private final Buffer bytesOut = new Buffer(); 47 private FrameWriter frameWriter = variant.newWriter(bytesOut, client); 48 private final List<OutFrame> outFrames = new ArrayList<>(); 49 private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<>(); 50 private int port; 51 private final ExecutorService executor = Executors.newSingleThreadExecutor( 52 Util.threadFactory("MockSpdyPeer", false)); 53 private ServerSocket serverSocket; 54 private Socket socket; 55 56 public void setVariantAndClient(Variant variant, boolean client) { 57 if (this.variant.getProtocol() == variant.getProtocol() && this.client == client) { 58 return; 59 } 60 this.client = client; 61 this.variant = variant; 62 this.frameWriter = variant.newWriter(bytesOut, client); 63 } 64 65 public void acceptFrame() { 66 frameCount++; 67 } 68 69 /** Maximum length of an outbound data frame. */ 70 public int maxOutboundDataLength() { 71 return frameWriter.maxDataLength(); 72 } 73 74 /** Count of frames sent or received. */ 75 public int frameCount() { 76 return frameCount; 77 } 78 79 public FrameWriter sendFrame() { 80 outFrames.add(new OutFrame(frameCount++, bytesOut.size(), false)); 81 return frameWriter; 82 } 83 84 /** 85 * Sends a manually-constructed frame. This is useful to test frames that 86 * won't be generated naturally. 87 */ 88 public void sendFrame(byte[] frame) throws IOException { 89 outFrames.add(new OutFrame(frameCount++, bytesOut.size(), false)); 90 bytesOut.write(frame); 91 } 92 93 /** 94 * Shortens the last frame from its original length to {@code length}. This 95 * will cause the peer to close the socket as soon as this frame has been 96 * written; otherwise the peer stays open until explicitly closed. 97 */ 98 public FrameWriter truncateLastFrame(int length) { 99 OutFrame lastFrame = outFrames.remove(outFrames.size() - 1); 100 if (length >= bytesOut.size() - lastFrame.start) throw new IllegalArgumentException(); 101 102 // Move everything from bytesOut into a new buffer. 103 Buffer fullBuffer = new Buffer(); 104 bytesOut.read(fullBuffer, bytesOut.size()); 105 106 // Copy back all but what we're truncating. 107 fullBuffer.read(bytesOut, lastFrame.start + length); 108 109 outFrames.add(new OutFrame(lastFrame.sequence, lastFrame.start, true)); 110 return frameWriter; 111 } 112 113 public InFrame takeFrame() throws InterruptedException { 114 return inFrames.take(); 115 } 116 117 public void play() throws IOException { 118 if (serverSocket != null) throw new IllegalStateException(); 119 serverSocket = new ServerSocket(0); 120 serverSocket.setReuseAddress(true); 121 port = serverSocket.getLocalPort(); 122 executor.execute(new Runnable() { 123 @Override public void run() { 124 try { 125 readAndWriteFrames(); 126 } catch (IOException e) { 127 Util.closeQuietly(MockSpdyPeer.this); 128 logger.info(MockSpdyPeer.this + " done: " + e.getMessage()); 129 } 130 } 131 }); 132 } 133 134 private void readAndWriteFrames() throws IOException { 135 if (socket != null) throw new IllegalStateException(); 136 socket = serverSocket.accept(); 137 138 // Bail out now if this instance was closed while waiting for the socket to accept. 139 synchronized (this) { 140 if (executor.isShutdown()) { 141 socket.close(); 142 return; 143 } 144 } 145 146 OutputStream out = socket.getOutputStream(); 147 InputStream in = socket.getInputStream(); 148 FrameReader reader = variant.newReader(Okio.buffer(Okio.source(in)), client); 149 150 Iterator<OutFrame> outFramesIterator = outFrames.iterator(); 151 byte[] outBytes = bytesOut.readByteArray(); 152 OutFrame nextOutFrame = null; 153 154 for (int i = 0; i < frameCount; i++) { 155 if (nextOutFrame == null && outFramesIterator.hasNext()) { 156 nextOutFrame = outFramesIterator.next(); 157 } 158 159 if (nextOutFrame != null && nextOutFrame.sequence == i) { 160 long start = nextOutFrame.start; 161 boolean truncated; 162 long end; 163 if (outFramesIterator.hasNext()) { 164 nextOutFrame = outFramesIterator.next(); 165 end = nextOutFrame.start; 166 truncated = false; 167 } else { 168 end = outBytes.length; 169 truncated = nextOutFrame.truncated; 170 } 171 172 // Write a frame. 173 int length = (int) (end - start); 174 out.write(outBytes, (int) start, length); 175 176 // If the last frame was truncated, immediately close the connection. 177 if (truncated) { 178 socket.close(); 179 } 180 } else { 181 // read a frame 182 InFrame inFrame = new InFrame(i, reader); 183 reader.nextFrame(inFrame); 184 inFrames.add(inFrame); 185 } 186 } 187 } 188 189 public Socket openSocket() throws IOException { 190 return new Socket("localhost", port); 191 } 192 193 @Override public synchronized void close() throws IOException { 194 executor.shutdown(); 195 Util.closeQuietly(socket); 196 Util.closeQuietly(serverSocket); 197 } 198 199 @Override public String toString() { 200 return "MockSpdyPeer[" + port + "]"; 201 } 202 203 private static class OutFrame { 204 private final int sequence; 205 private final long start; 206 private final boolean truncated; 207 208 private OutFrame(int sequence, long start, boolean truncated) { 209 this.sequence = sequence; 210 this.start = start; 211 this.truncated = truncated; 212 } 213 } 214 215 public static class InFrame implements FrameReader.Handler { 216 public final int sequence; 217 public final FrameReader reader; 218 public int type = -1; 219 public boolean clearPrevious; 220 public boolean outFinished; 221 public boolean inFinished; 222 public int streamId; 223 public int associatedStreamId; 224 public ErrorCode errorCode; 225 public long windowSizeIncrement; 226 public List<Header> headerBlock; 227 public byte[] data; 228 public Settings settings; 229 public HeadersMode headersMode; 230 public boolean ack; 231 public int payload1; 232 public int payload2; 233 234 public InFrame(int sequence, FrameReader reader) { 235 this.sequence = sequence; 236 this.reader = reader; 237 } 238 239 @Override public void settings(boolean clearPrevious, Settings settings) { 240 if (this.type != -1) throw new IllegalStateException(); 241 this.type = Spdy3.TYPE_SETTINGS; 242 this.clearPrevious = clearPrevious; 243 this.settings = settings; 244 } 245 246 @Override public void ackSettings() { 247 if (this.type != -1) throw new IllegalStateException(); 248 this.type = Spdy3.TYPE_SETTINGS; 249 this.ack = true; 250 } 251 252 @Override public void headers(boolean outFinished, boolean inFinished, int streamId, 253 int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) { 254 if (this.type != -1) throw new IllegalStateException(); 255 this.type = Spdy3.TYPE_HEADERS; 256 this.outFinished = outFinished; 257 this.inFinished = inFinished; 258 this.streamId = streamId; 259 this.associatedStreamId = associatedStreamId; 260 this.headerBlock = headerBlock; 261 this.headersMode = headersMode; 262 } 263 264 @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) 265 throws IOException { 266 if (this.type != -1) throw new IllegalStateException(); 267 this.type = Spdy3.TYPE_DATA; 268 this.inFinished = inFinished; 269 this.streamId = streamId; 270 this.data = source.readByteString(length).toByteArray(); 271 } 272 273 @Override public void rstStream(int streamId, ErrorCode errorCode) { 274 if (this.type != -1) throw new IllegalStateException(); 275 this.type = Spdy3.TYPE_RST_STREAM; 276 this.streamId = streamId; 277 this.errorCode = errorCode; 278 } 279 280 @Override public void ping(boolean ack, int payload1, int payload2) { 281 if (this.type != -1) throw new IllegalStateException(); 282 this.type = Spdy3.TYPE_PING; 283 this.ack = ack; 284 this.payload1 = payload1; 285 this.payload2 = payload2; 286 } 287 288 @Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { 289 if (this.type != -1) throw new IllegalStateException(); 290 this.type = Spdy3.TYPE_GOAWAY; 291 this.streamId = lastGoodStreamId; 292 this.errorCode = errorCode; 293 this.data = debugData.toByteArray(); 294 } 295 296 @Override public void windowUpdate(int streamId, long windowSizeIncrement) { 297 if (this.type != -1) throw new IllegalStateException(); 298 this.type = Spdy3.TYPE_WINDOW_UPDATE; 299 this.streamId = streamId; 300 this.windowSizeIncrement = windowSizeIncrement; 301 } 302 303 @Override public void priority(int streamId, int streamDependency, int weight, 304 boolean exclusive) { 305 throw new UnsupportedOperationException(); 306 } 307 308 @Override 309 public void pushPromise(int streamId, int associatedStreamId, List<Header> headerBlock) { 310 this.type = Http2.TYPE_PUSH_PROMISE; 311 this.streamId = streamId; 312 this.associatedStreamId = associatedStreamId; 313 this.headerBlock = headerBlock; 314 } 315 316 @Override public void alternateService(int streamId, String origin, ByteString protocol, 317 String host, int port, long maxAge) { 318 throw new UnsupportedOperationException(); 319 } 320 } 321 } 322