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