1 /*
   2  * Copyright (c) 2015, 2016, 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 java.net.ServerSocket;
  29 import java.net.Socket;
  30 import java.nio.charset.StandardCharsets;
  31 import java.util.Collections;
  32 import java.util.LinkedList;
  33 import java.util.List;
  34 import java.util.Iterator;
  35 import java.util.concurrent.ArrayBlockingQueue;
  36 import java.util.concurrent.TimeUnit;
  37 import java.util.concurrent.atomic.AtomicInteger;
  38 
  39 /**
  40  * A cut-down Http/1 Server for testing various error situations
  41  *
  42  * use interrupt() to halt
  43  */
  44 public class Server extends Thread implements Closeable {
  45 
  46     ServerSocket ss;
  47     private final List<Connection> sockets;
  48     private final List<Connection> removals;
  49     private final List<Connection> additions;
  50     AtomicInteger counter = new AtomicInteger(0);
  51 
  52     // waits up to 20 seconds for something to happen
  53     // dont use this unless certain activity coming.
  54     public Connection activity() {
  55         for (int i = 0; i < 80 * 100; i++) {
  56             doRemovalsAndAdditions();
  57             for (Connection c : sockets) {
  58                 if (c.poll()) {
  59                     return c;
  60                 }
  61             }
  62             try {
  63                 Thread.sleep(250);
  64             } catch (InterruptedException e) {
  65                 e.printStackTrace();
  66             }
  67         }
  68         return null;
  69     }
  70 
  71     private void doRemovalsAndAdditions() {
  72         if (removals.isEmpty() && additions.isEmpty())
  73             return;
  74         Iterator<Connection> i = removals.iterator();
  75         while (i.hasNext())
  76             sockets.remove(i.next());
  77         removals.clear();
  78 
  79         i = additions.iterator();
  80         while (i.hasNext())
  81             sockets.add(i.next());
  82         additions.clear();
  83     }
  84 
  85     // clears all current connections on Server.
  86     public void reset() {
  87         for (Connection c : sockets) {
  88             c.close();
  89         }
  90     }
  91 
  92     /**
  93      * Reads data into an ArrayBlockingQueue<String> where each String
  94      * is a line of input, that was terminated by CRLF (not included)
  95      */
  96     class Connection extends Thread {
  97         Connection(Socket s) throws IOException {
  98             this.socket = s;
  99             id = counter.incrementAndGet();
 100             is = s.getInputStream();
 101             os = s.getOutputStream();
 102             incoming = new ArrayBlockingQueue<>(100);
 103             setName("Server-Connection");
 104             setDaemon(true);
 105         }
 106         final Socket socket;
 107         final int id;
 108         final InputStream is;
 109         final OutputStream os;
 110         final ArrayBlockingQueue<String> incoming;
 111 
 112         final static String CRLF = "\r\n";
 113 
 114         // sentinel indicating connection closed
 115         final static String CLOSED = "C.L.O.S.E.D";
 116         volatile boolean closed = false;
 117 
 118         @Override
 119         public void run() {
 120             byte[] buf = new byte[256];
 121             String s = "";
 122             try {
 123                 while (true) {
 124                     int n = is.read(buf);
 125                     if (n == -1) {
 126                         cleanup();
 127                         return;
 128                     }
 129                     String s0 = new String(buf, 0, n, StandardCharsets.ISO_8859_1);
 130                     s = s + s0;
 131                     int i;
 132                     while ((i=s.indexOf(CRLF)) != -1) {
 133                         String s1 = s.substring(0, i+2);
 134                         incoming.put(s1);
 135                         if (i+2 == s.length()) {
 136                             s = "";
 137                             break;
 138                         }
 139                         s = s.substring(i+2);
 140                     }
 141                 }
 142             } catch (IOException |InterruptedException e1) {
 143                 cleanup();
 144             } catch (Throwable t) {
 145                 System.out.println("X: " + t);
 146                 cleanup();
 147             }
 148         }
 149 
 150         @Override
 151         public String toString() {
 152             return "Server.Connection: " + socket.toString();
 153         }
 154 
 155         public void sendHttpResponse(int code, String body, String... headers)
 156             throws IOException
 157         {
 158             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
 159             for (int i=0; i<headers.length; i+=2) {
 160                 r1 += headers[i] + ": " + headers[i+1] + CRLF;
 161             }
 162             int clen = body == null ? 0 : body.length();
 163             r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
 164             r1 += CRLF;
 165             if (body != null) {
 166                 r1 += body;
 167             }
 168             send(r1);
 169         }
 170 
 171         // content-length is 10 bytes too many
 172         public void sendIncompleteHttpResponseBody(int code) throws IOException {
 173             String body = "Hello World Helloworld Goodbye World";
 174             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
 175             int clen = body.length() + 10;
 176             r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
 177             r1 += CRLF;
 178             if (body != null) {
 179                 r1 += body;
 180             }
 181             send(r1);
 182         }
 183 
 184         public void sendIncompleteHttpResponseHeaders(int code)
 185             throws IOException
 186         {
 187             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
 188             send(r1);
 189         }
 190 
 191         public void send(String r) throws IOException {
 192             os.write(r.getBytes(StandardCharsets.ISO_8859_1));
 193         }
 194 
 195         public synchronized void close() {
 196             cleanup();
 197             closed = true;
 198             incoming.clear();
 199         }
 200 
 201         public String nextInput(long timeout, TimeUnit unit) {
 202             String result = "";
 203             while (poll()) {
 204                 try {
 205                     String s = incoming.poll(timeout, unit);
 206                     if (s == null && closed) {
 207                         return CLOSED;
 208                     } else {
 209                         result += s;
 210                     }
 211                 } catch (InterruptedException e) {
 212                     return null;
 213                 }
 214             }
 215             return result;
 216         }
 217 
 218         public String nextInput() {
 219             return nextInput(0, TimeUnit.SECONDS);
 220         }
 221 
 222         public boolean poll() {
 223             return incoming.peek() != null;
 224         }
 225 
 226         private void cleanup() {
 227             try {
 228                 socket.close();
 229             } catch (IOException e) {}
 230             removals.add(this);
 231         }
 232     }
 233 
 234     Server(int port) throws IOException {
 235         ss = new ServerSocket(port);
 236         sockets = Collections.synchronizedList(new LinkedList<>());
 237         removals = Collections.synchronizedList(new LinkedList<>());
 238         additions = Collections.synchronizedList(new LinkedList<>());
 239         setName("Test-Server");
 240         setDaemon(true);
 241     }
 242 
 243     Server() throws IOException {
 244         this(0);
 245     }
 246 
 247     int port() {
 248         return ss.getLocalPort();
 249     }
 250 
 251     public String getURL() {
 252         return "http://127.0.0.1:" + port() + "/foo/";
 253     }
 254 
 255     private volatile boolean closed;
 256 
 257     @Override
 258     public void close() {
 259         closed = true;
 260         try {
 261             ss.close();
 262         } catch (IOException e) {
 263             e.printStackTrace();
 264         }
 265         for (Connection c : sockets) {
 266             c.close();
 267         }
 268     }
 269 
 270     @Override
 271     public void run() {
 272         while (!closed) {
 273             try {
 274                 Socket s = ss.accept();
 275                 Connection c = new Connection(s);
 276                 c.start();
 277                 additions.add(c);
 278             } catch (IOException e) {
 279                 if (closed)
 280                     return;
 281                 e.printStackTrace();
 282             }
 283         }
 284     }
 285 
 286 }