View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.util;
16  
17  import java.io.CharArrayWriter;
18  import java.io.Closeable;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.net.BindException;
23  import java.net.InetSocketAddress;
24  import java.net.ServerSocket;
25  import java.net.Socket;
26  import java.net.SocketException;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.List;
32  
33  import org.apache.commons.io.IOUtils;
34  import org.apache.commons.lang3.StringUtils;
35  import org.htmlunit.HttpHeader;
36  import org.htmlunit.WebServerTestCase;
37  import org.htmlunit.WebTestCase;
38  
39  /**
40   * A very simple implementation of a Web Server.
41   * This covers some cases which are not possible with Jetty.
42   *
43   * @author Ahmed Ashour
44   * @author Ronald Brill
45   */
46  public class PrimitiveWebServer implements Closeable {
47  
48      private final int port_;
49      private final String firstResponse_;
50      private final String secondResponse_;
51      private final String otherResponse_;
52      private ServerSocket server_;
53      private Charset charset_ = StandardCharsets.ISO_8859_1;
54      private final List<String> requests_ = Collections.synchronizedList(new ArrayList<>());
55  
56      /**
57       * Constructs a new SimpleWebServer.
58       *
59       * @param charset the charset
60       * @param firstResponse the first response, must contain the full response (to start with "HTTP/1.1 200 OK")
61       * @param otherResponse the subsequent response, must contain the full response (to start with "HTTP/1.1 200 OK")
62       * @throws Exception in case of error
63       */
64      public PrimitiveWebServer(final Charset charset, final String firstResponse, final String otherResponse)
65              throws Exception {
66          this(charset, firstResponse, null, otherResponse);
67      }
68  
69      /**
70       * Constructs a new SimpleWebServer.
71       *
72       * @param charset the charset
73       * @param firstResponse the first response, must contain the full response (to start with "HTTP/1.1 200 OK")
74       * @param secondResponse the second response, must contain the full response (to start with "HTTP/1.1 200 OK") or null if not used
75       * @param otherResponse the subsequent response, must contain the full response (to start with "HTTP/1.1 200 OK")
76       * @throws Exception in case of error
77       */
78      public PrimitiveWebServer(final Charset charset, final String firstResponse, final String secondResponse, final String otherResponse)
79              throws Exception {
80          this(WebTestCase.PORT_PRIMITIVE_SERVER, charset, firstResponse, secondResponse, otherResponse);
81      }
82  
83      /**
84       * Constructs a new SimpleWebServer.
85       *
86       * @param port the port
87       * @param charset the charset
88       * @param firstResponse the first response, must contain the full response (to start with "HTTP/1.1 200 OK")
89       * @param secondResponse the second response, must contain the full response (to start with "HTTP/1.1 200 OK") or null if not used
90       * @param otherResponse the subsequent response, must contain the full response (to start with "HTTP/1.1 200 OK")
91       * @throws Exception in case of error
92       */
93      public PrimitiveWebServer(final int port, final Charset charset, final String firstResponse, final String secondResponse, final String otherResponse)
94              throws Exception {
95          port_ = port;
96          firstResponse_ = firstResponse;
97          secondResponse_ = secondResponse;
98          otherResponse_ = otherResponse;
99          if (charset != null) {
100             charset_ = charset;
101         }
102 
103         start();
104     }
105 
106     /**
107      * Starts the server.
108      * @throws Exception if an error occurs
109      */
110     private void start() throws Exception {
111         server_ = new ServerSocket();
112         server_.setReuseAddress(true);
113 
114         final long maxWait = System.currentTimeMillis() + WebServerTestCase.BIND_TIMEOUT;
115 
116         while (true) {
117             try {
118                 server_.bind(new InetSocketAddress(port_));
119                 break;
120             }
121             catch (final BindException e) {
122                 if (System.currentTimeMillis() > maxWait) {
123                     throw (BindException) new BindException("Port " + port_ + " is already in use").initCause(e);
124                 }
125                 Thread.sleep(200);
126             }
127         }
128 
129         new Thread(() -> {
130             int responseCount = 0;
131             try {
132                 while (true) {
133                     try (final Socket socket = server_.accept()) {
134                         final InputStream in = socket.getInputStream();
135                         final CharArrayWriter writer = new CharArrayWriter();
136 
137                         String requestString = writer.toString();
138                         int i;
139 
140                         while ((i = in.read()) != -1) {
141                             writer.append((char) i);
142                             requestString = writer.toString();
143 
144                             if (i == '\n' && requestString.endsWith("\r\n\r\n")) {
145                                 break;
146                             }
147                         }
148                         final int contentLengthPos = StringUtils.indexOfIgnoreCase(requestString, HttpHeader.CONTENT_LENGTH);
149                         if (contentLengthPos > -1) {
150                             final int endPos = requestString.indexOf('\n', contentLengthPos + 16);
151                             final String toParse = requestString.substring(contentLengthPos + 16, endPos);
152                             final int contentLength = Integer.parseInt(toParse.trim());
153 
154                             if (contentLength > 0) {
155                                 final byte[] charArray = new byte[contentLength];
156                                 IOUtils.readFully(in, charArray);
157                                 requestString += new String(charArray);
158                             }
159                         }
160 
161                         final String response;
162                         if (requestString.isEmpty()
163                                 || requestString.contains("/favicon.ico")) {
164                             response = "HTTP/1.1 404 Not Found\r\n"
165                                     + "Content-Length: 0\r\n"
166                                     + "Connection: close\r\n"
167                                     + "\r\n";
168                         }
169                         else {
170                             requests_.add(requestString);
171                             if (responseCount == 0 || otherResponse_ == null) {
172                                 response = firstResponse_;
173                             }
174                             else if (responseCount == 1 && secondResponse_ != null) {
175                                 response = secondResponse_;
176                             }
177                             else {
178                                 response = otherResponse_;
179                             }
180                             responseCount++;
181                         }
182 
183                         final OutputStream out = socket.getOutputStream();
184                         final int headPos = response.indexOf("\r\n\r\n");
185                         out.write(response.substring(0, headPos + 4).getBytes(StandardCharsets.US_ASCII));
186                         out.write(response.substring(headPos + 4).getBytes(charset_));
187                         out.flush();
188                     }
189                 }
190             }
191             catch (final SocketException e) {
192                 // ignore
193             }
194             catch (final Exception e) {
195                 throw new RuntimeException(e);
196             }
197         }).start();
198     }
199 
200     /**
201      * Stops the server.
202      * @throws IOException if an error occurs
203      */
204     @Override
205     public void close() throws IOException {
206         server_.close();
207     }
208 
209     /**
210      * Returns the saved requests.
211      * @return the requests
212      */
213     public List<String> getRequests() {
214         return requests_;
215     }
216 
217     /**
218      * Returns the port.
219      * @return the port
220      */
221     public int getPort() {
222         return port_;
223     }
224 
225     /**
226      * Clears all requests.
227      */
228     public void clearRequests() {
229         requests_.clear();
230     }
231 }