Autopsy  4.21.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
CTCloudHttpClient.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2023 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package com.basistech.df.cybertriage.autopsy.ctapi;
20 
24 import com.fasterxml.jackson.databind.ObjectMapper;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.net.Proxy;
28 import java.net.ProxySelector;
29 import java.net.SocketAddress;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.security.KeyManagementException;
33 import java.security.KeyStore;
34 import java.security.KeyStoreException;
35 import java.security.NoSuchAlgorithmException;
36 import java.security.SecureRandom;
37 import java.security.UnrecoverableKeyException;
38 import java.text.MessageFormat;
39 import java.util.Collection;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Map.Entry;
44 import java.util.logging.Level;
45 import java.util.stream.Stream;
46 import javax.net.ssl.KeyManager;
47 import javax.net.ssl.KeyManagerFactory;
48 import javax.net.ssl.SSLContext;
49 import javax.net.ssl.TrustManager;
50 import javax.net.ssl.TrustManagerFactory;
51 import javax.net.ssl.X509TrustManager;
52 import org.apache.commons.collections4.MapUtils;
53 import org.apache.commons.lang.ArrayUtils;
54 import org.apache.commons.lang3.StringUtils;
55 import org.apache.http.HttpEntity;
56 import org.apache.http.HttpStatus;
57 import org.apache.http.client.config.RequestConfig;
58 import org.apache.http.client.methods.CloseableHttpResponse;
59 import org.apache.http.client.methods.HttpPost;
60 import org.apache.http.client.methods.HttpPut;
61 import org.apache.http.client.methods.HttpRequestBase;
62 import org.apache.http.impl.client.CloseableHttpClient;
63 import org.apache.http.util.EntityUtils;
64 import org.apache.http.client.utils.URIBuilder;
65 import org.apache.http.entity.ContentType;
66 import org.apache.http.entity.InputStreamEntity;
67 import org.apache.http.entity.StringEntity;
69 import org.apache.http.impl.client.HttpClientBuilder;
70 import org.apache.http.impl.client.HttpClients;
71 import org.apache.http.impl.client.WinHttpClients;
72 import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
73 import org.apache.http.ssl.SSLInitializationException;
74 import org.netbeans.core.ProxySettings;
75 import org.openide.util.Lookup;
77 
87 class CTCloudHttpClient {
88 
89  private static final Logger LOGGER = Logger.getLogger(CTCloudHttpClient.class.getName());
90  private static final String HOST_URL = Version.getBuildType() == Version.Type.RELEASE ? Constants.CT_CLOUD_SERVER : Constants.CT_CLOUD_DEV_SERVER;
91  private static final String NB_PROXY_SELECTOR_NAME = "org.netbeans.core.NbProxySelector";
92 
93  private static final int CONNECTION_TIMEOUT_MS = 58 * 1000; // milli sec
94 
95  private static final CTCloudHttpClient instance = new CTCloudHttpClient();
96 
97  public static CTCloudHttpClient getInstance() {
98  return instance;
99  }
100 
101  private final ObjectMapper mapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper();
102  private final SSLContext sslContext;
103  private final ProxySelector proxySelector;
104 
105  private CTCloudHttpClient() {
106  // leave as null for now unless we want to customize this at a later date
107  this.sslContext = createSSLContext();
108  this.proxySelector = getProxySelector();
109  }
110 
111  private static URI getUri(String host, String path, Map<String, String> urlReqParams) throws URISyntaxException {
112  String url = host + path;
113  URIBuilder builder = new URIBuilder(url);
114 
115  if (!MapUtils.isEmpty(urlReqParams)) {
116  for (Entry<String, String> e : urlReqParams.entrySet()) {
117  String key = e.getKey();
118  String value = e.getValue();
119  if (StringUtils.isNotBlank(key) || StringUtils.isNotBlank(value)) {
120  builder.addParameter(key, value);
121  }
122  }
123  }
124 
125  return builder.build();
126  }
127 
128  public <O> O doPost(String urlPath, Object jsonBody, Class<O> classType) throws CTCloudException {
129  return doPost(urlPath, Collections.emptyMap(), jsonBody, classType);
130  }
131 
132  public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jsonBody, Class<O> classType) throws CTCloudException {
133 
134  URI postURI = null;
135  try {
136  postURI = getUri(HOST_URL, urlPath, urlReqParams);
137  LOGGER.log(Level.INFO, "initiating http connection to ctcloud server");
138  try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) {
139 
140  HttpPost postRequest = new HttpPost(postURI);
141 
142  configureRequestTimeout(postRequest);
143  postRequest.setHeader("Content-type", "application/json");
144 
145  if (jsonBody != null) {
146  String requestBody = mapper.writeValueAsString(jsonBody);
147  if (StringUtils.isNotBlank(requestBody)) {
148  HttpEntity entity = new StringEntity(requestBody, "UTF-8");
149  postRequest.setEntity(entity);
150  }
151  }
152 
153  LOGGER.log(Level.INFO, "initiating http post request to ctcloud server " + postRequest.getURI());
154  try (CloseableHttpResponse response = httpclient.execute(postRequest)) {
155 
156  if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
157  LOGGER.log(Level.INFO, "Response Received. - Status OK");
158  // Parse Response
159  if (classType != null) {
160  HttpEntity entity = response.getEntity();
161  if (entity != null) {
162  String entityStr = EntityUtils.toString(entity);
163  if (StringUtils.isNotBlank(entityStr)) {
164  O respObj = mapper.readValue(entityStr, classType);
165  return respObj;
166  }
167  }
168  }
169 
170  return null;
171  } else {
172  LOGGER.log(Level.WARNING, "Response Received. - Status Error {}", response.getStatusLine());
173  handleNonOKResponse(response, "");
174  }
175  // transform all non-CTCloudException's into a CTCloudException
176  } catch (CTCloudException ex) {
177  throw ex;
178  } catch (Exception ex) {
179  LOGGER.log(Level.WARNING, "Error when parsing response from CyberTriage Cloud", ex);
180  throw new CTCloudException(CTCloudException.parseUnknownException(ex), ex);
181  }
182  }
183  } catch (IOException ex) {
184  LOGGER.log(Level.WARNING, "IO Exception raised when connecting to CT Cloud using " + postURI, ex);
185  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex);
186  } catch (SSLInitializationException ex) {
187  LOGGER.log(Level.WARNING, "No such algorithm exception raised when creating SSL connection for CT Cloud using " + postURI, ex);
188  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex);
189  } catch (URISyntaxException ex) {
190  LOGGER.log(Level.WARNING, "Wrong URL syntax for CT Cloud " + postURI, ex);
191  throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex);
192  }
193 
194  return null;
195  }
196 
197  public void doFileUploadPut(FileUploadRequest fileUploadRequest) throws CTCloudException {
198  if (fileUploadRequest == null) {
199  throw new CTCloudException(ErrorCode.BAD_REQUEST, new IllegalArgumentException("fileUploadRequest cannot be null"));
200  }
201 
202  String fullUrlPath = fileUploadRequest.getFullUrlPath();
203  String fileName = fileUploadRequest.getFileName();
204  InputStream fileInputStream = fileUploadRequest.getFileInputStream();
205  Long contentLength = fileUploadRequest.getContentLength();
206 
207  if (StringUtils.isBlank(fullUrlPath) || fileInputStream == null || contentLength == null || contentLength <= 0) {
208  throw new CTCloudException(ErrorCode.BAD_REQUEST, new IllegalArgumentException("fullUrlPath, fileInputStream, contentLength must not be empty, null or less than 0"));
209  }
210 
211  URI putUri;
212  try {
213  putUri = new URI(fullUrlPath);
214  } catch (URISyntaxException ex) {
215  LOGGER.log(Level.WARNING, "Wrong URL syntax for CT Cloud " + fullUrlPath, ex);
216  throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex);
217  }
218 
219  try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) {
220  LOGGER.log(Level.INFO, "initiating http post request to ctcloud server " + fullUrlPath);
221  HttpPut put = new HttpPut(putUri);
222  configureRequestTimeout(put);
223 
224  put.addHeader("Connection", "keep-alive");
225  put.setEntity(new InputStreamEntity(fileInputStream, contentLength, ContentType.APPLICATION_OCTET_STREAM));
226 
227  try (CloseableHttpResponse response = httpclient.execute(put)) {
228  int statusCode = response.getStatusLine().getStatusCode();
229  if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
230  LOGGER.log(Level.INFO, "Response Received. - Status OK");
231  } else {
232  LOGGER.log(Level.WARNING, MessageFormat.format("Response Received. - Status Error {0}", response.getStatusLine()));
233  handleNonOKResponse(response, fileName);
234  }
235  }
236  } catch (SSLInitializationException ex) {
237  LOGGER.log(Level.WARNING, "SSL exception raised when connecting to Reversing Labs for file content upload ", ex);
238  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex);
239  } catch (IOException ex) {
240  LOGGER.log(Level.WARNING, "IO Exception raised when connecting to Reversing Labs for file content upload ", ex);
241  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex);
242  }
243  }
244 
254  private void handleNonOKResponse(CloseableHttpResponse response, String fileName) throws CTCloudException, IOException {
255  LOGGER.log(Level.WARNING, MessageFormat.format(
256  "Response code {0}. Message Body {1}",
257  response.getStatusLine().getStatusCode(),
258  EntityUtils.toString(response.getEntity())));
259 
260  switch (response.getStatusLine().getStatusCode()) {
261 
262  case HttpStatus.SC_BAD_REQUEST:
263  //400: Bad request => Unsupported HTTP method or invalid http request (e.g., empty body).
264  throw new CTCloudException(CTCloudException.ErrorCode.BAD_REQUEST);
265  case HttpStatus.SC_UNAUTHORIZED:
266  //401 Invalid API key => An invalid API key, or no API key, has been provided
267  throw new CTCloudException(CTCloudException.ErrorCode.INVALID_KEY);
268  case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
269  // 407 Proxy server authentication required.
270  throw new CTCloudException(CTCloudException.ErrorCode.PROXY_UNAUTHORIZED);
271  case HttpStatus.SC_FORBIDDEN:
272  throw new CTCloudException(CTCloudException.ErrorCode.UN_AUTHORIZED);
273  case HttpStatus.SC_INTERNAL_SERVER_ERROR:
274  //500 Internal error Server temporarily unavailable; please try again later. If the issue persists, please contact RL.
275  throw new CTCloudException(CTCloudException.ErrorCode.TEMP_UNAVAILABLE);
276  case HttpStatus.SC_SERVICE_UNAVAILABLE:
277  //503 Server is too busy. Try again later.
278  //503 Failed to request scan. Try again later. The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. If the issue persists, please contact RL.
279  throw new CTCloudException(CTCloudException.ErrorCode.TEMP_UNAVAILABLE);
280  case HttpStatus.SC_GATEWAY_TIMEOUT:
281  throw new CTCloudException(CTCloudException.ErrorCode.GATEWAY_TIMEOUT);
282  default:
283  String returnData = EntityUtils.toString(response.getEntity());
284  LOGGER.log(Level.WARNING, MessageFormat.format("upload response content for {0}:\n {1}", fileName, returnData));
285  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR);
286  }
287  }
288 
297  private void configureRequestTimeout(HttpRequestBase request) {
298  RequestConfig config = RequestConfig.custom()
299  .setConnectionRequestTimeout(CONNECTION_TIMEOUT_MS)
300  .setConnectTimeout(CONNECTION_TIMEOUT_MS)
301  .setSocketTimeout(CONNECTION_TIMEOUT_MS)
302  .build();
303  request.setConfig(config);
304  }
305 
311  private static ProxySelector getProxySelector() {
312  Collection<? extends ProxySelector> selectors = Lookup.getDefault().lookupAll(ProxySelector.class);
313  return (selectors != null ? selectors.stream() : Stream.empty())
314  .filter(s -> s != null)
315  .map(s -> (ProxySelector) s)
316  .sorted((a, b) -> {
317  String aName = a.getClass().getCanonicalName();
318  String bName = b.getClass().getCanonicalName();
319  boolean aIsNb = aName.equalsIgnoreCase(NB_PROXY_SELECTOR_NAME);
320  boolean bIsNb = bName.equalsIgnoreCase(NB_PROXY_SELECTOR_NAME);
321  if (aIsNb == bIsNb) {
322  return StringUtils.compareIgnoreCase(aName, bName);
323  } else {
324  return aIsNb ? -1 : 1;
325  }
326  })
327  .findFirst()
328  // TODO take this out to remove proxy selector logging
329  .map(s -> new LoggingProxySelector(s))
330  .orElse(null);
331  }
332 
338  private static SSLContext createSSLContext() {
339  LOGGER.log(Level.INFO, "Creating custom SSL context");
340  try {
341 
342  // I'm not sure how much of this is really necessary to set up, but it works
343  SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
344  KeyManager[] keyManagers = getKeyManagers();
345  TrustManager[] trustManagers = getTrustManagers();
346  sslContext.init(keyManagers, trustManagers, new SecureRandom());
347  return sslContext;
348  } catch (NoSuchAlgorithmException | KeyManagementException ex) {
349  LOGGER.log(Level.SEVERE, "Error creating SSL context", ex);
350  return null;
351  }
352  }
353 
354  // jvm default key manager
355  // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909
356  private static KeyManager[] getKeyManagers() {
357  LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + KeyManagerFactory.getDefaultAlgorithm());
358  try {
359  KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
360  kmf.init(null, null);
361  return kmf.getKeyManagers();
362  } catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException ex) {
363  LOGGER.log(Level.SEVERE, "Error getting KeyManagers", ex);
364  return new KeyManager[0];
365  }
366 
367  }
368 
369  // jvm default trust store
370  // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909
371  private static TrustManager[] getTrustManagers() {
372  try {
373  LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + TrustManagerFactory.getDefaultAlgorithm());
374  TrustManagerFactory tmf
375  = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
376  tmf.init((KeyStore) null);
377  X509TrustManager tm = (X509TrustManager) tmf.getTrustManagers()[0];
378 
379  return new TrustManager[]{tm};
380  } catch (KeyStoreException | NoSuchAlgorithmException ex) {
381  LOGGER.log(Level.SEVERE, "Error getting TrustManager", ex);
382  return new TrustManager[0];
383  }
384  }
385 
393  private static CloseableHttpClient createConnection(ProxySelector proxySelector, SSLContext sslContext) throws SSLInitializationException {
394  HttpClientBuilder builder;
395 
396  if (ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION
397  && StringUtils.isBlank(ProxySettings.getAuthenticationUsername())
398  && ArrayUtils.isEmpty(ProxySettings.getAuthenticationPassword())
399  && WinHttpClients.isWinAuthAvailable()) {
400 
401  builder = WinHttpClients.custom();
402  builder.useSystemProperties();
403  LOGGER.log(Level.WARNING, "Using Win HTTP Client");
404  } else {
405  builder = HttpClients.custom();
406  // builder.setDefaultRequestConfig(config);
407  LOGGER.log(Level.WARNING, "Using default http client");
408  }
409 
410  if (sslContext != null) {
411  builder.setSSLContext(sslContext);
412  }
413 
414  if (proxySelector != null) {
415  builder.setRoutePlanner(new SystemDefaultRoutePlanner(proxySelector));
416  }
417 
418  return builder.build();
419  }
420 
421  private static class LoggingProxySelector extends ProxySelector {
422 
423  private final ProxySelector delegate;
424 
425  public LoggingProxySelector(ProxySelector delegate) {
426  this.delegate = delegate;
427  }
428 
429  @Override
430  public List<Proxy> select(URI uri) {
431  List<Proxy> selectedProxies = delegate.select(uri);
432  LOGGER.log(Level.INFO, MessageFormat.format("Proxy selected for {0} are {1}", uri, selectedProxies));
433  return selectedProxies;
434  }
435 
436  @Override
437  public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
438  LOGGER.log(Level.WARNING, MessageFormat.format("Connection failed connecting to {0} socket address {1}", uri, sa), ioe);
439  delegate.connectFailed(uri, sa, ioe);
440  }
441 
442  }
443 }
static Version.Type getBuildType()
Definition: Version.java:87

Copyright © 2012-2022 Basis Technology. Generated on: Tue Feb 6 2024
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.