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