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