diff --git a/bindings/java/ivy.xml b/bindings/java/ivy.xml index af9f85d09f8d67a0bc96cab2f3647f211098b736..e70fc02a89891d2e3a9aebfc76ae2b5c4b6f5106 100644 --- a/bindings/java/ivy.xml +++ b/bindings/java/ivy.xml @@ -20,6 +20,9 @@ </dependency> <dependency org="com.mchange" name="c3p0" rev="0.9.5" /> <dependency org="com.zaxxer" name="SparseBitSet" rev="1.1" /> + + <dependency org="org.apache.httpcomponents" name="httpmime" rev="4.5.13"/> + <dependency org="org.apache.httpcomponents" name="httpclient" rev="4.5.13"/> </dependencies> </ivy-module> diff --git a/bindings/java/nbproject/project.xml b/bindings/java/nbproject/project.xml index 58ab53b9b262c0a5a2414f1bbbefc6a354627323..24a86c1896e50ffb3e19dbd0ed510bc9ee6dad89 100755 --- a/bindings/java/nbproject/project.xml +++ b/bindings/java/nbproject/project.xml @@ -114,7 +114,7 @@ <java-data xmlns="http://www.netbeans.org/ns/freeform-project-java/4"> <compilation-unit> <package-root>src</package-root> - <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.8.2.jar;lib/postgresql-9.4-1201.jdbc41.jar;lib/c3p0-0.9.5.jar;lib/mchange-commons-java-0.2.9.jar;lib/c3p0-0.9.5-sources.jar;lib/c3p0-0.9.5-javadoc.jar;lib/joda-time-2.4.jar;lib/commons-lang3-3.0.jar;lib/guava-19.0.jar;lib/SparseBitSet-1.1.jar;lib/gson-2.8.5.jar;lib/commons-validator-1.6.jar</classpath> + <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.8.2.jar;lib/postgresql-9.4-1201.jdbc41.jar;lib/c3p0-0.9.5.jar;lib/mchange-commons-java-0.2.9.jar;lib/c3p0-0.9.5-sources.jar;lib/c3p0-0.9.5-javadoc.jar;lib/joda-time-2.4.jar;lib/commons-lang3-3.0.jar;lib/guava-19.0.jar;lib/SparseBitSet-1.1.jar;lib/gson-2.8.5.jar;lib/commons-validator-1.6.jar;lib/httpclient-4.5.13.jar;lib/httpcore-4.4.13.jar;lib/commons-logging-1.2.jar;lib/commons-codec-1.11.jar;lib/httpmime-4.5.13.jar</classpath> <built-to>build</built-to> <source-level>1.8</source-level> </compilation-unit> diff --git a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java index 82776adccab51e5c539d65e76af0a2a9954654d4..a8911a3e788a2a9cce75d7406c04650c52ffdca4 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java +++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java @@ -20,7 +20,13 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.sql.SQLException; import java.sql.Statement; import java.text.MessageFormat; @@ -1073,13 +1079,16 @@ private synchronized void loadLocalFile() throws TskCoreException { throw new TskCoreException("Previously failed to load file with object ID " + getId() + " from file repository."); } - // Copy the file from the server - try { - localFile = FileRepository.downloadFromFileRepository(this); - } catch (TskCoreException ex) { + try (InputStream fileRepositoryStream = FileRepository.download(this)) { + Path localFilePath = Paths.get(FileRepository.getTempDirectory().getAbsolutePath(), this.getSha256Hash()); + if (!Files.exists(localFilePath)) { + Files.copy(fileRepositoryStream, localFilePath); + } + localFile = localFilePath.toFile(); + } catch (FileRepositoryException | IOException ex) { // If we've failed to download from the file repository, don't try again for this session. errorLoadingFromFileRepo = true; - throw ex; + throw new TskCoreException("Failed trying to download file from repository", ex); } } } diff --git a/bindings/java/src/org/sleuthkit/datamodel/BulkExistenceEnum.java b/bindings/java/src/org/sleuthkit/datamodel/BulkExistenceEnum.java new file mode 100755 index 0000000000000000000000000000000000000000..e43718da46b45f2a1c5bafaccccf42243b9c1dd6 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/BulkExistenceEnum.java @@ -0,0 +1,30 @@ +/* + * SleuthKit Java Bindings + * + * Copyright 2020 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 org.sleuthkit.datamodel; + +/** + * Represents the states of existence for a file in the file repository. + * Given that the bulk endpoint is tolerant of dirty SHA-256 values, INVALID + * is an enum status for all malformed SHA-256 values. + */ +public enum BulkExistenceEnum { + TRUE, + FALSE, + INVALID; +} diff --git a/bindings/java/src/org/sleuthkit/datamodel/BulkExistenceResult.java b/bindings/java/src/org/sleuthkit/datamodel/BulkExistenceResult.java new file mode 100755 index 0000000000000000000000000000000000000000..b358513926990ef1810dd4cd051dfe69b2057b97 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/BulkExistenceResult.java @@ -0,0 +1,49 @@ +/* + * SleuthKit Java Bindings + * + * Copyright 2020 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 org.sleuthkit.datamodel; + +import java.util.Collections; +import java.util.Map; + +/** + * Container for bulk existence results. + */ +public class BulkExistenceResult { + + private Map<String, BulkExistenceEnum> files; + + /** + * Checks the status of a file in this container. It is assumed that the + * file is contained within response, otherwise a null value is returned. + * + * @param file File to test + * @return + */ + public BulkExistenceEnum getResult(AbstractFile file) { + if (file.getSha256Hash() == null || file.getSha256Hash().isEmpty()) { + return BulkExistenceEnum.INVALID; + } + + return files.get(file.getSha256Hash()); + } + + void setFiles(Map<String, BulkExistenceEnum> files) { + this.files = Collections.unmodifiableMap(files); + } +} diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties index 74eeb7f4a9e9cc75adb712f49373d558e8eeb9e3..3d3265735e9b247a3914a3a55b742f0ba218ef2b 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties @@ -354,6 +354,6 @@ tagsFilter.displayName.text=Must be tagged TextFilter.displayName.text=Must include text: TypeFilter.displayName.text=Limit event types to FileTypesFilter.displayName.text=Limit file types to -FileRepository.downloadError.title.text=Error downloading from file repository -FileRepository.downloadError.msg.text=Failed to download file with object ID {0} and SHA-256 hash {1} -FileRepository.notEnabled.msg.text=File repository is not enabled +FileRepository.error.title.text=Error from file repository +FileRepository.downloadError.msg.text=Encountered unexpected error from the file repository. Please see logs for more information. +FileRepository.notEnabled.msg.text=File repository is not enabled \ No newline at end of file diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED index 74eeb7f4a9e9cc75adb712f49373d558e8eeb9e3..3d3265735e9b247a3914a3a55b742f0ba218ef2b 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED @@ -354,6 +354,6 @@ tagsFilter.displayName.text=Must be tagged TextFilter.displayName.text=Must include text: TypeFilter.displayName.text=Limit event types to FileTypesFilter.displayName.text=Limit file types to -FileRepository.downloadError.title.text=Error downloading from file repository -FileRepository.downloadError.msg.text=Failed to download file with object ID {0} and SHA-256 hash {1} -FileRepository.notEnabled.msg.text=File repository is not enabled +FileRepository.error.title.text=Error from file repository +FileRepository.downloadError.msg.text=Encountered unexpected error from the file repository. Please see logs for more information. +FileRepository.notEnabled.msg.text=File repository is not enabled \ No newline at end of file diff --git a/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java b/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java index 6c4b456b4acc1f195adbeda028df9c0efa0a5b7a..557145a3e52f7d241fdf66c6a5d5bcd6110e26c7 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java +++ b/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java @@ -18,91 +18,147 @@ */ package org.sleuthkit.datamodel; +import com.google.gson.Gson; import java.io.File; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; /** * Class to represent a file repository. */ public class FileRepository { - private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle"); + private static final Logger logger = Logger.getLogger(FileRepository.class.getName()); - private static FileRepositoryErrorHandler errorHandler; + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle"); + private static final int MAX_BULK_QUERY_SIZE = 500; + private final static String V1_FILES_TEMPLATE = "http://%s:%s/v1/files/"; - private final static String FILE_PATH = "v1/files/"; + private final Gson gson; private final FileRepositorySettings settings; private final File fileDownloadFolder; + private static FileRepositoryErrorHandler errorHandler; private static FileRepository instance; - /** - * Create the file repository. - * - * @param settings The file repository settings - * @param fileDownloadPath The temporary folder to download files to from the repository - */ - private FileRepository(FileRepositorySettings settings, File fileDownloadPath) { - this.settings = settings; - this.fileDownloadFolder = fileDownloadPath; - } - - /** - * Initializes the file repository. - * - * @param settings The file repository settings - * @param fileDownloadPath The temporary folder to download files to from the repository + /** + * Create the file repository. + * + * @param settings The file repository settings + * @param fileDownloadPath The temporary folder to download files to from the repository */ - public static synchronized void initialize(FileRepositorySettings settings, File fileDownloadPath) { - // If the download path is changing, delete any files in the old one - if ((instance != null) && (instance.fileDownloadFolder != null) - && ( ! instance.fileDownloadFolder.equals(fileDownloadPath))) { - deleteDownloadFolder(instance.fileDownloadFolder); - } - instance = new FileRepository(settings, fileDownloadPath); + private FileRepository(FileRepositorySettings settings, File fileDownloadPath) { + this.settings = settings; + this.fileDownloadFolder = fileDownloadPath; + this.gson = new Gson(); } - /** - * De-initializes the file repository. - */ - public static synchronized void deinitialize() { - if (instance != null) { - // Delete the temp folder - deleteDownloadFolder(instance.fileDownloadFolder); - } - - instance = null; + /** + * Delete the folder of downloaded files. + */ + private static synchronized void deleteDownloadFolder(File dirPath) { + if (dirPath.isDirectory() == false || dirPath.exists() == false) { + return; + } + + File[] files = dirPath.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDownloadFolder(file); + } else { + if (file.delete() == false) { + logger.log(Level.WARNING, "Failed to delete file {0}", file.getPath()); //NON-NLS + } + } + } + } + if (dirPath.delete() == false) { + logger.log(Level.WARNING, "Failed to delete the empty directory at {0}", dirPath.getPath()); //NON-NLS + } } - + + /** + * Initializes the file repository. + * + * @param settings The file repository settings + * @param fileDownloadPath The temporary folder to download files to from the repository + */ + public static synchronized void initialize(FileRepositorySettings settings, File fileDownloadPath) { + // If the download path is changing, delete any files in the old one + if ((instance != null) && (instance.fileDownloadFolder != null) + && ( ! instance.fileDownloadFolder.equals(fileDownloadPath))) { + deleteDownloadFolder(instance.fileDownloadFolder); + } + instance = new FileRepository(settings, fileDownloadPath); + } + + /** + * De-initializes the file repository. + */ + public static synchronized void deinitialize() { + if (instance != null) { + // Delete the temp folder + deleteDownloadFolder(instance.fileDownloadFolder); + } + + instance = null; + } + /** * Check if the file repository is enabled. - * + * * @return true if enabled, false otherwise. */ public static boolean isEnabled() { return instance != null; } - + /** * Set the error handling callback. - * + * * @param handler The error handler. */ public static synchronized void setErrorHandler(FileRepositoryErrorHandler handler) { errorHandler = handler; } + static synchronized File getTempDirectory() throws FileRepositoryException { + ensureInstanceIsEnabled(); + return instance.fileDownloadFolder; + } + /** - * Report an error to the user. - * The idea is to use this for cases where it's a user error that may be able - * to be corrected through changing the repository settings. - * + * Report an error to the user. The idea is to use this for cases where it's + * a user error that may be able to be corrected through changing the + * repository settings. + * * @param errorTitle The title for the error display. * @param errorStr The error message. */ @@ -111,205 +167,326 @@ private static synchronized void reportError(String errorTitle, String errorStr) errorHandler.displayErrorToUser(errorTitle, errorStr); } } - + /** - * Delete the folder of downloaded files. + * Download a file's data from the file repository. The resulting stream + * must be closed and it should be read as soon as possible. + * + * @param abstractFile The file to be downloaded. + * + * @return The file contents, as a stream. + * + * @throws org.sleuthkit.datamodel.FileRepositoryException + * @throws java.io.IOException + * */ - private static synchronized void deleteDownloadFolder(File dirPath) { - if (dirPath.isDirectory() == false || dirPath.exists() == false) { - return; - } - - File[] files = dirPath.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - deleteDownloadFolder(file); - } else { - if (file.delete() == false) { - logger.log(Level.WARNING, "Failed to delete file {0}", file.getPath()); //NON-NLS - } - } - } - } - if (dirPath.delete() == false) { - logger.log(Level.WARNING, "Failed to delete the empty directory at {0}", dirPath.getPath()); //NON-NLS - } + public static synchronized InputStream download(AbstractFile abstractFile) throws FileRepositoryException, IOException { + // Preconditions + ensureInstanceIsEnabled(); + ensureNonEmptySHA256(abstractFile); + ensureFileLocationIsRemote(abstractFile); + + return instance.sendDownloadRequest(abstractFile.getSha256Hash()); } - + /** - * Download a file from the file repository. - * The caller must ensure that this is not called on the same file multiple times concurrently. - * - * @param abstractFile The file being downloaded. - * - * @return The downloaded file. - * - * @throws TskCoreException - */ - public static synchronized File downloadFromFileRepository(AbstractFile abstractFile) throws TskCoreException { - - if (instance == null) { - String title = BUNDLE.getString("FileRepository.downloadError.title.text"); - String msg = BUNDLE.getString("FileRepository.notEnabled.msg.text"); - reportError(title, msg); - throw new TskCoreException("File repository is not enabled"); + * Private function to perform file download. + */ + private InputStream sendDownloadRequest(String SHA256) throws IOException, FileRepositoryException { + final CloseableHttpClient httpClient = HttpClients.createDefault(); + final String downloadURL = settings.createBaseURL(V1_FILES_TEMPLATE) + SHA256; + + final HttpGet downloadRequest = new HttpGet(downloadURL); + final CloseableHttpResponse response = httpClient.execute(downloadRequest); + final int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode != HttpStatus.SC_OK) { + FileRepositoryException repoEx = null; + try { + final String title = BUNDLE.getString("FileRepository.error.title.text"); + final String message = BUNDLE.getString("FileRepository.downloadError.msg.text"); + reportError(title, message); + final String errorMessage = extractErrorMessage(response); + repoEx = new FileRepositoryException(String.format("Request " + + "failed with the following response body %s. Please " + + "check the file repository logs for more information.", errorMessage)); + } finally { + try { + response.close(); + } catch (IOException ex) { + // Best effort + if (repoEx != null) { + repoEx.addSuppressed(ex); + } + } + + try { + httpClient.close(); + } catch (IOException ex) { + // Best effort + if (repoEx != null) { + repoEx.addSuppressed(ex); + } + } + } + + throw repoEx; } - - if (! abstractFile.getFileLocation().equals(TskData.FileLocation.REPOSITORY)) { - throw new TskCoreException("File with object ID " + abstractFile.getId() + " is not stored in the file repository"); + + // Client and response will close once the stream has been + // consumed and closed by the client. + return new HTTPInputStream(httpClient, response); + } + + /** + * Uploads a stream of data to the file repository. + * + * + * @param stream Arbitrary data to store in this file repository. + * + * @throws java.io.IOException + * @throws org.sleuthkit.datamodel.FileRepositoryException + */ + public static synchronized void upload(InputStream stream) throws IOException, FileRepositoryException { + // Preconditions + ensureInstanceIsEnabled(); + + instance.sendUploadRequest(stream); + } + + /** + * Private function to perform file upload. + */ + private void sendUploadRequest(InputStream stream) throws IOException, FileRepositoryException { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + final String uploadURL = settings.createBaseURL(V1_FILES_TEMPLATE); + + // Flush the stream to a local temp file for transport. + final Path temp = Paths.get(fileDownloadFolder.getAbsolutePath(), UUID.randomUUID().toString()); + Files.copy(stream, temp); + + final HttpEntity fileUpload = MultipartEntityBuilder.create() + .addBinaryBody("file", temp.toFile()) + .build(); + final HttpUriRequest postRequest = RequestBuilder.post(uploadURL) + .setEntity(fileUpload) + .build(); + + try (CloseableHttpResponse response = httpClient.execute(postRequest)) { + checkSuccess(response); + } catch (IOException | FileRepositoryException ex) { + try { + Files.delete(temp); + } catch (IOException deleteEx) { + ex.addSuppressed(deleteEx); + } + throw ex; + } finally { + try { + Files.delete(temp); + } catch (IOException ex) { + // Do nothing, best effort. + } + } } - - if (abstractFile.getSha256Hash() == null || abstractFile.getSha256Hash().isEmpty()) { - throw new TskCoreException("File with object ID " + abstractFile.getId() + " has no SHA-256 hash and can not be downloaded"); + } + + /** + * Checks if many abstract files are stored within this file repository. + * This API is tolerant of files without SHA-256 values, as opposed to its + * overridden counterpart, which will throw an exception if not present. + * + * @param files Files to test + * + * @return An object encapsulating the response for each file. + * + * @throws java.io.IOException + * @throws org.sleuthkit.datamodel.FileRepositoryException + */ + public static synchronized BulkExistenceResult exists(List<AbstractFile> files) throws IOException, FileRepositoryException { + // Preconditions + ensureInstanceIsEnabled(); + ensureBulkQuerySize(files); + + return instance.sendMultiExistenceQuery(new ExistenceQuery(files)); + } + + /** + * Private function to perform the bulk existence query. + */ + private BulkExistenceResult sendMultiExistenceQuery(ExistenceQuery query) throws IOException, FileRepositoryException { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + final String existsURL = settings.createBaseURL(V1_FILES_TEMPLATE) + "exists"; + final String jsonString = gson.toJson(query); + final StringEntity jsonEntity = new StringEntity(jsonString, StandardCharsets.UTF_8); + + final HttpPut bulkExistsPost = new HttpPut(existsURL); + bulkExistsPost.setEntity(jsonEntity); + bulkExistsPost.setHeader("Accept", "application/json"); + bulkExistsPost.setHeader("Content-type", "application/json"); + + try (CloseableHttpResponse response = httpClient.execute(bulkExistsPost)) { + checkSuccess(response); + + final HttpEntity entity = response.getEntity(); + final ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + entity.writeTo(byteOutputStream); + final String jsonBody = new String(byteOutputStream.toByteArray(), StandardCharsets.UTF_8); + return gson.fromJson(jsonBody, BulkExistenceResult.class); + } } - - // Download the file if it's not already there. - String targetPath = Paths.get(instance.fileDownloadFolder.getAbsolutePath(), abstractFile.getSha256Hash()).toString(); - if ( ! new File(targetPath).exists()) { - instance.downloadFile(abstractFile, targetPath); + } + + /** + * Checks if the abstract file is stored within this file repository. + * + * @param file Abstract file to query + * + * @return True/False + * + * @throws IOException + * @throws FileRepositoryException + */ + public static synchronized boolean exists(AbstractFile file) throws IOException, FileRepositoryException { + // Preconditions + ensureInstanceIsEnabled(); + ensureNonEmptySHA256(file); + + if (!file.getFileLocation().equals(TskData.FileLocation.REPOSITORY)) { + return false; } - - // Check that we got the file. - File tempFile = new File(targetPath); - if (tempFile.exists()) { - return tempFile; - } else { - String title = BUNDLE.getString("FileRepository.downloadError.title.text"); - String msg = MessageFormat.format(BUNDLE.getString("FileRepository.downloadError.msg.text"), abstractFile.getId(), abstractFile.getSha256Hash()); - reportError(title, msg); - throw new TskCoreException("Failed to download file with object ID " + abstractFile.getId() - + " and SHA-256 hash " + abstractFile.getSha256Hash() + " from file repository"); + + return instance.sendSingularExistenceQuery(file.getSha256Hash()); + } + + /** + * Private function to perform the existence query. + */ + private boolean sendSingularExistenceQuery(String SHA256) throws IOException, FileRepositoryException { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + final String existsURL = settings.createBaseURL(V1_FILES_TEMPLATE) + SHA256; + + final HttpHead request = new HttpHead(existsURL); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + final int statusCode = response.getStatusLine().getStatusCode(); + switch (statusCode) { + case HttpStatus.SC_OK: + return true; + case HttpStatus.SC_NOT_FOUND: + return false; + default: + throw new FileRepositoryException(String.format("Unexpected " + + "response code. Expected 200 or 404, but instead got %d. " + + "Please check the file repository logs for more information.", statusCode)); + } + } } } - + /** - * Download the file. - * - * @param abstractFile The file being downloaded. - * @param targetPath The location to save the file to. - * - * @throws TskCoreException - */ - private void downloadFile(AbstractFile abstractFile, String targetPath) throws TskCoreException { - - String url = "http://" + settings.getAddress() + ":" + settings.getPort() + "/" + FILE_PATH + abstractFile.getSha256Hash(); - - List<String> command = new ArrayList<>(); - command.add("curl"); - command.add("-X"); - command.add("GET"); - command.add(url); - command.add("-H"); - command.add("accept: */*"); - command.add("--output"); - command.add(targetPath); - - ProcessBuilder processBuilder = new ProcessBuilder(command).inheritIO(); - try { - Process process = processBuilder.start(); - process.waitFor(); - } catch (IOException ex) { - String title = BUNDLE.getString("FileRepository.downloadError.title.text"); - String msg = MessageFormat.format(BUNDLE.getString("FileRepository.downloadError.msg.text"), abstractFile.getId(), abstractFile.getSha256Hash()); - reportError(title, msg); - throw new TskCoreException("Error downloading file with SHA-256 hash " + abstractFile.getSha256Hash() + " from file repository", ex); - } catch (InterruptedException ex) { - throw new TskCoreException("Interrupted while downloading file with SHA-256 hash " + abstractFile.getSha256Hash() + " from file repository", ex); + * Prevents a query if the file is not remote. + */ + private static void ensureFileLocationIsRemote(AbstractFile abstractFile) throws FileRepositoryException { + if (!abstractFile.getFileLocation().equals(TskData.FileLocation.REPOSITORY)) { + throw new FileRepositoryException("File with object ID " + abstractFile.getId() + " is not stored in the file repository"); } } - + /** - * Upload a given file to the file repository. - * - * @param filePath The path on disk to the file being uploaded. - * - * @throws TskCoreException - */ - public static synchronized void uploadToFileRepository(String filePath) throws TskCoreException { - - if (instance == null) { - throw new TskCoreException("File repository is not enabled"); + * Prevents a query for a file with no SHA-256. + */ + private static void ensureNonEmptySHA256(AbstractFile abstractFile) throws FileRepositoryException { + if (abstractFile.getSha256Hash() == null || abstractFile.getSha256Hash().isEmpty()) { + throw new FileRepositoryException("File with object ID " + abstractFile.getId() + " has no SHA-256 hash."); } - - File file = new File(filePath); - if (! file.exists()) { - throw new TskCoreException("Error uploading file " + filePath + " to file repository - file does not exist"); + } + + /** + * Ensures the instance is enabled, notifying users otherwise. + */ + private static void ensureInstanceIsEnabled() throws FileRepositoryException { + if (!isEnabled()) { + final String title = BUNDLE.getString("FileRepository.error.title.text"); + final String msg = BUNDLE.getString("FileRepository.notEnabled.msg.text"); + reportError(title, msg); + throw new FileRepositoryException("File repository is not enabled"); } - - // Upload the file. - instance.uploadFile(file); } - + /** - * Upload the file. - * - * @param file The file being uploaded. - * - * @throws TskCoreException - */ - private void uploadFile(File file) throws TskCoreException { - String url = "http://" + settings.getAddress() + ":" + settings.getPort() + "/" + FILE_PATH; - - // Example: curl -X POST "http://localhost:8080/api/files" -H "accept: application/json" -H "Content-Type: multipart/form-data" -F "file=@Report.xml" - List<String> command = new ArrayList<>(); - command.add("curl"); - command.add("-X"); - command.add("POST"); - command.add(url); - command.add("-H"); - command.add("accept: application/json"); - command.add("-H"); - command.add("Content-Type: multipart/form-data"); - command.add("-F"); - command.add("file=@" + file.getAbsolutePath()); - - ProcessBuilder processBuilder = new ProcessBuilder(command).inheritIO(); - try { - Process process = processBuilder.start(); - process.waitFor(); - } catch (IOException | InterruptedException ex) { - throw new TskCoreException("Error saving file at " + file.getAbsolutePath() + " to file repository", ex); - } - } - + * Prevents a request from being made if it exceeds the maximum threshold + * for a bulk query. + */ + private static void ensureBulkQuerySize(List<AbstractFile> files) throws FileRepositoryException { + if (files.size() > MAX_BULK_QUERY_SIZE) { + throw new FileRepositoryException(String.format("Exceeds the allowable " + + "threshold (%d) for a single request.", MAX_BULK_QUERY_SIZE)); + } + } + /** - * Utility class to hold the file repository server settings. + * Checks the status code of the response and throws a templated exception + * if it's not the expected 200 code. */ - static public class FileRepositorySettings { - private final String address; - private final String port; - - /** - * Create a FileRepositorySettings instance for the server. - * - * @param address The IP address/hostname of the server. - * @param port The port. - */ - public FileRepositorySettings(String address, String port) { - this.address = address; - this.port = port; + private static void checkSuccess(CloseableHttpResponse response) throws FileRepositoryException, IOException { + final int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode != HttpStatus.SC_OK) { + final String errorMessage = extractErrorMessage(response); + throw new FileRepositoryException(String.format("Request failed with " + + "the following response body %s. Please check the file " + + "repository logs for more information.", errorMessage)); } - - String getAddress() { - return address; + } + + /** + * Extracts the entire response body as a plain string. + */ + private static String extractErrorMessage(CloseableHttpResponse response) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + response.getEntity().getContent(), StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); } - - String getPort() { - return port; + } + + /** + * Query object to be serialized by GSON and sent as a payload to the bulk + * exists endpoint. + */ + private static class ExistenceQuery { + + private final List<String> files; + + ExistenceQuery(List<AbstractFile> absFiles) { + files = new ArrayList<>(); + for (AbstractFile file : absFiles) { + if (file.getSha256Hash() != null && !file.getSha256Hash().isEmpty()) { + files.add(file.getSha256Hash()); + } + } } } - + /** - * Callback class to use for error reporting. + * Streams data over a HTTP connection. */ - public interface FileRepositoryErrorHandler { - /** - * Handles displaying an error message to the user (if appropriate). - * - * @param title The title for the error display. - * @param error The more detailed error message to display. - */ - void displayErrorToUser(String title, String error); + private static class HTTPInputStream extends FilterInputStream { + + private final CloseableHttpResponse response; + private final CloseableHttpClient client; + + HTTPInputStream(CloseableHttpClient client, CloseableHttpResponse response) throws IOException { + super(response.getEntity().getContent()); + this.response = response; + this.client = client; + } + + @Override + public void close() throws IOException { + super.close(); + response.close(); + client.close(); + } } } diff --git a/bindings/java/src/org/sleuthkit/datamodel/FileRepositoryErrorHandler.java b/bindings/java/src/org/sleuthkit/datamodel/FileRepositoryErrorHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..341a6f4b830b17a68043b56f8137cc3d84cd5e61 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/FileRepositoryErrorHandler.java @@ -0,0 +1,34 @@ +/* + * SleuthKit Java Bindings + * + * Copyright 2020 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 org.sleuthkit.datamodel; + +/** + * Callback class to use for error reporting. + */ +public interface FileRepositoryErrorHandler { + + /** + * Handles displaying an error message to the user (if appropriate). + * + * @param title The title for the error display. + * @param error The more detailed error message to display. + */ + void displayErrorToUser(String title, String error); + +} diff --git a/bindings/java/src/org/sleuthkit/datamodel/FileRepositoryException.java b/bindings/java/src/org/sleuthkit/datamodel/FileRepositoryException.java new file mode 100755 index 0000000000000000000000000000000000000000..df66e7a7ac3c999ac0aee58902666aa6a6367683 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/FileRepositoryException.java @@ -0,0 +1,28 @@ +/* + * SleuthKit Java Bindings + * + * Copyright 2020 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 org.sleuthkit.datamodel; + +public class FileRepositoryException extends Exception { + + private static final long serialVersionUID = 1L; + + public FileRepositoryException(String msg) { + super(msg); + } +} diff --git a/bindings/java/src/org/sleuthkit/datamodel/FileRepositorySettings.java b/bindings/java/src/org/sleuthkit/datamodel/FileRepositorySettings.java new file mode 100755 index 0000000000000000000000000000000000000000..8b64881f786c30a9ef4e4bd7f3dbb61c0252c1f0 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/FileRepositorySettings.java @@ -0,0 +1,46 @@ +/* + * SleuthKit Java Bindings + * + * Copyright 2020 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 org.sleuthkit.datamodel; + +/** + * Utility class to hold the file repository server settings. + */ +public class FileRepositorySettings { + + private final String address; + private final String port; + + /** + * Create a FileRepositorySettings instance for the server. + * + * @param address The IP address/hostname of the server. + * @param port The port. + */ + public FileRepositorySettings(String address, String port) { + this.address = address; + this.port = port; + } + + /** + * Fills in an API template with the address and port. + */ + String createBaseURL(String urlTemplate) { + return String.format(urlTemplate, address, port); + } +}