View Javadoc

1   // Copyright (C) 2005 - 2011 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.Mockito.reset;
31  import static org.mockito.Mockito.times;
32  import static org.mockito.Mockito.verify;
33  import static org.mockito.Mockito.verifyNoMoreInteractions;
34  
35  import java.io.ByteArrayOutputStream;
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.io.OutputStreamWriter;
39  import java.io.PrintWriter;
40  import java.io.StringWriter;
41  import java.net.ServerSocket;
42  import java.net.Socket;
43  import java.net.UnknownHostException;
44  
45  import net.grinder.testutility.CallData;
46  import net.grinder.tools.tcpproxy.TCPProxyFilter.FilterException;
47  import net.grinder.util.StreamCopier;
48  import net.grinder.util.TerminalColour;
49  
50  import org.junit.Before;
51  import org.junit.Test;
52  import org.mockito.Mock;
53  import org.mockito.MockitoAnnotations;
54  import org.slf4j.Logger;
55  
56  
57  /**
58   * Unit test case for {@link PortForwarderTCPProxyEngine}.
59   *
60   * @author Philip Aston
61   */
62  public class TestPortForwarderTCPProxyEngine {
63  
64    private final MyFilterStubFactory m_requestFilterStubFactory =
65      new MyFilterStubFactory();
66    private final TCPProxyFilter m_requestFilter =
67      m_requestFilterStubFactory.getStub();
68  
69    private final MyFilterStubFactory m_responseFilterStubFactory =
70      new MyFilterStubFactory();
71    private final TCPProxyFilter m_responseFilter =
72      m_responseFilterStubFactory.getStub();
73  
74    @Mock private Logger m_logger;
75    private final PrintWriter m_out = new PrintWriter(new StringWriter());
76  
77    private int m_localPort;
78  
79    @Before public void setUp() throws Exception {
80      m_localPort = findFreePort();
81  
82      MockitoAnnotations.initMocks(this);
83  
84      resetLogger();
85    }
86  
87    private void resetLogger() {
88      reset(m_logger);
89    }
90  
91    @Test public void testBadLocalPort() throws Exception {
92      final ConnectionDetails badConnectionDetails =
93        new ConnectionDetails(new EndPoint("fictitious-host", 111),
94                              new EndPoint("to", 222),
95                              false);
96  
97      try {
98        new PortForwarderTCPProxyEngine(m_requestFilter,
99                                        m_responseFilter,
100                                       m_out,
101                                       m_logger,
102                                       badConnectionDetails,
103                                       false,
104                                       1000);
105       fail("Expected UnknownHostException");
106     }
107     catch (UnknownHostException e) {
108     }
109   }
110 
111   @Test public void testTimeOut() throws Exception {
112 
113     final ConnectionDetails connectionDetails =
114       new ConnectionDetails(new EndPoint("localhost", m_localPort),
115                             new EndPoint("wherever", 9999),
116                             false);
117 
118     final TCPProxyEngine engine =
119       new PortForwarderTCPProxyEngine(m_requestFilter,
120                                       m_responseFilter,
121                                       m_out,
122                                       m_logger,
123                                       connectionDetails,
124                                       false,
125                                       10);
126 
127     resetLogger();
128 
129     // If this ends up spinning its probably because
130     // some other test has not terminated all of its filter
131     // threads correctly.
132     engine.run();
133 
134     verify(m_logger).error("Listen time out");
135     verifyNoMoreInteractions(m_logger);
136   }
137 
138   private void engineTests(AbstractTCPProxyEngine engine,
139                            ConnectionDetails connectionDetails)
140     throws Exception {
141 
142     final Thread engineThread = new Thread(engine, "Run engine");
143     engineThread.start();
144 
145     final Socket clientSocket =
146       new Socket(engine.getListenEndPoint().getHost(),
147                  engine.getListenEndPoint().getPort());
148 
149     final OutputStreamWriter outputStreamWriter =
150       new OutputStreamWriter(clientSocket.getOutputStream());
151 
152     final PrintWriter clientWriter =
153       new PrintWriter(outputStreamWriter, true);
154 
155     final String message =
156       "This is some stuff\r\nWe expect to be echoed.\u00ff\u00fe";
157     clientWriter.print(message);
158     clientWriter.flush();
159 
160     final InputStream clientInputStream = clientSocket.getInputStream();
161 
162     while (clientInputStream.available() <= 0) {
163       Thread.sleep(10);
164     }
165 
166     final ByteArrayOutputStream response = new ByteArrayOutputStream();
167 
168     // Don't use a StreamCopier because it will block reading the
169     // input stream.
170     final byte[] buffer = new byte[100];
171 
172     while (clientInputStream.available() > 0) {
173       final int bytesRead = clientInputStream.read(buffer, 0, buffer.length);
174       response.write(buffer, 0, bytesRead);
175     }
176 
177     // Not sure why, but on some JVMs, this fails if we use BAOS.toString().
178     // Why should the default encoding used by OSW differ from that used
179     // by BAOS?
180     assertEquals(message, response.toString(outputStreamWriter.getEncoding()));
181 
182     clientSocket.close();
183 
184     engine.stop();
185     engineThread.join();
186 
187     final CallData callData =
188       m_requestFilterStubFactory.assertSuccess("connectionOpened",
189                                                ConnectionDetails.class);
190 
191     // Check the remote endpoint and isSecure of the connection details matches
192     // those of our remote endpoint.
193     final ConnectionDetails localConnectionDetails =
194       (ConnectionDetails)callData.getParameters()[0];
195 
196     assertEquals(connectionDetails.getRemoteEndPoint(),
197       localConnectionDetails.getRemoteEndPoint());
198     assertEquals(connectionDetails.isSecure(),
199       localConnectionDetails.isSecure());
200 
201     m_requestFilterStubFactory.assertSuccess("handle",
202                                              ConnectionDetails.class,
203                                              new byte[0].getClass(),
204                                              Integer.class);
205     m_requestFilterStubFactory.assertSuccess("connectionClosed",
206                                              ConnectionDetails.class);
207     m_requestFilterStubFactory.assertNoMoreCalls();
208 
209     m_responseFilterStubFactory.assertSuccess("connectionOpened",
210                                              ConnectionDetails.class);
211     m_responseFilterStubFactory.assertSuccess("handle",
212                                              ConnectionDetails.class,
213                                              new byte[0].getClass(),
214                                              Integer.class);
215 
216     m_responseFilterStubFactory.setIgnoreCallOrder(true);
217 
218     m_responseFilterStubFactory.assertSuccess(
219       "connectionClosed", ConnectionDetails.class);
220     m_responseFilterStubFactory.assertNoMoreCalls();
221 
222     verifyNoMoreInteractions(m_logger);
223 
224     // Stopping engine or filter again doesn't do anything.
225     engine.stop();
226 
227     m_requestFilterStubFactory.assertNoMoreCalls();
228     m_responseFilterStubFactory.assertNoMoreCalls();
229   }
230 
231   @Test public void testEngine() throws Exception {
232 
233     final AcceptSingleConnectionAndEcho echoer =
234       new AcceptSingleConnectionAndEcho();
235 
236     final EndPoint localEndPoint = new EndPoint("localhost", m_localPort);
237 
238     final ConnectionDetails connectionDetails =
239       new ConnectionDetails(localEndPoint,
240                             echoer.getEndPoint(),
241                             false);
242 
243     // Set the filters not to randomly generate output.
244     m_requestFilterStubFactory.setResult(null);
245     m_responseFilterStubFactory.setResult(null);
246 
247     final AbstractTCPProxyEngine engine =
248       new PortForwarderTCPProxyEngine(m_requestFilter,
249                                       m_responseFilter,
250                                       m_out,
251                                       m_logger,
252                                       connectionDetails,
253                                       false,
254                                       100000);
255 
256     m_responseFilterStubFactory.assertNoMoreCalls();
257     m_requestFilterStubFactory.assertNoMoreCalls();
258 
259     assertEquals(localEndPoint, engine.getListenEndPoint());
260     assertNotNull(engine.getSocketFactory());
261     m_requestFilterStubFactory.assertIsWrappedBy(engine.getRequestFilter());
262     m_responseFilterStubFactory.assertIsWrappedBy(engine.getResponseFilter());
263     assertEquals(TerminalColour.NONE, engine.getRequestColour());
264     assertEquals(TerminalColour.NONE, engine.getResponseColour());
265 
266     resetLogger();
267     m_requestFilterStubFactory.resetCallHistory();
268     m_responseFilterStubFactory.resetCallHistory();
269 
270     engineTests(engine, connectionDetails);
271   }
272 
273   @Test public void testColourEngine() throws Exception {
274 
275     final AcceptSingleConnectionAndEcho echoer =
276       new AcceptSingleConnectionAndEcho();
277 
278     final EndPoint localEndPoint = new EndPoint("localhost", m_localPort);
279 
280     final ConnectionDetails connectionDetails =
281       new ConnectionDetails(localEndPoint,
282                             echoer.getEndPoint(),
283                             true);
284 
285     // Set the filters not to randomly generate output.
286     m_requestFilterStubFactory.setResult(null);
287     m_responseFilterStubFactory.setResult(null);
288 
289     final AbstractTCPProxyEngine engine =
290       new PortForwarderTCPProxyEngine(m_requestFilter,
291                                       m_responseFilter,
292                                       m_out,
293                                       m_logger,
294                                       connectionDetails,
295                                       true,
296                                       100000);
297 
298     m_responseFilterStubFactory.assertNoMoreCalls();
299     m_requestFilterStubFactory.assertNoMoreCalls();
300 
301     assertEquals(localEndPoint, engine.getListenEndPoint());
302     assertNotNull(engine.getSocketFactory());
303     m_requestFilterStubFactory.assertIsWrappedBy(engine.getRequestFilter());
304     m_responseFilterStubFactory.assertIsWrappedBy(engine.getResponseFilter());
305     assertEquals(TerminalColour.RED, engine.getRequestColour());
306     assertEquals(TerminalColour.BLUE, engine.getResponseColour());
307 
308     resetLogger();
309     m_requestFilterStubFactory.resetCallHistory();
310     m_responseFilterStubFactory.resetCallHistory();
311 
312     engineTests(engine, connectionDetails);
313   }
314 
315   @Test public void testOutputStreamFilterTeeWithBadFilters() throws Exception {
316 
317     final EndPoint localEndPoint = new EndPoint("localhost", m_localPort);
318 
319     final ConnectionDetails connectionDetails =
320       new ConnectionDetails(localEndPoint,
321                             new EndPoint("bah", 456),
322                             false);
323 
324     final AbstractTCPProxyEngine engine =
325       new PortForwarderTCPProxyEngine(m_requestFilter,
326                                       m_responseFilter,
327                                       m_out,
328                                       m_logger,
329                                       connectionDetails,
330                                       true,
331                                       100000);
332 
333     final AbstractTCPProxyEngine.OutputStreamFilterTee filterTee =
334       engine.new OutputStreamFilterTee(connectionDetails,
335                                        new ByteArrayOutputStream(),
336                                        new BadFilter(),
337                                        TerminalColour.NONE);
338 
339     resetLogger();
340 
341     filterTee.connectionOpened();
342     verify(m_logger).error(contains("Problem"), isA(FilterException.class));
343 
344     filterTee.connectionClosed();
345     verify(m_logger, times(2))
346       .error(contains("Problem"), isA(FilterException.class));
347 
348     filterTee.handle(new byte[0], 0);
349     verify(m_logger, times(3))
350       .error(contains("Problem"), isA(FilterException.class));
351 
352     verifyNoMoreInteractions(m_logger);
353   }
354 
355   private static final class BadFilter implements TCPProxyFilter {
356 
357     public byte[] handle(ConnectionDetails connectionDetails,
358                          byte[] buffer,
359                          int bytesRead)
360       throws FilterException {
361       throw new FilterException("Problem", null);
362     }
363 
364     public void connectionOpened(ConnectionDetails connectionDetails)
365       throws FilterException {
366       throw new FilterException("Problem", null);
367     }
368 
369     public void connectionClosed(ConnectionDetails connectionDetails)
370       throws FilterException {
371       throw new FilterException("Problem", null);
372     }
373   }
374 
375   private static final class AcceptSingleConnectionAndEcho implements Runnable {
376     private final ServerSocket m_serverSocket;
377 
378     public AcceptSingleConnectionAndEcho() throws IOException {
379       m_serverSocket = new ServerSocket(0);
380       new Thread(this, getClass().getName()).start();
381     }
382 
383     public EndPoint getEndPoint() {
384       return EndPoint.serverEndPoint(m_serverSocket);
385     }
386 
387     public void run() {
388       try {
389         final Socket socket = m_serverSocket.accept();
390 
391         new StreamCopier(1000, true).copy(socket.getInputStream(),
392                                           socket.getOutputStream());
393       }
394       catch (IOException e) {
395         System.err.println("Got a " + e.getMessage());
396       }
397     }
398   }
399 }