1 /*
2  * Copyright 2019 The Android Open Source Project
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  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.androidx.webkit;
18 
19 import static java.nio.charset.StandardCharsets.UTF_8;
20 
21 import org.jspecify.annotations.NonNull;
22 import org.jspecify.annotations.Nullable;
23 
24 import java.io.BufferedReader;
25 import java.io.BufferedWriter;
26 import java.io.IOException;
27 import java.io.InputStreamReader;
28 import java.io.OutputStreamWriter;
29 import java.net.ServerSocket;
30 import java.net.Socket;
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 /**
35  * Simple HTTP server that accepts requests.
36  * Can be used to test proxying or as a end destination server.
37  */
38 public class HttpServer {
39 
40     /**
41      * This interface offers a callback that is run when a request is served.
42      */
43     public interface OnRequestCallback {
44 
45         /**
46          * Called when this proxy serves a request.
47          */
onRequestServed()48         void onRequestServed();
49     }
50 
51     /**
52      * Provider of request handlers.
53      *
54      * Typically just a static reference to either {@code HttpServer.EchoRequestHandler::new} or
55      * {@code HttpServer.ProxyRequestHandler::new}
56      */
57     public interface RequestHandlerFactory {
58         /**
59          * Called to initialize a new RequestHandler
60          */
create(@onNull Socket socket)61         @NonNull RequestHandler create(@NonNull Socket socket);
62     }
63 
64     private static final int TIMEOUT_MILLIS = 5000;
65     private boolean mRunning = false;
66     private OnRequestCallback mCallback;
67     private RequestHandlerFactory mHandlerFactory;
68     private ServerSocket mServerSocket;
69     private List<Thread> mThreadsList;
70     private int mRequestCount;
71 
72     /**
73      * Create a server using provided port number.
74      *
75      * @param port port number
76      * @param handlerFactory Provider of request handler
77      * @param callback callback run when this a request is served
78      */
79     @SuppressWarnings("CatchAndPrintStackTrace")
HttpServer(int port, @NonNull RequestHandlerFactory handlerFactory, @Nullable OnRequestCallback callback)80     public HttpServer(int port,
81             @NonNull RequestHandlerFactory handlerFactory,
82             @Nullable OnRequestCallback callback) {
83         mRequestCount = 0;
84         mCallback = callback;
85         mHandlerFactory = handlerFactory;
86         mThreadsList = new ArrayList<>();
87         try {
88             mServerSocket = new ServerSocket(port);
89         } catch (IOException e) {
90             e.printStackTrace();
91         }
92     }
93 
94     /**
95      * Get port number.
96      */
getPort()97     public int getPort() {
98         return mServerSocket.getLocalPort();
99     }
100 
101     /**
102      * Get the number of requests this server has served.
103      */
getRequestCount()104     public int getRequestCount() {
105         return mRequestCount;
106     }
107 
108     /**
109      * Start listening for requests.
110      */
start()111     public void start() {
112         if (mRunning) return;
113         mRunning = true;
114         new Thread(() -> {
115             while (mRunning) {
116                 listen();
117             }
118         }).start();
119     }
120 
121     @SuppressWarnings("CatchAndPrintStackTrace")
listen()122     private void listen() {
123         try {
124             Socket socket = mServerSocket.accept();
125             mRequestCount++;
126             if (mCallback != null) {
127                 mCallback.onRequestServed();
128             }
129             Thread thread = new Thread(mHandlerFactory.create(socket));
130             mThreadsList.add(thread);
131             thread.start();
132         } catch (IOException e) {
133             e.printStackTrace();
134         }
135     }
136 
137     /**
138      * Shutdown.
139      */
140     @SuppressWarnings("CatchAndPrintStackTrace")
shutdown()141     public void shutdown() {
142         if (!mRunning) return;
143         mRunning = false;
144         for (Thread thread : mThreadsList) {
145             if (thread.isAlive()) {
146                 try {
147                     thread.join(TIMEOUT_MILLIS);
148                 } catch (InterruptedException e) {
149                     e.printStackTrace();
150                 }
151             }
152         }
153         mThreadsList.clear();
154         try {
155             mServerSocket.close();
156         } catch (IOException e) {
157             e.printStackTrace();
158         }
159     }
160 
161     /**
162      * Request handler whose response can be controlled through the
163      * {@link #writeResponse(String, BufferedWriter)} method.
164      */
165     abstract static class RequestHandler implements Runnable {
166         private Socket mSocket;
167         private BufferedReader mReader;
168         private BufferedWriter mWriter;
169 
170         @SuppressWarnings("CatchAndPrintStackTrace")
RequestHandler(Socket socket)171         RequestHandler(Socket socket) {
172             mSocket = socket;
173             try {
174                 mSocket.setSoTimeout(TIMEOUT_MILLIS);
175                 mReader = new BufferedReader(
176                         new InputStreamReader(mSocket.getInputStream(), UTF_8));
177                 mWriter = new BufferedWriter(
178                         new OutputStreamWriter(mSocket.getOutputStream(), UTF_8));
179             } catch (IOException e) {
180                 e.printStackTrace();
181             }
182         }
183 
184         @Override
185         @SuppressWarnings("CatchAndPrintStackTrace")
run()186         public void run() {
187             try {
188                 StringBuilder sb = new StringBuilder();
189                 String s = mReader.readLine();
190                 while (s != null && !s.trim().isEmpty()) {
191                     sb.append(s);
192                     sb.append("\n");
193                     s = mReader.readLine();
194                 }
195                 String request = sb.toString();
196                 writeResponse(request, mWriter);
197             } catch (IOException e) {
198                 e.printStackTrace();
199             }
200         }
201 
writeResponse(String request, BufferedWriter responseWriter)202         abstract void writeResponse(String request, BufferedWriter responseWriter)
203                 throws IOException;
204     }
205 
206     /**
207      * Request handler that responds with a HTML document containing the received request header.
208      */
209     static class EchoRequestHandler extends RequestHandler {
EchoRequestHandler(Socket socket)210         EchoRequestHandler(Socket socket) {
211             super(socket);
212         }
213 
214         @Override
writeResponse(String request, BufferedWriter responseWriter)215         void writeResponse(String request, BufferedWriter responseWriter) throws IOException {
216             responseWriter.write("HTTP/1.0 200 OK\nContent-Type: text/html\n\r\n");
217             responseWriter.write("<html><head><title>HttpServer</title></head>"
218                     + "<body>Server handled this request:<br><pre>"
219                     + request + "</pre></body></html>");
220             responseWriter.flush();
221         }
222     }
223 
224     /**
225      * Respond with text that indicates that the request was handled by a proxy.
226      */
227     static class ProxyRequestHandler extends RequestHandler {
228 
ProxyRequestHandler(Socket socket)229         ProxyRequestHandler(Socket socket) {
230             super(socket);
231         }
232 
233         @Override
writeResponse(String request, BufferedWriter responseWriter)234         void writeResponse(String request, BufferedWriter responseWriter) throws IOException {
235             responseWriter.write("HTTP/1.0 200 OK\nUser-Agent: Proxy\n\r\n");
236             responseWriter.write("<html><head><title>Proxy</title></head>"
237                     + "<body>Proxy handled this request:<br><br>"
238                     + request + "</body></html>");
239             responseWriter.flush();
240         }
241     }
242 }
243