diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java index c358feec61a460b49c71addeeb741237198a26f6..f21873f565755fc9754694d6faa6a2b8327fd029 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java @@ -25,6 +25,7 @@ import com.basistech.df.cybertriage.autopsy.ctapi.json.CTCloudBeanResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.DecryptedLicenseResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.FileReputationRequest; +import com.basistech.df.cybertriage.autopsy.ctapi.json.FileUploadRequest; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseRequest; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.MetadataUploadRequest; @@ -78,21 +79,22 @@ public LicenseResponse getLicenseInfo(String licenseString) throws CTCloudExcept } public AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted) throws CTCloudException { - return getAuthToken(decrypted, false); + return getAuthToken(decrypted, null); } - public AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted, boolean fileUpload) throws CTCloudException { + public AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted, Long fileUploadSize) throws CTCloudException { AuthTokenRequest authTokenRequest = new AuthTokenRequest() .setAutopsyVersion(getAppVersion()) - .setRequestFileUpload(fileUpload) + .setRequestFileUpload(fileUploadSize != null && fileUploadSize > 0) + .setFileUploadSize(fileUploadSize != null && fileUploadSize > 0 ? fileUploadSize : null) .setBoostLicenseId(decrypted.getBoostLicenseId()) .setHostId(decrypted.getLicenseHostId()); return httpClient.doPost(AUTH_TOKEN_REQUEST_PATH, authTokenRequest, AuthTokenResponse.class); } - public void uploadFile(String url, String fileName, InputStream fileIs) throws CTCloudException { - httpClient.doFileUploadPost(url, fileName, fileIs); + public void uploadFile(FileUploadRequest fileUploadRequest) throws CTCloudException { + httpClient.doFileUploadPut(fileUploadRequest); } public void uploadMeta(AuthenticatedRequestData authenticatedRequestData, MetadataUploadRequest metaRequest) throws CTCloudException { diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java index 0a280c87e89bc5f11b52e9f42930620bddf33e2b..0a7c69cdeecb263bffda33a6b227952914bbd518 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java @@ -18,6 +18,8 @@ */ package com.basistech.df.cybertriage.autopsy.ctapi; +import com.basistech.df.cybertriage.autopsy.ctapi.CTCloudException.ErrorCode; +import com.basistech.df.cybertriage.autopsy.ctapi.json.FileUploadRequest; import com.basistech.df.cybertriage.autopsy.ctapi.util.ObjectMapperUtil; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; @@ -55,13 +57,14 @@ import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; +import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; -import org.apache.http.entity.mime.MultipartEntityBuilder; import org.sleuthkit.autopsy.coreutils.Logger; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; @@ -184,10 +187,23 @@ public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jso return null; } - public void doFileUploadPost(String fullUrlPath, String fileName, InputStream fileIs) throws CTCloudException { - URI postUri; + public void doFileUploadPut(FileUploadRequest fileUploadRequest) throws CTCloudException { + if (fileUploadRequest == null) { + throw new CTCloudException(ErrorCode.BAD_REQUEST, new IllegalArgumentException("fileUploadRequest cannot be null")); + } + + String fullUrlPath = fileUploadRequest.getFullUrlPath(); + String fileName = fileUploadRequest.getFileName(); + InputStream fileInputStream = fileUploadRequest.getFileInputStream(); + Long contentLength = fileUploadRequest.getContentLength(); + + if (StringUtils.isBlank(fullUrlPath) || fileInputStream == null || contentLength == null || contentLength <= 0) { + throw new CTCloudException(ErrorCode.BAD_REQUEST, new IllegalArgumentException("fullUrlPath, fileInputStream, contentLength must not be empty, null or less than 0")); + } + + URI putUri; try { - postUri = new URI(fullUrlPath); + putUri = new URI(fullUrlPath); } catch (URISyntaxException ex) { LOGGER.log(Level.WARNING, "Wrong URL syntax for CT Cloud " + fullUrlPath, ex); throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex); @@ -195,23 +211,13 @@ public void doFileUploadPost(String fullUrlPath, String fileName, InputStream fi try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) { LOGGER.log(Level.INFO, "initiating http post request to ctcloud server " + fullUrlPath); - HttpPost post = new HttpPost(postUri); - configureRequestTimeout(post); - - post.addHeader("Connection", "keep-alive"); - - MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.addBinaryBody( - "file", - fileIs, - ContentType.APPLICATION_OCTET_STREAM, - fileName - ); + HttpPut put = new HttpPut(putUri); + configureRequestTimeout(put); - HttpEntity multipart = builder.build(); - post.setEntity(multipart); + put.addHeader("Connection", "keep-alive"); + put.setEntity(new InputStreamEntity(fileInputStream, contentLength, ContentType.APPLICATION_OCTET_STREAM)); - try (CloseableHttpResponse response = httpclient.execute(post)) { + try (CloseableHttpResponse response = httpclient.execute(put)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) { LOGGER.log(Level.INFO, "Response Received. - Status OK"); @@ -381,7 +387,7 @@ private static CloseableHttpClient createConnection(ProxySelector proxySelector, HttpClientBuilder builder; if (ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION - && StringUtils.isBlank(ProxySettings.getAuthenticationUsername()) + && StringUtils.isBlank(ProxySettings.getAuthenticationUsername()) && ArrayUtils.isEmpty(ProxySettings.getAuthenticationPassword()) && WinHttpClients.isWinAuthAvailable()) { diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java index 6a093fee8ededfa7cb01720fd6687e6677897c74..f137818346ef2332f1ebc8e70d41bcc67737a9a2 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java @@ -34,6 +34,9 @@ public class AuthTokenRequest { @JsonProperty("requestFileUpload") private boolean requestFileUpload; + @JsonProperty("fileUploadSize") + private Long fileUploadSize; + @JsonProperty("host_id") private String hostId; @@ -64,6 +67,16 @@ public AuthTokenRequest setRequestFileUpload(boolean requestFileUpload) { return this; } + public Long getFileUploadSize() { + return fileUploadSize; + } + + public AuthTokenRequest setFileUploadSize(Long fileUploadSize) { + this.fileUploadSize = fileUploadSize; + return this; + } + + public String getHostId() { return hostId; } diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileUploadRequest.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileUploadRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..48d8dc77b4705d9fcdc2dda816a802e1bead20a7 --- /dev/null +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileUploadRequest.java @@ -0,0 +1,69 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2023 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.basistech.df.cybertriage.autopsy.ctapi.json; + +import java.io.InputStream; + +/** + * Data for a file upload request. + */ +public class FileUploadRequest { + + private String fullUrlPath; + private String fileName; + private InputStream fileInputStream; + private Long contentLength; + + public String getFullUrlPath() { + return fullUrlPath; + } + + public FileUploadRequest setFullUrlPath(String fullUrlPath) { + this.fullUrlPath = fullUrlPath; + return this; + } + + public String getFileName() { + return fileName; + } + + public FileUploadRequest setFileName(String fileName) { + this.fileName = fileName; + return this; + } + + public InputStream getFileInputStream() { + return fileInputStream; + } + + public FileUploadRequest setFileInputStream(InputStream fileInputStream) { + this.fileInputStream = fileInputStream; + return this; + } + + public Long getContentLength() { + return contentLength; + } + + public FileUploadRequest setContentLength(Long contentLength) { + this.contentLength = contentLength; + return this; + } + +} diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java index 2d27671b298a84b188dec8e2af2cf14d4f6e80bd..ba8bd556b101550519d4784c3926aa8fb7d60282 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java @@ -23,6 +23,7 @@ import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthenticatedRequestData; import com.basistech.df.cybertriage.autopsy.ctapi.json.CTCloudBean; +import com.basistech.df.cybertriage.autopsy.ctapi.json.FileUploadRequest; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo; import com.basistech.df.cybertriage.autopsy.ctapi.json.MalwareResultBean.Status; import com.basistech.df.cybertriage.autopsy.ctapi.json.MetadataUploadRequest; @@ -112,8 +113,10 @@ private static class SharedProcessing { //minimum file uploads left before issuing warning private static final long LOW_UPLOADS_REMAINING = 25; + // min and max upload size in bytes private static final long MIN_UPLOAD_SIZE = 1; - private static final long MAX_UPLOAD_SIZE = 1_000_000_000; + private static final long MAX_UPLOAD_SIZE = 100_000_000; // 100MB + private static final int NUM_FILE_UPLOAD_RETRIES = 7; private static final long FILE_UPLOAD_RETRY_SLEEP_MILLIS = 60 * 1000; @@ -640,7 +643,7 @@ private boolean uploadFile(IngestJobState ingestJobState, String md5, long objId } // get auth token / file upload url - AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense(), true); + AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense(), af.getSize()); if (StringUtils.isBlank(authTokenResponse.getFileUploadUrl())) { throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR); } else if (remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount()) <= 0) { @@ -658,7 +661,13 @@ private boolean uploadFile(IngestJobState ingestJobState, String md5, long objId // upload bytes ReadContentInputStream fileInputStream = new ReadContentInputStream(af); - ctApiDAO.uploadFile(authTokenResponse.getFileUploadUrl(), af.getName(), fileInputStream); + + ctApiDAO.uploadFile(new FileUploadRequest() + .setContentLength(af.getSize()) + .setFileInputStream(fileInputStream) + .setFileName(af.getName()) + .setFullUrlPath(authTokenResponse.getFileUploadUrl()) + ); // upload metadata MetadataUploadRequest metaRequest = new MetadataUploadRequest()