View Javadoc

1   // Copyright (C) 2005 - 2012 Philip Aston
2   // All rights reserved.
3   //
4   // This file is part of The Grinder software distribution. Refer to
5   // the file LICENSE which is part of The Grinder distribution for
6   // licensing details. The Grinder distribution is available on the
7   // Internet at http://grinder.sourceforge.net/
8   //
9   // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
12  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
13  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
14  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
17  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
19  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
20  // OF THE POSSIBILITY OF SUCH DAMAGE.
21  
22  package net.grinder.tools.tcpproxy;
23  
24  import static net.grinder.testutility.SocketUtilities.findFreePort;
25  import static org.junit.Assert.assertEquals;
26  import static org.junit.Assert.assertNotNull;
27  import static org.junit.Assert.fail;
28  import static org.mockito.Matchers.contains;
29  import static org.mockito.Matchers.isA;
30  import static org.mockito.Matchers.same;
31  import static org.mockito.Mockito.atLeast;
32  import static org.mockito.Mockito.reset;
33  import static org.mockito.Mockito.timeout;
34  import static org.mockito.Mockito.verify;
35  import static org.mockito.Mockito.verifyNoMoreInteractions;
36  
37  import java.io.ByteArrayOutputStream;
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.PrintWriter;
41  import java.io.StringWriter;
42  import java.net.ServerSocket;
43  import java.net.Socket;
44  import java.net.SocketException;
45  import java.net.UnknownHostException;
46  import java.util.Iterator;
47  import java.util.List;
48  import java.util.regex.Matcher;
49  import java.util.regex.Pattern;
50  
51  import javax.net.ssl.SSLSocket;
52  
53  import net.grinder.common.UncheckedInterruptedException;
54  import net.grinder.testutility.AssertUtilities;
55  import net.grinder.util.StreamCopier;
56  import net.grinder.util.TerminalColour;
57  
58  import org.junit.After;
59  import org.junit.Before;
60  import org.junit.Test;
61  import org.mockito.ArgumentCaptor;
62  import org.mockito.Captor;
63  import org.mockito.Mock;
64  import org.mockito.MockitoAnnotations;
65  import org.slf4j.Logger;
66  
67  
68  /**
69   * Unit tests for {@link HTTPProxyTCPProxyEngine}.
70   *
71   * @author Philip Aston
72   */
73  public class TestHTTPProxyTCPProxyEngine {
74  
75    private final List<AcceptAndEcho> m_echoers = new java.util.LinkedList<AcceptAndEcho>();
76  
77    @Mock private TCPProxyFilter m_requestFilter;
78    @Mock private TCPProxyFilter m_responseFilter;
79    @Captor private ArgumentCaptor<ConnectionDetails> m_connectionDetailsCaptor;
80  
81    @Mock private Logger m_logger;
82    private final PrintWriter m_out = new PrintWriter(new StringWriter());
83  
84    private EndPoint m_localEndPoint;
85  
86    private TCPProxySSLSocketFactory m_sslSocketFactory;
87  
88    private EndPoint createFreeLocalEndPoint() throws IOException {
89      return new EndPoint("localhost", findFreePort());
90    }
91  
92    @Before public void setUp() throws Exception {
93      MockitoAnnotations.initMocks(this);
94  
95      m_localEndPoint = createFreeLocalEndPoint();
96  
97      m_sslSocketFactory = new TCPProxySSLSocketFactoryImplementation();
98  
99      // Speed things up.
100     System.setProperty("tcpproxy.connecttimeout", "500");
101   }
102 
103   @After public void tearDown() throws Exception {
104     final Iterator<AcceptAndEcho> iterator = m_echoers.iterator();
105 
106     while (iterator.hasNext()) {
107       iterator.next().shutdown();
108     }
109   }
110 
111   @Test public void testBadLocalPort() throws Exception {
112     try {
113       new HTTPProxyTCPProxyEngine(null,
114                                   m_requestFilter,
115                                   m_responseFilter,
116                                   m_out,
117                                   m_logger,
118                                   new EndPoint("fictitious-host", 222),
119                                   false,
120                                   1000,
121                                   null,
122                                   null);
123       fail("Expected UnknownHostException");
124     }
125     catch (UnknownHostException e) {
126     }
127   }
128 
129   @Test public void testTimeOut() throws Exception {
130 
131     final TCPProxyEngine engine =
132       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
133                                   m_requestFilter,
134                                   m_responseFilter,
135                                   m_out,
136                                   m_logger,
137                                   m_localEndPoint,
138                                   false,
139                                   10,
140                                   null,
141                                   null);
142 
143     // If this ends up spinning its probably because
144     // some other test has not terminated all of its filter
145     // threads correctly.
146     engine.run();
147 
148     verify(m_logger).error(contains("Listen time out"));
149   }
150 
151   /**
152    * Read a response from a socket until it matches a given regular expression.
153    *
154    * @param clientSocket
155    *          Socket to read from.
156    * @param terminalExpression
157    *          The expression, or <code>null</code> to return the first buffer
158    *          full.
159    * @return The response.
160    * @throws IOException If a IO problem occurs.
161    * @throws InterruptedException If we're interrupted.
162    */
163   private String readResponse(final Socket clientSocket,
164                               String terminalExpression)
165     throws IOException, InterruptedException {
166     final InputStream clientInputStream = clientSocket.getInputStream();
167 
168     if (clientSocket instanceof SSLSocket) {
169       // Another reason to hate JSSE: available() returns 0 until the
170       // first read after the server has sent something; reading nothing
171       // works around this.
172       clientSocket.getInputStream().read(new byte[0]);
173     }
174 
175     while (clientInputStream.available() <= 0) {
176       Thread.sleep(10);
177     }
178 
179     final ByteArrayOutputStream response = new ByteArrayOutputStream();
180 
181     // Don't use a StreamCopier because it will block reading the
182     // input stream.
183     final byte[] buffer = new byte[100];
184 
185     final Pattern terminalPattern =
186       Pattern.compile(terminalExpression != null ? terminalExpression : ".*");
187 
188     while (true) {
189       while (clientInputStream.available() > 0) {
190         final int bytesRead = clientInputStream.read(buffer, 0, buffer.length);
191         response.write(buffer, 0, bytesRead);
192       }
193 
194       // Workaround JRockit bug.
195       //final String s = response.toString();
196       final String s = response.toString(System.getProperty("file.encoding"));
197 
198       final Matcher matcher = terminalPattern.matcher(s);
199 
200       if (matcher.find()) {
201         return s;
202       }
203 
204       final long RETRIES = 100;
205 
206       for (int i=0; i<RETRIES && clientInputStream.available() == 0; ++i) {
207         Thread.sleep(10);
208       }
209 
210       if (clientInputStream.available() == 0) {
211         fail("Stream has been idle for " + (RETRIES * 10/1000d) +
212              " seconds and the terminal expression '" + terminalExpression +
213              "' does not match received data:\n" + s);
214       }
215     }
216   }
217 
218   private void waitUntilAllStreamThreadsStopped(AbstractTCPProxyEngine engine)
219     throws InterruptedException {
220 
221     for (int i = 0;
222          i < 10 && engine.getStreamThreadGroup().activeCount() > 0;
223          ++i) {
224       Thread.sleep(50);
225     }
226 
227     assertEquals("Failed waiting for all stream threads to stop",
228                  0, engine.getStreamThreadGroup().activeCount());
229   }
230 
231   private void httpProxyEngineBadRequestTests(AbstractTCPProxyEngine engine)
232     throws Exception {
233 
234     final Socket clientSocket =
235       new Socket(engine.getListenEndPoint().getHost(),
236                  engine.getListenEndPoint().getPort());
237 
238     final PrintWriter clientWriter =
239       new PrintWriter(clientSocket.getOutputStream(), true);
240 
241     final String message = "This is not a valid HTTP message";
242     clientWriter.print(message);
243     clientWriter.flush();
244 
245     final String response = readResponse(clientSocket, null);
246 
247     AssertUtilities.assertStartsWith(response, "HTTP/1.0 400 Bad Request");
248     AssertUtilities.assertContainsHeader(response, "Connection", "close");
249     AssertUtilities.assertContainsHeader(response, "Content-Type", "text/html");
250     AssertUtilities.assertContains(response, message);
251 
252     clientSocket.close();
253 
254     verify(m_logger).error(contains("Failed to determine proxy destination"));
255 
256     final Socket clientSocket2 =
257       new Socket(engine.getListenEndPoint().getHost(),
258                  engine.getListenEndPoint().getPort());
259 
260     clientSocket2.shutdownOutput();
261 
262     try {
263       readResponse(clientSocket, null);
264       fail("Expected IOException");
265     }
266     catch (IOException e) {
267     }
268 
269     clientSocket2.close();
270 
271     verify(m_logger, timeout(10000).times(2))
272       .error(contains("Failed to determine proxy destination"));
273 
274     final Socket clientSocket3 =
275       new Socket(engine.getListenEndPoint().getHost(),
276                  engine.getListenEndPoint().getPort());
277 
278     final byte[] hugeBunchOfCrap = new byte[50000];
279     clientSocket3.getOutputStream().write(hugeBunchOfCrap);
280 
281     final String response3 = readResponse(clientSocket3, null);
282 
283     AssertUtilities.assertStartsWith(response3, "HTTP/1.0 400 Bad Request");
284     AssertUtilities.assertContainsHeader(response3, "Connection", "close");
285     AssertUtilities.assertContainsHeader(response3, "Content-Type", "text/html");
286 
287     clientSocket3.close();
288 
289     waitUntilAllStreamThreadsStopped(engine);
290 
291     verify(m_logger, timeout(10000))
292       .error(contains("failed to match HTTP message"));
293 
294     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
295   }
296 
297   private void httpProxyEngineGoodRequestTests(AbstractTCPProxyEngine engine)
298     throws Exception {
299 
300     final AcceptAndEcho echoer = new AcceptAndEcho();
301 
302     final Socket clientSocket =
303       new Socket(engine.getListenEndPoint().getHost(),
304                  engine.getListenEndPoint().getPort());
305 
306     final PrintWriter clientWriter =
307       new PrintWriter(clientSocket.getOutputStream(), true);
308 
309     final String message0 =
310       "GET http://" + echoer.getEndPoint() + "/foo HTTP/1.1\r\n" +
311       "foo: bah\r\n" +
312       "\r\n" +
313       "A \u00e0 message";
314     clientWriter.print(message0);
315     clientWriter.flush();
316 
317     final String response0 = readResponse(clientSocket, "message$");
318 
319     AssertUtilities.assertStartsWith(response0, "GET /foo HTTP/1.1\r\n");
320     AssertUtilities.assertContainsHeader(response0, "foo", "bah");
321     AssertUtilities.assertContainsPattern(response0,
322                                           "\r\n\r\nA \u00e0 message$");
323 
324     verify(m_requestFilter)
325       .connectionOpened(m_connectionDetailsCaptor.capture());
326 
327     final ConnectionDetails requestConnectionDetails =
328         m_connectionDetailsCaptor.getValue();
329     assertEquals(echoer.getEndPoint(),
330                  requestConnectionDetails.getRemoteEndPoint());
331 
332     verify(m_requestFilter).handle(same(requestConnectionDetails),
333                                    isA(new byte[0].getClass()),
334                                    isA(Integer.class));
335 
336     verify(m_responseFilter)
337       .connectionOpened(m_connectionDetailsCaptor.capture());
338 
339     final ConnectionDetails responseConnectionDetails =
340         m_connectionDetailsCaptor.getValue();
341     assertEquals(requestConnectionDetails.getOtherEnd(),
342                  responseConnectionDetails);
343 
344     verify(m_responseFilter).handle(same(responseConnectionDetails),
345                                     isA(new byte[0].getClass()),
346                                     isA(Integer.class));
347 
348     final String message1Headers =
349       "POST http://" + echoer.getEndPoint() + "/blah?x=123&y=99 HTTP/1.0\r\n" +
350       "\r\n" +
351       "Another message";
352     clientWriter.print(message1Headers);
353     clientWriter.flush();
354 
355     final String message1PostBody = "Some data, lah 0x810x820x830x84 dah";
356     clientWriter.print(message1PostBody);
357     clientWriter.flush();
358 
359     final String response1 = readResponse(clientSocket, "dah$");
360 
361     AssertUtilities.assertStartsWith(response1,
362                                      "POST /blah?x=123&y=99 HTTP/1.0\r\n");
363     AssertUtilities.assertContainsPattern(response1,
364                                           "\r\n\r\nAnother message" +
365                                           message1PostBody + "$");
366 
367     // Do again, but force engine to handle body in two parts.
368     clientWriter.print(message1Headers);
369     clientWriter.flush();
370 
371     final String response2a = readResponse(clientSocket, "Another message$");
372 
373     AssertUtilities.assertStartsWith(response2a,
374                                      "POST /blah?x=123&y=99 HTTP/1.0\r\n");
375 
376     clientWriter.print(message1PostBody);
377     clientWriter.flush();
378 
379     final String response2b = readResponse(clientSocket, "dah$");
380     assertNotNull(response2b);
381 
382     clientSocket.close();
383 
384     waitUntilAllStreamThreadsStopped(engine);
385 
386     // handle() must have been called at least 3 further times, but can be
387     // called more.
388     verify(m_requestFilter, atLeast(4)).handle(same(requestConnectionDetails),
389                                                isA(new byte[0].getClass()),
390                                                isA(Integer.class));
391 
392     verify(m_responseFilter, atLeast(4)).handle(same(responseConnectionDetails),
393                                                 isA(new byte[0].getClass()),
394                                                 isA(Integer.class));
395 
396     verify(m_requestFilter).connectionClosed(requestConnectionDetails);
397     verify(m_responseFilter).connectionClosed(responseConnectionDetails);
398 
399     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
400   }
401 
402   private void httpsProxyEngineGoodRequestTest(AbstractTCPProxyEngine engine)
403     throws Exception {
404 
405     final AcceptAndEcho echoer = new SSLAcceptAndEcho();
406 
407     final Socket clientPlainSocket =
408       new Socket(engine.getListenEndPoint().getHost(),
409                  engine.getListenEndPoint().getPort());
410 
411     final PrintWriter clientWriter =
412       new PrintWriter(clientPlainSocket.getOutputStream(), true);
413     clientWriter.print("CONNECT " + echoer.getEndPoint() + "\r\n\r\n");
414     clientWriter.flush();
415 
416     final String response = readResponse(clientPlainSocket, "Proxy-agent");
417 
418     AssertUtilities.assertStartsWith(response, "HTTP/1.0 200 OK\r\n");
419     AssertUtilities.assertContainsHeader(response,
420                                          "Proxy-agent",
421                                          "The Grinder.*");
422 
423     final Socket clientSSLSocket =
424       m_sslSocketFactory.createClientSocket(clientPlainSocket,
425                                             echoer.getEndPoint());
426 
427     final PrintWriter secureClientWriter =
428       new PrintWriter(clientSSLSocket.getOutputStream(), true);
429 
430     // No URL decoration should take place. Feed an absolute URL
431     // to be difficult.
432     final String message0 =
433       "GET http://galafray/foo HTTP/1.1\r\n" +
434       "foo: bah\r\n" +
435       "\r\n" +
436       "A \u00e0 message";
437     secureClientWriter.print(message0);
438     secureClientWriter.flush();
439 
440     final String response0 = readResponse(clientSSLSocket, "message$");
441 
442     AssertUtilities.assertStartsWith(response0,
443                                      "GET http://galafray/foo HTTP/1.1\r\n");
444     AssertUtilities.assertContainsHeader(response0, "foo", "bah");
445     AssertUtilities.assertContainsPattern(response0,
446                                           "\r\n\r\nA \u00e0 message$");
447 
448     verify(m_requestFilter)
449       .connectionOpened(m_connectionDetailsCaptor.capture());
450 
451     final ConnectionDetails requestConnectionDetails =
452       m_connectionDetailsCaptor.getValue();
453     assertEquals(echoer.getEndPoint(),
454                  requestConnectionDetails.getRemoteEndPoint());
455 
456     verify(m_requestFilter).handle(same(requestConnectionDetails),
457                                    isA(new byte[0].getClass()),
458                                    isA(Integer.class));
459 
460     verifyNoMoreInteractions(m_requestFilter);
461 
462 
463     verify(m_responseFilter)
464       .connectionOpened(m_connectionDetailsCaptor.capture());
465 
466     final ConnectionDetails responseConnectionDetails =
467       m_connectionDetailsCaptor.getValue();
468     assertEquals(requestConnectionDetails.getOtherEnd(),
469                  responseConnectionDetails);
470 
471     verify(m_responseFilter).handle(same(responseConnectionDetails),
472                                    isA(new byte[0].getClass()),
473                                    isA(Integer.class));
474 
475     verifyNoMoreInteractions(m_responseFilter);
476 
477     clientSSLSocket.close();
478 
479     waitUntilAllStreamThreadsStopped(engine);
480 
481     verify(m_requestFilter, timeout(5000))
482       .connectionClosed(requestConnectionDetails);
483 
484     verify(m_responseFilter, timeout(5000))
485     .connectionClosed(responseConnectionDetails);
486 
487     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
488   }
489 
490   @Test public void testHTTPProxyEngine() throws Exception {
491     final AbstractTCPProxyEngine engine =
492       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
493                                   m_requestFilter,
494                                   m_responseFilter,
495                                   m_out,
496                                   m_logger,
497                                   m_localEndPoint,
498                                   false,
499                                   100000,
500                                   null,
501                                   null);
502 
503     final Thread engineThread = new Thread(engine, "Run engine");
504     engineThread.start();
505 
506     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
507 
508     assertEquals(m_localEndPoint, engine.getListenEndPoint());
509     assertNotNull(engine.getSocketFactory());
510     assertEquals(TerminalColour.NONE, engine.getRequestColour());
511     assertEquals(TerminalColour.NONE, engine.getResponseColour());
512 
513     httpProxyEngineBadRequestTests(engine);
514     reset(m_requestFilter, m_responseFilter);
515     httpProxyEngineGoodRequestTests(engine);
516     reset(m_requestFilter, m_responseFilter);
517     httpsProxyEngineGoodRequestTest(engine);
518 
519     engine.stop();
520     engineThread.join();
521 
522     // Stopping engine again doesn't do anything.
523     engine.stop();
524 
525     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
526   }
527 
528   @Test public void testColourHTTPProxyEngine() throws Exception {
529 
530     final AbstractTCPProxyEngine engine =
531       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
532                                   m_requestFilter,
533                                   m_responseFilter,
534                                   m_out,
535                                   m_logger,
536                                   m_localEndPoint,
537                                   true,
538                                   100000,
539                                   null,
540                                   null);
541 
542     final Thread engineThread = new Thread(engine, "Run engine");
543     engineThread.start();
544 
545     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
546 
547     assertEquals(m_localEndPoint, engine.getListenEndPoint());
548     assertNotNull(engine.getSocketFactory());
549     assertEquals(TerminalColour.RED, engine.getRequestColour());
550     assertEquals(TerminalColour.BLUE, engine.getResponseColour());
551 
552     httpProxyEngineBadRequestTests(engine);
553     reset(m_requestFilter, m_responseFilter);
554     httpProxyEngineGoodRequestTests(engine);
555 
556     engine.stop();
557     engineThread.join();
558 
559     // Stopping engine again doesn't do anything.
560     engine.stop();
561 
562     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
563   }
564 
565   @Test public void testWithChainedHTTPProxy() throws Exception {
566     final AcceptAndEcho echoer = new AcceptAndEcho();
567 
568     final EndPoint chainedProxyEndPoint = createFreeLocalEndPoint();
569 
570     final AbstractTCPProxyEngine chainedProxy =
571       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
572                                   m_requestFilter,
573                                   m_responseFilter,
574                                   m_out,
575                                   m_logger,
576                                   chainedProxyEndPoint,
577                                   true,
578                                   100000,
579                                   null,
580                                   null);
581 
582     final Thread chainedProxyThread =
583       new Thread(chainedProxy, "Run chained proxy engine");
584     chainedProxyThread.start();
585 
586     final AbstractTCPProxyEngine engine =
587       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
588                                   new NullFilter(),
589                                   new NullFilter(),
590                                   m_out,
591                                   m_logger,
592                                   m_localEndPoint,
593                                   true,
594                                   100000,
595                                   chainedProxyEndPoint,
596                                   null);
597     final Thread engineThread = new Thread(engine, "Run engine");
598     engineThread.start();
599 
600     final Socket clientSocket =
601       new Socket(engine.getListenEndPoint().getHost(),
602                  engine.getListenEndPoint().getPort());
603 
604     final PrintWriter clientWriter =
605       new PrintWriter(clientSocket.getOutputStream(), true);
606 
607     final String message0 =
608       "GET http://" + echoer.getEndPoint() + "/ HTTP/1.1\r\n" +
609       "foo: bah\r\n" +
610       "\r\n" +
611       "Proxy me";
612     clientWriter.print(message0);
613     clientWriter.flush();
614 
615     final String response0 = readResponse(clientSocket, "Proxy me$");
616 
617     AssertUtilities.assertStartsWith(response0, "GET / HTTP/1.1\r\n");
618     AssertUtilities.assertContainsHeader(response0, "foo", "bah");
619     AssertUtilities.assertContainsPattern(response0, "\r\n\r\nProxy me$");
620 
621     verify(m_requestFilter)
622       .connectionOpened(m_connectionDetailsCaptor.capture());
623 
624     final ConnectionDetails requestConnectionDetails =
625         m_connectionDetailsCaptor.getValue();
626 
627     verify(m_requestFilter).handle(same(requestConnectionDetails),
628                                    isA(new byte[0].getClass()),
629                                    isA(Integer.class));
630     verifyNoMoreInteractions(m_requestFilter);
631 
632     verify(m_responseFilter)
633     .connectionOpened(m_connectionDetailsCaptor.capture());
634 
635     final ConnectionDetails responseConnectionDetails =
636         m_connectionDetailsCaptor.getValue();
637 
638     verify(m_responseFilter).handle(same(responseConnectionDetails),
639                                    isA(new byte[0].getClass()),
640                                    isA(Integer.class));
641     verifyNoMoreInteractions(m_responseFilter);
642 
643     chainedProxy.stop();
644     chainedProxyThread.join();
645 
646     engine.stop();
647     engineThread.join();
648 
649     waitUntilAllStreamThreadsStopped(engine);
650 
651     verify(m_requestFilter).connectionClosed(requestConnectionDetails);
652     verify(m_responseFilter).connectionClosed(responseConnectionDetails);
653 
654     // Stopping engine again doesn't do anything.
655     engine.stop();
656 
657     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
658   }
659 
660   @Test public void testWithChainedHTTPSProxy() throws Exception {
661     final AcceptAndEcho echoer = new SSLAcceptAndEcho();
662 
663     final EndPoint chainedProxyEndPoint = createFreeLocalEndPoint();
664 
665     final AbstractTCPProxyEngine chainedProxy =
666       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
667                                   m_requestFilter,
668                                   m_responseFilter,
669                                   m_out,
670                                   m_logger,
671                                   chainedProxyEndPoint,
672                                   true,
673                                   100000,
674                                   null,
675                                   null);
676 
677     final Thread chainedProxyThread =
678       new Thread(chainedProxy, "Run chained proxy engine");
679     chainedProxyThread.start();
680 
681     final AbstractTCPProxyEngine engine =
682       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
683                                   new NullFilter(),
684                                   new NullFilter(),
685                                   m_out,
686                                   m_logger,
687                                   m_localEndPoint,
688                                   true,
689                                   100000,
690                                   null,
691                                   chainedProxyEndPoint);
692 
693     final Thread engineThread = new Thread(engine, "Run engine");
694     engineThread.start();
695 
696     final Socket clientPlainSocket =
697       new Socket(engine.getListenEndPoint().getHost(),
698                  engine.getListenEndPoint().getPort());
699 
700     final PrintWriter clientWriter =
701       new PrintWriter(clientPlainSocket.getOutputStream(), true);
702     clientWriter.print("CONNECT " + echoer.getEndPoint() + "\r\n\r\n");
703     clientWriter.flush();
704 
705     final String response = readResponse(clientPlainSocket, "Proxy-agent");
706 
707     AssertUtilities.assertStartsWith(response, "HTTP/1.0 200 OK\r\n");
708     AssertUtilities.assertContainsHeader(response,
709                                          "Proxy-agent",
710                                          "The Grinder.*");
711 
712     final Socket clientSSLSocket =
713       m_sslSocketFactory.createClientSocket(clientPlainSocket,
714                                             echoer.getEndPoint());
715 
716     final PrintWriter secureClientWriter =
717       new PrintWriter(clientSSLSocket.getOutputStream(), true);
718 
719     // No URL decoration should take place. Feed an absolute URL
720     // to be difficult.
721     final String message0 =
722       "GET http://galafray/foo HTTP/1.1\r\n" +
723       "foo: bah\r\n" +
724       "\r\n" +
725       "A \u00e0 message";
726     secureClientWriter.print(message0);
727     secureClientWriter.flush();
728 
729     final String response0 = readResponse(clientSSLSocket, "message$");
730 
731     AssertUtilities.assertStartsWith(response0,
732                                      "GET http://galafray/foo HTTP/1.1\r\n");
733     AssertUtilities.assertContainsHeader(response0, "foo", "bah");
734     AssertUtilities.assertContainsPattern(response0,
735                                           "\r\n\r\nA \u00e0 message$");
736 
737     verify(m_requestFilter)
738       .connectionOpened(m_connectionDetailsCaptor.capture());
739 
740     final ConnectionDetails requestConnectionDetails =
741         m_connectionDetailsCaptor.getValue();
742 
743     verify(m_requestFilter).handle(same(requestConnectionDetails),
744                                    isA(new byte[0].getClass()),
745                                    isA(Integer.class));
746     verifyNoMoreInteractions(m_requestFilter);
747 
748     verify(m_responseFilter)
749       .connectionOpened(m_connectionDetailsCaptor.capture());
750 
751     final ConnectionDetails responseConnectionDetails =
752         m_connectionDetailsCaptor.getValue();
753 
754     verify(m_responseFilter).handle(same(responseConnectionDetails),
755                                    isA(new byte[0].getClass()),
756                                    isA(Integer.class));
757     verifyNoMoreInteractions(m_responseFilter);
758 
759     engine.stop();
760     engineThread.join();
761 
762     chainedProxy.stop();
763     chainedProxyThread.join();
764 
765     waitUntilAllStreamThreadsStopped(engine);
766 
767     verify(m_requestFilter).connectionClosed(requestConnectionDetails);
768     verify(m_responseFilter).connectionClosed(responseConnectionDetails);
769 
770     // Sometimes log an SSL exception when shutting down.
771     // m_loggerStubFactory.assertNoMoreCalls();
772 
773     // Stopping engine or filter again doesn't do anything.
774     engine.stop();
775 
776     verifyNoMoreInteractions(m_requestFilter, m_responseFilter);
777   }
778 
779 
780   @Test public void testStopWithBlockedFilterThreads() throws Exception {
781 
782     final AcceptAndEcho echoer = new AcceptAndEcho();
783 
784     // I wanted to implement this test so that a filter thread blocked
785     // on some hung connection. However, it's hard to simulate a connection
786     // timeout in a single JVM; I thought it could be done by connected to a
787     // socket that never accepted but the client side of the connection was
788     // established just fine. Instead, we use an evil filter that blocks
789     // when it receives the connection opened event.
790 
791     final AbstractTCPProxyEngine engine =
792       new HTTPProxyTCPProxyEngine(m_sslSocketFactory,
793                                   new HungFilter(),
794                                   m_responseFilter,
795                                   m_out,
796                                   m_logger,
797                                   m_localEndPoint,
798                                   false,
799                                   100000,
800                                   null,
801                                   null);
802 
803     final Thread engineThread = new Thread(engine, "Run engine");
804     engineThread.start();
805 
806     final ServerSocket serverSocket = new ServerSocket(0);
807 
808     final Socket clientSocket =
809       new Socket(engine.getListenEndPoint().getHost(),
810                  engine.getListenEndPoint().getPort());
811 
812     final PrintWriter clientWriter =
813       new PrintWriter(clientSocket.getOutputStream(), true);
814 
815     final String message0 =
816       "GET http://" + echoer.getEndPoint() + "/foo HTTP/1.1\r\n" +
817       "foo: bah\r\n" +
818       "\r\n" +
819       "A \u00e0 message";
820     clientWriter.print(message0);
821     clientWriter.flush();
822 
823     // Wait until the filter thread is spinning so that there's
824     // a good chancce it's hung.
825     for (int i = 0;
826          i < 10 && engine.getStreamThreadGroup().activeCount() != 1;
827          ++i) {
828       Thread.sleep(50);
829     }
830 
831     assertEquals(1, engine.getStreamThreadGroup().activeCount());
832 
833     engine.stop();
834     engineThread.join();
835 
836     serverSocket.close();
837 
838     waitUntilAllStreamThreadsStopped(engine);
839   }
840 
841   private static class HungFilter implements TCPProxyFilter {
842 
843     public void connectionClosed(ConnectionDetails connectionDetails)
844       throws FilterException {
845     }
846 
847     public void connectionOpened(ConnectionDetails connectionDetails)
848       throws FilterException {
849 
850       try {
851         synchronized (this) {
852           wait();
853         }
854       }
855       catch (InterruptedException e) {
856         throw new UncheckedInterruptedException(e);
857       }
858     }
859 
860     public byte[] handle(ConnectionDetails connectionDetails,
861                          byte[] buffer,
862                          int bytesRead) throws FilterException {
863       return null;
864     }
865   }
866 
867 
868   private class AcceptAndEcho implements Runnable {
869     private final ServerSocket m_serverSocket;
870 
871     public AcceptAndEcho() throws IOException {
872       this(new ServerSocket(0));
873     }
874 
875     protected AcceptAndEcho(ServerSocket serverSocket) throws IOException {
876       m_serverSocket = serverSocket;
877       new Thread(this, getClass().getName()).start();
878       m_echoers.add(this);
879     }
880 
881     public EndPoint getEndPoint() {
882       return EndPoint.serverEndPoint(m_serverSocket);
883     }
884 
885     public void run() {
886       try {
887         while (true) {
888           final Socket socket = m_serverSocket.accept();
889 
890           new Thread(
891             new StreamCopier(1000, true).getRunnable(socket.getInputStream(),
892                                                      socket.getOutputStream()),
893             "Echo thread").start();
894         }
895       }
896       catch (SocketException e) {
897         // Ignore - probably shutdown.
898       }
899       catch (IOException e) {
900         fail("Got a " + e.getClass());
901       }
902     }
903 
904     public void shutdown() throws IOException {
905       m_serverSocket.close();
906     }
907   }
908 
909   private class SSLAcceptAndEcho extends AcceptAndEcho {
910     public SSLAcceptAndEcho() throws IOException {
911       super(
912         m_sslSocketFactory.createServerSocket(createFreeLocalEndPoint(), 0));
913     }
914   }
915 }