1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.testing; 17 18 import android.net.Uri; 19 import android.util.Log; 20 import com.google.common.base.Optional; 21 import java.io.File; 22 import java.io.IOException; 23 import java.net.InetAddress; 24 import java.net.ServerSocket; 25 import java.net.Socket; 26 import java.util.concurrent.atomic.AtomicBoolean; 27 import org.apache.http.Header; 28 import org.apache.http.HttpException; 29 import org.apache.http.HttpRequest; 30 import org.apache.http.HttpResponse; 31 import org.apache.http.HttpStatus; 32 import org.apache.http.entity.FileEntity; 33 import org.apache.http.impl.DefaultConnectionReuseStrategy; 34 import org.apache.http.impl.DefaultHttpResponseFactory; 35 import org.apache.http.impl.DefaultHttpServerConnection; 36 import org.apache.http.params.BasicHttpParams; 37 import org.apache.http.params.CoreConnectionPNames; 38 import org.apache.http.params.HttpParams; 39 import org.apache.http.protocol.BasicHttpContext; 40 import org.apache.http.protocol.BasicHttpProcessor; 41 import org.apache.http.protocol.HttpContext; 42 import org.apache.http.protocol.HttpRequestHandler; 43 import org.apache.http.protocol.HttpRequestHandlerRegistry; 44 import org.apache.http.protocol.HttpService; 45 46 /** TestHttpServer is a simple http server that listens to http requests on a single thread. */ 47 public final class TestHttpServer { 48 49 private static final String TAG = "TestHttpServer"; 50 private static final String TEST_HOST = "localhost"; 51 52 private static final String HEAD_REQUEST_METHOD = "HEAD"; 53 private static final String ETAG_HEADER = "ETag"; 54 private static final String IF_NONE_MATCH_HEADER = "If-None-Match"; 55 private static final String BINARY_CONTENT_TYPE = "application/binary"; 56 private static final String PROTO_CONTENT_TYPE = "application/x-protobuf"; 57 private static final String TEXT_CONTENT_TYPE = "text/plain"; 58 59 private final HttpParams httpParams = new BasicHttpParams(); 60 private final HttpService httpService; 61 private final HttpRequestHandlerRegistry registry; 62 private final AtomicBoolean finished = new AtomicBoolean(); 63 64 private Thread serverThread; 65 private ServerSocket serverSocket; 66 // 0 means user didn't specify a port number and will use automatically assigned port. 67 private final int userDesignatedPort; 68 TestHttpServer()69 public TestHttpServer() { 70 this(0); 71 } 72 TestHttpServer(int portNumber)73 public TestHttpServer(int portNumber) { 74 userDesignatedPort = portNumber; 75 httpParams.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true); 76 registry = new HttpRequestHandlerRegistry(); 77 78 httpService = 79 new HttpService( 80 new BasicHttpProcessor(), 81 new DefaultConnectionReuseStrategy(), 82 new DefaultHttpResponseFactory()); 83 httpService.setHandlerResolver(registry); 84 httpService.setParams(httpParams); 85 } 86 /** Registers a handler for an endpoint pattern. */ registerHandler(String pattern, HttpRequestHandler handler)87 public void registerHandler(String pattern, HttpRequestHandler handler) { 88 registry.register(pattern, handler); 89 } 90 91 /** Registers a handler that binds onto a text file for an endpoint pattern. */ registerTextFile(String pattern, String filepath)92 public void registerTextFile(String pattern, String filepath) { 93 registerFile(pattern, filepath, TEXT_CONTENT_TYPE, /* eTagOptional= */ Optional.absent()); 94 } 95 96 /** Registers a handler that binds onto a file for an endpoint pattern. */ registerBinaryFile(String pattern, String filepath)97 public void registerBinaryFile(String pattern, String filepath) { 98 registerFile(pattern, filepath, BINARY_CONTENT_TYPE, /* eTagOptional= */ Optional.absent()); 99 } 100 101 /** 102 * Registers a handler that binds onto a proto file for an endpoint pattern with the specified 103 * ETag. 104 */ registerProtoFileWithETag(String pattern, String filepath, String eTag)105 public void registerProtoFileWithETag(String pattern, String filepath, String eTag) { 106 registerFile(pattern, filepath, PROTO_CONTENT_TYPE, Optional.of(eTag)); 107 } 108 registerFile( String pattern, String filepath, String contentType, Optional<String> eTagOptional)109 private void registerFile( 110 String pattern, String filepath, String contentType, Optional<String> eTagOptional) { 111 registerHandler( 112 pattern, 113 (httpRequest, httpResponse, httpContext) -> { 114 if (eTagOptional.isPresent()) { 115 String eTag = eTagOptional.get(); 116 httpResponse.addHeader(ETAG_HEADER, eTag); 117 setHttpStatusCode(httpRequest, httpResponse, eTag); 118 if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED 119 || HEAD_REQUEST_METHOD.equals(httpRequest.getRequestLine().getMethod())) { 120 return; 121 } 122 } else { // The ETag is not present. 123 httpResponse.setStatusCode(HttpStatus.SC_OK); 124 } 125 File file = new File(filepath); 126 httpResponse.setEntity(new FileEntity(file, contentType)); 127 }); 128 } 129 130 /** Starts the test http server and returns the prefix of the test url. */ startServer()131 public Uri.Builder startServer() throws IOException { 132 serverSocket = 133 new ServerSocket( 134 /* port= */ userDesignatedPort, /* backlog= */ 0, InetAddress.getByName(TEST_HOST)); 135 serverThread = 136 new Thread( 137 () -> { 138 try { 139 while (!finished.get()) { 140 Socket socket = serverSocket.accept(); 141 handleRequest(socket); 142 } 143 } catch (IOException e) { 144 Log.e(TAG, "Exception: " + e); 145 } 146 }); 147 serverThread.start(); 148 return getTestUrlPrefix(); 149 } 150 stopServer()151 public void stopServer() { 152 try { 153 finished.set(true); 154 serverSocket.close(); 155 serverThread.join(); 156 } catch (IOException | InterruptedException e) { 157 Log.e(TAG, "Exception when stopping server: " + e); 158 } 159 } 160 handleRequest(Socket socket)161 private void handleRequest(Socket socket) { 162 DefaultHttpServerConnection connection = new DefaultHttpServerConnection(); 163 try { 164 connection.bind(socket, httpParams); 165 HttpContext httpContext = new BasicHttpContext(); 166 httpService.handleRequest(connection, httpContext); 167 } catch (IOException | HttpException e) { 168 Log.e(TAG, "Unexpected exception while processing request " + e); 169 } finally { 170 try { 171 connection.shutdown(); 172 } catch (IOException e) { 173 // Ignore. 174 } 175 } 176 } 177 getTestUrlPrefix()178 private Uri.Builder getTestUrlPrefix() { 179 String authority = TEST_HOST + ":" + serverSocket.getLocalPort(); 180 return new Uri.Builder().scheme("http").encodedAuthority(authority); 181 } 182 setHttpStatusCode( HttpRequest httpRequest, HttpResponse httpResponse, String eTag)183 private static void setHttpStatusCode( 184 HttpRequest httpRequest, HttpResponse httpResponse, String eTag) { 185 Header[] headers = httpRequest.getAllHeaders(); 186 // We use `If-None-Match` header and ETag to detect whether the file has been changed since the 187 // last sync. If the ETag from client matches the one at server, the file is not changed and 188 // HttpStatus.SC_NOT_MODIFIED is returned; otherwise, the file is changed and HttpStatus.SC_OK 189 // is returned. 190 for (Header header : headers) { 191 // Find the `If-None-Match` header. 192 if (!IF_NONE_MATCH_HEADER.equals(header.getName())) { 193 continue; 194 } 195 httpResponse.setStatusCode( 196 eTag.equals(header.getValue()) ? HttpStatus.SC_NOT_MODIFIED : HttpStatus.SC_OK); 197 return; 198 } 199 httpResponse.setStatusCode(HttpStatus.SC_OK); 200 } 201 } 202