1 /*
   2  * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.Closeable;
  25 import java.io.IOException;
  26 import java.io.InputStream;
  27 import java.io.OutputStream;
  28 import javax.net.ServerSocketFactory;
  29 import javax.net.ssl.SSLServerSocket;
  30 import java.net.ServerSocket;
  31 import java.net.Socket;
  32 import java.nio.charset.StandardCharsets;
  33 import java.util.Collections;
  34 import java.util.LinkedList;
  35 import java.util.List;
  36 import java.util.Iterator;
  37 import java.util.concurrent.ArrayBlockingQueue;
  38 import java.util.concurrent.TimeUnit;
  39 import java.util.concurrent.atomic.AtomicInteger;
  40 
  41 /**
  42  * A cut-down Http/1 Server for testing various error situations
  43  *
  44  * use interrupt() to halt
  45  */
  46 public class MockServer extends Thread implements Closeable {
  47 
  48     ServerSocket ss;
  49     private final List<Connection> sockets;
  50     private final List<Connection> removals;
  51     private final List<Connection> additions;
  52     AtomicInteger counter = new AtomicInteger(0);
  53 
  54     // waits up to 20 seconds for something to happen
  55     // dont use this unless certain activity coming.
  56     public Connection activity() {
  57         for (int i = 0; i < 80 * 100; i++) {
  58             doRemovalsAndAdditions();
  59             for (Connection c : sockets) {
  60                 if (c.poll()) {
  61                     return c;
  62                 }
  63             }
  64             try {
  65                 Thread.sleep(250);
  66             } catch (InterruptedException e) {
  67                 e.printStackTrace();
  68             }
  69         }
  70         return null;
  71     }
  72 
  73     private void doRemovalsAndAdditions() {
  74         synchronized (removals) {
  75             Iterator<Connection> i = removals.iterator();
  76             while (i.hasNext()) {
  77                 Connection c = i.next();
  78                 System.out.println("socket removed: " + c);
  79                 sockets.remove(c);
  80             }
  81             removals.clear();
  82         }
  83 
  84         synchronized (additions) {
  85             Iterator<Connection> i = additions.iterator();
  86             while (i.hasNext()) {
  87                 Connection c = i.next();
  88                 System.out.println("socket added: " + c);
  89                 sockets.add(c);
  90             }
  91             additions.clear();
  92         }
  93     }
  94 
  95     // clears all current connections on Server.
  96     public void reset() {
  97         for (Connection c : sockets) {
  98             c.close();
  99         }
 100     }
 101 
 102     /**
 103      * Reads data into an ArrayBlockingQueue<String> where each String
 104      * is a line of input, that was terminated by CRLF (not included)
 105      */
 106     class Connection extends Thread {
 107         Connection(Socket s) throws IOException {
 108             this.socket = s;
 109             id = counter.incrementAndGet();
 110             is = s.getInputStream();
 111             os = s.getOutputStream();
 112             incoming = new ArrayBlockingQueue<>(100);
 113             setName("Server-Connection");
 114             setDaemon(true);
 115         }
 116         final Socket socket;
 117         final int id;
 118         final InputStream is;
 119         final OutputStream os;
 120         final ArrayBlockingQueue<String> incoming;
 121 
 122         final static String CRLF = "\r\n";
 123 
 124         // sentinel indicating connection closed
 125         final static String CLOSED = "C.L.O.S.E.D";
 126         volatile boolean closed = false;
 127         volatile boolean released = false;
 128 
 129         @Override
 130         public void run() {
 131             byte[] buf = new byte[256];
 132             String s = "";
 133             try {
 134                 while (true) {
 135                     int n = is.read(buf);
 136                     if (n == -1) {
 137                         cleanup();
 138                         return;
 139                     }
 140                     String s0 = new String(buf, 0, n, StandardCharsets.ISO_8859_1);
 141                     s = s + s0;
 142                     int i;
 143                     while ((i=s.indexOf(CRLF)) != -1) {
 144                         String s1 = s.substring(0, i+2);
 145                         System.out.println("Server got: " + s1.substring(0,i));
 146                         incoming.put(s1);
 147                         if (i+2 == s.length()) {
 148                             s = "";
 149                             break;
 150                         }
 151                         s = s.substring(i+2);
 152                     }
 153                 }
 154             } catch (IOException |InterruptedException e1) {
 155                 cleanup();
 156             } catch (Throwable t) {
 157                 System.out.println("X: " + t);
 158                 cleanup();
 159             }
 160         }
 161 
 162         @Override
 163         public String toString() {
 164             return "Server.Connection: " + socket.toString();
 165         }
 166 
 167         public void sendHttpResponse(int code, String body, String... headers)
 168             throws IOException
 169         {
 170             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
 171             for (int i=0; i<headers.length; i+=2) {
 172                 r1 += headers[i] + ": " + headers[i+1] + CRLF;
 173             }
 174             int clen = body == null ? 0 : body.length();
 175             r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
 176             r1 += CRLF;
 177             if (body != null) {
 178                 r1 += body;
 179             }
 180             send(r1);
 181         }
 182 
 183         // content-length is 10 bytes too many
 184         public void sendIncompleteHttpResponseBody(int code) throws IOException {
 185             String body = "Hello World Helloworld Goodbye World";
 186             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
 187             int clen = body.length() + 10;
 188             r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
 189             r1 += CRLF;
 190             if (body != null) {
 191                 r1 += body;
 192             }
 193             send(r1);
 194         }
 195 
 196         public void sendIncompleteHttpResponseHeaders(int code)
 197             throws IOException
 198         {
 199             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
 200             send(r1);
 201         }
 202 
 203         public void send(String r) throws IOException {
 204             os.write(r.getBytes(StandardCharsets.ISO_8859_1));
 205         }
 206 
 207         public synchronized void close() {
 208             cleanup();
 209             closed = true;
 210             incoming.clear();
 211         }
 212 
 213         public String nextInput(long timeout, TimeUnit unit) {
 214             String result = "";
 215             while (poll()) {
 216                 try {
 217                     String s = incoming.poll(timeout, unit);
 218                     if (s == null && closed) {
 219                         return CLOSED;
 220                     } else {
 221                         result += s;
 222                     }
 223                 } catch (InterruptedException e) {
 224                     return null;
 225                 }
 226             }
 227             return result;
 228         }
 229 
 230         public String nextInput() {
 231             return nextInput(0, TimeUnit.SECONDS);
 232         }
 233 
 234         public boolean poll() {
 235             return incoming.peek() != null;
 236         }
 237 
 238         private void cleanup() {
 239             if (released) return;
 240             synchronized(this) {
 241                 if (released) return;
 242                 released = true;
 243             }
 244             try {
 245                 socket.close();
 246             } catch (IOException e) {}
 247             synchronized (removals) {
 248                 removals.add(this);
 249             }
 250         }
 251     }
 252 
 253     MockServer(int port, ServerSocketFactory factory) throws IOException {
 254         ss = factory.createServerSocket(port);
 255         sockets = Collections.synchronizedList(new LinkedList<>());
 256         removals = new LinkedList<>();
 257         additions = new LinkedList<>();
 258         setName("Test-Server");
 259         setDaemon(true);
 260     }
 261 
 262     MockServer(int port) throws IOException {
 263         this(port, ServerSocketFactory.getDefault());
 264     }
 265 
 266     MockServer() throws IOException {
 267         this(0);
 268     }
 269 
 270     int port() {
 271         return ss.getLocalPort();
 272     }
 273 
 274     public String getURL() {
 275         if (ss instanceof SSLServerSocket) {
 276             return "https://127.0.0.1:" + port() + "/foo/";
 277         } else {
 278             return "http://127.0.0.1:" + port() + "/foo/";
 279         }
 280     }
 281 
 282     private volatile boolean closed;
 283 
 284     @Override
 285     public void close() {
 286         closed = true;
 287         try {
 288             ss.close();
 289         } catch (IOException e) {
 290             e.printStackTrace();
 291         }
 292         for (Connection c : sockets) {
 293             c.close();
 294         }
 295     }
 296 
 297     @Override
 298     public void run() {
 299         while (!closed) {
 300             try {
 301                 System.out.println("Server waiting for connection");
 302                 Socket s = ss.accept();
 303                 Connection c = new Connection(s);
 304                 c.start();
 305                 System.out.println("Server got new connection: " + c);
 306                 synchronized (additions) {
 307                     additions.add(c);
 308                 }
 309             } catch (IOException e) {
 310                 if (closed)
 311                     return;
 312                 e.printStackTrace();
 313             }
 314         }
 315     }
 316 
 317 }