1 /*
2  * Copyright (c) 2015, 2018, 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 com.sun.net.httpserver.HttpServer;
25 
26 import java.io.Closeable;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.OutputStream;
30 import javax.net.ServerSocketFactory;
31 import javax.net.ssl.SSLServerSocket;
32 import java.net.InetAddress;
33 import java.net.InetSocketAddress;
34 import java.net.ServerSocket;
35 import java.net.Socket;
36 import java.nio.charset.StandardCharsets;
37 import java.util.Collections;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Iterator;
41 import java.util.concurrent.ArrayBlockingQueue;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.atomic.AtomicInteger;
44 import static java.nio.charset.StandardCharsets.ISO_8859_1;
45 
46 /**
47  * A cut-down Http/1 Server for testing various error situations
48  *
49  * use interrupt() to halt
50  */
51 public class MockServer extends Thread implements Closeable {
52 
53     final ServerSocket ss;
54     private final List<Connection> sockets;
55     private final List<Connection> removals;
56     private final List<Connection> additions;
57     AtomicInteger counter = new AtomicInteger(0);
58     // if specified (not null), only requests which
59     // contain this value in their status line
60     // will be taken into account and returned by activity().
61     // Other requests will get summarily closed.
62     // When specified, this can prevent answering to rogue
63     // (external) clients that might be lurking
64     // on the test machine instead of answering
65     // to the test client.
66    final String root;
67 
68     // waits up to 2000 seconds for something to happen
69     // dont use this unless certain activity coming.
activity()70     public Connection activity() {
71         for (int i = 0; i < 80 * 100; i++) {
72             doRemovalsAndAdditions();
73             for (Connection c : sockets) {
74                 if (c.poll()) {
75                     if (root != null) {
76                         // if a root was specified in MockServer
77                         // constructor, rejects (by closing) all
78                         // requests whose statusLine does not contain
79                         // root.
80                         if (!c.statusLine.contains(root)) {
81                             System.out.println("Bad statusLine: "
82                                     + c.statusLine
83                                     + " closing connection");
84                             c.close();
85                             continue;
86                         }
87                     }
88                     return c;
89                 }
90             }
91             try {
92                 Thread.sleep(250);
93             } catch (InterruptedException e) {
94                 e.printStackTrace();
95             }
96         }
97         return null;
98     }
99 
doRemovalsAndAdditions()100     private void doRemovalsAndAdditions() {
101         synchronized (removals) {
102             Iterator<Connection> i = removals.iterator();
103             while (i.hasNext()) {
104                 Connection c = i.next();
105                 System.out.println("socket removed: " + c);
106                 sockets.remove(c);
107             }
108             removals.clear();
109         }
110 
111         synchronized (additions) {
112             Iterator<Connection> i = additions.iterator();
113             while (i.hasNext()) {
114                 Connection c = i.next();
115                 System.out.println("socket added: " + c);
116                 sockets.add(c);
117             }
118             additions.clear();
119         }
120     }
121 
122     // clears all current connections on Server.
reset()123     public void reset() {
124         for (Connection c : sockets) {
125             c.close();
126         }
127     }
128 
129     /**
130      * Reads data into an ArrayBlockingQueue<String> where each String
131      * is a line of input, that was terminated by CRLF (not included)
132      */
133     class Connection extends Thread {
Connection(Socket s)134         Connection(Socket s) throws IOException {
135             this.socket = s;
136             id = counter.incrementAndGet();
137             is = s.getInputStream();
138             os = s.getOutputStream();
139             incoming = new ArrayBlockingQueue<>(100);
140             setName("Server-Connection");
141             setDaemon(true);
142         }
143         final Socket socket;
144         final int id;
145         final InputStream is;
146         final OutputStream os;
147         final ArrayBlockingQueue<String> incoming;
148         volatile String statusLine;
149 
150         final static String CRLF = "\r\n";
151 
152         // sentinel indicating connection closed
153         final static String CLOSED = "C.L.O.S.E.D";
154         volatile boolean closed = false;
155         volatile boolean released = false;
156 
157         @Override
run()158         public void run() {
159             byte[] buf = new byte[256];
160             String s = "";
161             try {
162                 while (true) {
163                     int n = is.read(buf);
164                     if (n == -1) {
165                         cleanup();
166                         return;
167                     }
168                     String s0 = new String(buf, 0, n, ISO_8859_1);
169                     s = s + s0;
170                     int i;
171                     while ((i=s.indexOf(CRLF)) != -1) {
172                         String s1 = s.substring(0, i+2);
173                         System.out.println("Server got: " + s1.substring(0,i));
174                         if (statusLine == null) statusLine = s1.substring(0,i);
175                         incoming.put(s1);
176                         if (i+2 == s.length()) {
177                             s = "";
178                             break;
179                         }
180                         s = s.substring(i+2);
181                     }
182                 }
183             } catch (IOException |InterruptedException e1) {
184                 cleanup();
185             } catch (Throwable t) {
186                 System.out.println("Exception: " + t);
187                 t.printStackTrace();
188                 cleanup();
189             }
190         }
191 
192         @Override
toString()193         public String toString() {
194             return "Server.Connection: " + socket.toString();
195         }
196 
sendHttpResponse(int code, String body, String... headers)197         public void sendHttpResponse(int code, String body, String... headers)
198             throws IOException
199         {
200             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
201             for (int i=0; i<headers.length; i+=2) {
202                 r1 += headers[i] + ": " + headers[i+1] + CRLF;
203             }
204             int clen = body == null ? 0 : body.getBytes(ISO_8859_1).length;
205             r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
206             r1 += CRLF;
207             if (body != null) {
208                 r1 += body;
209             }
210             send(r1);
211         }
212 
213         // content-length is 10 bytes too many
sendIncompleteHttpResponseBody(int code)214         public void sendIncompleteHttpResponseBody(int code) throws IOException {
215             String body = "Hello World Helloworld Goodbye World";
216             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
217             int clen = body.getBytes(ISO_8859_1).length + 10;
218             r1 += "Content-Length: " + Integer.toString(clen) + CRLF;
219             r1 += CRLF;
220             if (body != null) {
221                 r1 += body;
222             }
223             send(r1);
224         }
225 
sendIncompleteHttpResponseHeaders(int code)226         public void sendIncompleteHttpResponseHeaders(int code)
227             throws IOException
228         {
229             String r1 = "HTTP/1.1 " + Integer.toString(code) + " status" + CRLF;
230             send(r1);
231         }
232 
send(String r)233         public void send(String r) throws IOException {
234             try {
235                 os.write(r.getBytes(ISO_8859_1));
236             } catch (IOException x) {
237                 IOException suppressed =
238                         new IOException("MockServer["
239                             + ss.getLocalPort()
240                             +"] Failed while writing bytes: "
241                             +  x.getMessage());
242                 x.addSuppressed(suppressed);
243                 System.err.println("WARNING: " + suppressed);
244                 throw x;
245             }
246         }
247 
close()248         public synchronized void close() {
249             cleanup();
250             closed = true;
251             incoming.clear();
252         }
253 
nextInput(long timeout, TimeUnit unit)254         public String nextInput(long timeout, TimeUnit unit) {
255             String result = "";
256             while (poll()) {
257                 try {
258                     String s = incoming.poll(timeout, unit);
259                     if (s == null && closed) {
260                         return CLOSED;
261                     } else {
262                         result += s;
263                     }
264                 } catch (InterruptedException e) {
265                     return null;
266                 }
267             }
268             return result;
269         }
270 
nextInput()271         public String nextInput() {
272             return nextInput(0, TimeUnit.SECONDS);
273         }
274 
poll()275         public boolean poll() {
276             return incoming.peek() != null;
277         }
278 
cleanup()279         private void cleanup() {
280             if (released) return;
281             synchronized(this) {
282                 if (released) return;
283                 released = true;
284             }
285             try {
286                 socket.close();
287             } catch (Throwable e) {}
288             synchronized (removals) {
289                 removals.add(this);
290             }
291         }
292     }
293 
MockServer(int port, ServerSocketFactory factory, String root)294     MockServer(int port, ServerSocketFactory factory, String root) throws IOException {
295         ss = factory.createServerSocket();
296         ss.setReuseAddress(false);
297         ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
298         this.root = root; // if specified, any request which don't have this value
299                           // in their statusLine will be rejected.
300         sockets = Collections.synchronizedList(new LinkedList<>());
301         removals = new LinkedList<>();
302         additions = new LinkedList<>();
303         setName("Test-Server");
304         setDaemon(true);
305     }
306 
MockServer(int port, ServerSocketFactory factory)307     MockServer(int port, ServerSocketFactory factory) throws IOException {
308         this(port, factory, "/foo/");
309     }
310 
MockServer(int port)311     MockServer(int port) throws IOException {
312         this(port, ServerSocketFactory.getDefault());
313     }
314 
MockServer()315     MockServer() throws IOException {
316         this(0);
317     }
318 
port()319     int port() {
320         return ss.getLocalPort();
321     }
322 
serverAuthority()323     String serverAuthority() {
324         return InetAddress.getLoopbackAddress().getHostName() + ":" + port();
325     }
326 
getURL()327     public String getURL() {
328         if (ss instanceof SSLServerSocket) {
329             return "https://" + serverAuthority() + "/foo/";
330         } else {
331             return "http://" + serverAuthority() + "/foo/";
332         }
333     }
334 
335     private volatile boolean closed;
336 
337     @Override
close()338     public void close() {
339         closed = true;
340         try {
341             ss.close();
342         } catch (Throwable e) {
343             e.printStackTrace();
344         }
345         for (Connection c : sockets) {
346             c.close();
347         }
348     }
349 
350     @Override
run()351     public void run() {
352         try {
353             while (!closed) {
354                 try {
355                     System.out.println("Server waiting for connection");
356                     Socket s = ss.accept();
357                     Connection c = new Connection(s);
358                     c.start();
359                     System.out.println("Server got new connection: " + c);
360                     synchronized (additions) {
361                         additions.add(c);
362                     }
363                 } catch (IOException e) {
364                     if (closed)
365                         return;
366                     e.printStackTrace(System.out);
367                 }
368             }
369         } catch (Throwable t) {
370             System.out.println("Unexpected exception in accept loop: " + t);
371             t.printStackTrace(System.out);
372         } finally {
373             if (closed) {
374                 System.out.println("Server closed: exiting accept loop");
375             } else {
376                 System.out.println("Server not closed: exiting accept loop and closing");
377                 close();
378             }
379         }
380     }
381 
382 }
383