• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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