Autopsy 4.22.1
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 */
19package com.basistech.df.cybertriage.autopsy.ctapi;
20
21import com.basistech.df.cybertriage.autopsy.ctapi.CTCloudException.ErrorCode;
22import com.basistech.df.cybertriage.autopsy.ctapi.json.FileUploadRequest;
23import com.basistech.df.cybertriage.autopsy.ctapi.util.ObjectMapperUtil;
24import com.fasterxml.jackson.databind.ObjectMapper;
25import java.io.IOException;
26import java.io.InputStream;
27import java.net.Proxy;
28import java.net.ProxySelector;
29import java.net.SocketAddress;
30import java.net.URI;
31import java.net.URISyntaxException;
32import java.security.KeyManagementException;
33import java.security.KeyStore;
34import java.security.KeyStoreException;
35import java.security.NoSuchAlgorithmException;
36import java.security.SecureRandom;
37import java.security.UnrecoverableKeyException;
38import java.text.MessageFormat;
39import java.util.Collection;
40import java.util.Collections;
41import java.util.List;
42import java.util.Map;
43import java.util.Map.Entry;
44import java.util.logging.Level;
45import java.util.stream.Stream;
46import javax.net.ssl.KeyManager;
47import javax.net.ssl.KeyManagerFactory;
48import javax.net.ssl.SSLContext;
49import javax.net.ssl.TrustManager;
50import javax.net.ssl.TrustManagerFactory;
51import javax.net.ssl.X509TrustManager;
52import org.apache.commons.collections4.MapUtils;
53import org.apache.commons.lang.ArrayUtils;
54import org.apache.commons.lang3.StringUtils;
55import org.apache.http.HttpEntity;
56import org.apache.http.HttpStatus;
57import org.apache.http.client.config.RequestConfig;
58import org.apache.http.client.methods.CloseableHttpResponse;
59import org.apache.http.client.methods.HttpPost;
60import org.apache.http.client.methods.HttpPut;
61import org.apache.http.client.methods.HttpRequestBase;
62import org.apache.http.impl.client.CloseableHttpClient;
63import org.apache.http.util.EntityUtils;
64import org.apache.http.client.utils.URIBuilder;
65import org.apache.http.entity.ContentType;
66import org.apache.http.entity.InputStreamEntity;
67import org.apache.http.entity.StringEntity;
68import org.sleuthkit.autopsy.coreutils.Logger;
69import org.apache.http.impl.client.HttpClientBuilder;
70import org.apache.http.impl.client.HttpClients;
71import org.apache.http.impl.client.WinHttpClients;
72import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
73import org.apache.http.ssl.SSLInitializationException;
74import org.netbeans.core.ProxySettings;
75import org.openide.util.Lookup;
76import org.sleuthkit.autopsy.coreutils.Version;
77
87class 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}

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.