Home | History | Annotate | Download | only in framed
      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