View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import java.io.BufferedReader;
18  import java.io.Closeable;
19  import java.io.IOException;
20  import java.io.InputStreamReader;
21  import java.io.OutputStream;
22  import java.io.PrintWriter;
23  import java.net.BindException;
24  import java.net.MalformedURLException;
25  import java.net.ServerSocket;
26  import java.net.Socket;
27  import java.net.SocketException;
28  import java.net.URL;
29  import java.nio.CharBuffer;
30  import java.nio.charset.StandardCharsets;
31  import java.util.HashSet;
32  import java.util.Set;
33  import java.util.concurrent.atomic.AtomicBoolean;
34  
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.htmlunit.MockWebConnection.RawResponseData;
38  import org.htmlunit.util.NameValuePair;
39  
40  /**
41   * Mini server simulating some not standard behaviors.
42   *
43   * @author Marc Guillemot
44   * @author Frank Danek
45   * @author Ronald Brill
46   * @author Sven Strickroth
47   */
48  public class MiniServer extends Thread implements Closeable {
49      private static final Log LOG = LogFactory.getLog(MiniServer.class);
50  
51      private final int port_;
52      private volatile boolean shutdown_ = false;
53      private final AtomicBoolean started_ = new AtomicBoolean(false);
54      private final MockWebConnection mockWebConnection_;
55      private volatile ServerSocket serverSocket_;
56      private String lastRequest_;
57  
58      private static final Set<URL> DROP_REQUESTS = new HashSet<>();
59      private static final Set<URL> DROP_GET_REQUESTS = new HashSet<>();
60  
61      /**
62       * Resets the drop and drop-get request counters.
63       */
64      public static void resetDropRequests() {
65          DROP_REQUESTS.clear();
66          DROP_GET_REQUESTS.clear();
67      }
68  
69      /**
70       * Add the given url to the list of drop requests.
71       * @param url to url to add
72       */
73      public static void configureDropRequest(final URL url) {
74          DROP_REQUESTS.add(url);
75      }
76  
77      /**
78       * Add the given url to the list of drop-get requests.
79       * @param url to url to add
80       */
81      public static void configureDropGetRequest(final URL url) {
82          DROP_GET_REQUESTS.add(url);
83      }
84  
85      /**
86       * Ctor.
87       * @param port the port to listen on
88       * @param mockWebConnection the {@link MockWebConnection} to get the responses from
89       */
90      public MiniServer(final int port, final MockWebConnection mockWebConnection) {
91          port_ = port;
92          mockWebConnection_ = mockWebConnection;
93          setDaemon(true);
94      }
95  
96      @Override
97      public void run() {
98          try {
99              final long maxWait = System.currentTimeMillis() + WebServerTestCase.BIND_TIMEOUT;
100             while (true) {
101                 try {
102                     serverSocket_ = new ServerSocket(port_);
103                     break;
104                 }
105                 catch (final BindException e) {
106                     if (System.currentTimeMillis() > maxWait) {
107                         throw (BindException) new BindException("Port " + port_ + " is already in use").initCause(e);
108                     }
109                     try {
110                         Thread.sleep(200);
111                     }
112                     catch (final InterruptedException ex) {
113                         LOG.error(ex.getMessage(), ex);
114                     }
115                 }
116             }
117 
118             started_.set(true);
119             LOG.info("Starting listening on port " + port_);
120             while (!shutdown_) {
121                 try (Socket s = serverSocket_.accept()) {
122                     try (BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()))) {
123 
124                         final CharBuffer cb = CharBuffer.allocate(5000);
125                         br.read(cb);
126                         cb.flip();
127                         final String in = cb.toString();
128                         cb.rewind();
129 
130                         RawResponseData responseData = null;
131                         final WebRequest request = parseRequest(in);
132 
133                         // try to get the data to count the request
134                         try {
135                             if (request != null) {
136                                 responseData = mockWebConnection_.getRawResponse(request);
137                             }
138                         }
139                         catch (final IllegalStateException e) {
140                             LOG.error(e);
141                         }
142 
143                         if (request == null
144                                 || (DROP_REQUESTS.contains(request.getUrl())
145                                         || (request.getHttpMethod() == HttpMethod.GET
146                                                 && DROP_GET_REQUESTS.contains(request.getUrl())))) {
147                             responseData = null;
148                         }
149 
150                         if (responseData == null) {
151                             LOG.info("Closing impolitely in & output streams");
152                             s.getOutputStream().close();
153                         }
154                         else if (responseData.getByteContent() != null) {
155                             try (OutputStream os = s.getOutputStream()) {
156                                 os.write(("HTTP/1.0 " + responseData.getStatusCode() + " "
157                                         + responseData.getStatusMessage())
158                                         .getBytes(StandardCharsets.US_ASCII));
159                                 os.write("\n".getBytes(StandardCharsets.US_ASCII));
160                                 for (final NameValuePair header : responseData.getHeaders()) {
161                                     os.write((header.getName() + ": "
162                                                 + header.getValue()).getBytes(StandardCharsets.US_ASCII));
163                                     os.write("\n".getBytes(StandardCharsets.US_ASCII));
164                                 }
165                                 os.write("\n".getBytes(StandardCharsets.US_ASCII));
166                                 os.write(responseData.getByteContent(), 0, responseData.getByteContent().length);
167                                 // bytes and no content length - don't attach anything
168                                 os.flush();
169                             }
170                         }
171                         else {
172                             try (PrintWriter pw = new PrintWriter(s.getOutputStream())) {
173                                 pw.println("HTTP/1.0 " + responseData.getStatusCode() + " "
174                                         + responseData.getStatusMessage());
175                                 for (final NameValuePair header : responseData.getHeaders()) {
176                                     pw.println(header.getName() + ": " + header.getValue());
177                                 }
178                                 pw.println();
179                                 pw.println(responseData.getStringContent());
180                                 pw.println();
181                                 pw.flush();
182                             }
183                         }
184                     }
185                 }
186             }
187         }
188         catch (final SocketException e) {
189             if (!shutdown_) {
190                 LOG.error(e);
191             }
192         }
193         catch (final IOException e) {
194             LOG.error(e);
195         }
196         finally {
197             LOG.info("Finished listening on port " + port_);
198         }
199     }
200 
201     private WebRequest parseRequest(final String request) {
202         final int firstSpace = request.indexOf(' ');
203         final int secondSpace = request.indexOf(' ', firstSpace + 1);
204 
205         HttpMethod submitMethod = HttpMethod.GET;
206         final String methodText = request.substring(0, firstSpace);
207         if ("OPTIONS".equalsIgnoreCase(methodText)) {
208             submitMethod = HttpMethod.OPTIONS;
209         }
210         else if ("POST".equalsIgnoreCase(methodText)) {
211             submitMethod = HttpMethod.POST;
212         }
213 
214         final String requestedPath = request.substring(firstSpace + 1, secondSpace);
215         if ("/favicon.ico".equals(requestedPath)) {
216             LOG.debug("Skipping /favicon.ico");
217             return null;
218         }
219         try {
220             final URL url = new URL("http://localhost:" + port_ + requestedPath);
221             lastRequest_ = request;
222             return new WebRequest(url, submitMethod);
223         }
224         catch (final MalformedURLException e) {
225             LOG.error(e);
226             return null;
227         }
228     }
229 
230     /**
231      * @return the last received request
232      */
233     public String getLastRequest() {
234         return lastRequest_;
235     }
236 
237     /**
238      * ShutDown this server.
239      * @throws InterruptedException in case of error
240      * @throws IOException in case of error
241      */
242     @Override
243     public void close() throws IOException {
244         shutdown_ = true;
245         if (serverSocket_ != null) {
246             serverSocket_.close();
247         }
248         interrupt();
249         try {
250             join(5000);
251         }
252         catch (final InterruptedException e) {
253             throw new IOException("MoniServer join() failed", e);
254         }
255     }
256 
257     @Override
258     public synchronized void start() {
259         super.start();
260 
261         // wait until the listener on the port has been started to be sure
262         // that the main thread doesn't perform the first request before the listener is ready
263         for (int i = 0; i < 10; i++) {
264             if (!started_.get()) {
265                 try {
266                     Thread.sleep(100);
267                 }
268                 catch (final InterruptedException e) {
269                     throw new RuntimeException(e);
270                 }
271             }
272         }
273     }
274 }