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..9696a7c308c241880e3c349ea7a11a89020c2f7e 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java +++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java @@ -18,13 +18,20 @@ */ package org.sleuthkit.datamodel; +import org.sleuthkit.datamodel.filerepository.FileRepository; 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.sql.SQLException; import java.sql.Statement; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.ResourceBundle; import java.util.Set; @@ -37,6 +44,7 @@ import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM; import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM; import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM; +import org.sleuthkit.datamodel.filerepository.FileRepositoryException; /** * An abstract base class for classes that represent files that have been added @@ -1074,12 +1082,17 @@ private synchronized void loadLocalFile() throws TskCoreException { } // Copy the file from the server - try { - localFile = FileRepository.downloadFromFileRepository(this); - } catch (TskCoreException ex) { + Path localFilePath = Paths.get(System.getProperty("java.io.tmpdir"), this.getSha256Hash()); + try (InputStream fileRepositoryStream = FileRepository.download(this)) { + Files.copy(fileRepositoryStream, localFilePath); + localFile = localFilePath.toFile(); + } catch (FileAlreadyExistsException ex) { + // Do nothing, file already exists. + this.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/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties index 74eeb7f4a9e9cc75adb712f49373d558e8eeb9e3..d36df3e0b55b4a5172a5e72abf5f633f29eda561 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties @@ -354,6 +354,3 @@ 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 diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED index 74eeb7f4a9e9cc75adb712f49373d558e8eeb9e3..d36df3e0b55b4a5172a5e72abf5f633f29eda561 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED @@ -354,6 +354,3 @@ 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 diff --git a/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java b/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java deleted file mode 100644 index 6c4b456b4acc1f195adbeda028df9c0efa0a5b7a..0000000000000000000000000000000000000000 --- a/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * 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.io.File; -import java.io.IOException; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * 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 final static String FILE_PATH = "v1/files/"; - private final FileRepositorySettings settings; - private final File fileDownloadFolder; - - 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 - */ - 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; - } - - /** - * 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. - */ - private static synchronized void reportError(String errorTitle, String errorStr) { - if (errorHandler != null) { - errorHandler.displayErrorToUser(errorTitle, errorStr); - } - } - - /** - * 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 - } - } - - /** - * 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"); - } - - if (! abstractFile.getFileLocation().equals(TskData.FileLocation.REPOSITORY)) { - throw new TskCoreException("File with object ID " + abstractFile.getId() + " is not stored in the file repository"); - } - - 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"); - } - - // 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); - } - - // 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"); - } - } - - /** - * 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); - } - } - - /** - * 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"); - } - - File file = new File(filePath); - if (! file.exists()) { - throw new TskCoreException("Error uploading file " + filePath + " to file repository - file does not exist"); - } - - // 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); - } - } - - /** - * Utility class to hold the file repository server settings. - */ - 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; - } - - String getAddress() { - return address; - } - - String getPort() { - return port; - } - } - - /** - * 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/filerepository/BulkExistenceEnum.java b/bindings/java/src/org/sleuthkit/datamodel/filerepository/BulkExistenceEnum.java new file mode 100755 index 0000000000000000000000000000000000000000..c6e30283be04cb0b3fd8865922084dbf00ea55eb --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/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.filerepository; + +/** + * 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/filerepository/BulkExistenceResult.java b/bindings/java/src/org/sleuthkit/datamodel/filerepository/BulkExistenceResult.java new file mode 100755 index 0000000000000000000000000000000000000000..9effcb9f38b92c7a560bca5ae1fb121644368a40 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/BulkExistenceResult.java @@ -0,0 +1,50 @@ +/* + * 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.filerepository; + +import java.util.Collections; +import java.util.Map; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * 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/filerepository/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/filerepository/Bundle.properties new file mode 100755 index 0000000000000000000000000000000000000000..801211667774deef18adbfc98408a0302f22efbf --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/Bundle.properties @@ -0,0 +1,3 @@ +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/Bundle.properties-MERGED b/bindings/java/src/org/sleuthkit/datamodel/filerepository/Bundle.properties-MERGED new file mode 100755 index 0000000000000000000000000000000000000000..801211667774deef18adbfc98408a0302f22efbf --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/Bundle.properties-MERGED @@ -0,0 +1,3 @@ +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/FileRepository.java b/bindings/java/src/org/sleuthkit/datamodel/filerepository/FileRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..5b862e176465ece98b02595e85f8c3c2b6a0b952 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/FileRepository.java @@ -0,0 +1,448 @@ +/* + * 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.filerepository; + +import com.google.gson.Gson; +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.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.UUID; +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; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskData; + +/** + * Class to represent a file repository. + */ +public class FileRepository { + + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.filerepository.Bundle"); + private static final int MAX_BULK_QUERY_SIZE = 500; + private final static String V1_FILES_TEMPLATE = "http://%s:%s/v1/files/"; + + private final Gson gson; + private final FileRepositorySettings settings; + + 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 + */ + FileRepository(FileRepositorySettings settings) { + this.settings = settings; + this.gson = new Gson(); + } + + /** + * Initializes the file repository. + * + * @param settings The file repository settings + */ + public static synchronized void initialize(FileRepositorySettings settings) { + instance = new FileRepository(settings); + } + + /** + * De-initializes the file repository. + */ + public static synchronized void deinitialize() { + 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; + } + + /** + * 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. + */ + private static synchronized void reportError(String errorTitle, String errorStr) { + if (errorHandler != null) { + errorHandler.displayErrorToUser(errorTitle, errorStr); + } + } + + /** + * 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.filerepository.FileRepositoryException + * @throws java.io.IOException + * + */ + public static synchronized InputStream download(AbstractFile abstractFile) throws FileRepositoryException, IOException { + // Preconditions + ensureInstanceIsEnabled(); + ensureNonEmptySHA256(abstractFile); + ensureFileLocationIsRemote(abstractFile); + + return instance.sendDownloadRequest(abstractFile.getSha256Hash()); + } + + /** + * 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; + } + + // 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.filerepository.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(System.getProperty("java.io.tmpdir"), 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. + } + } + } + } + + /** + * 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.filerepository.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); + } + } + } + + /** + * 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; + } + + 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)); + } + } + } + } + + /** + * 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"); + } + } + + /** + * 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."); + } + } + + /** + * 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"); + } + } + + /** + * 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)); + } + } + + /** + * Checks the status code of the response and throws a templated exception + * if it's not the expected 200 code. + */ + 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)); + } + } + + /** + * 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")); + } + } + + /** + * 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()); + } + } + } + } + + /** + * Streams data over a HTTP connection. + */ + 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/filerepository/FileRepositoryErrorHandler.java b/bindings/java/src/org/sleuthkit/datamodel/filerepository/FileRepositoryErrorHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..b58a4b064631a8049a0ae8a500c5c1d4f7ead867 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/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.filerepository; + +/** + * 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/filerepository/FileRepositoryException.java b/bindings/java/src/org/sleuthkit/datamodel/filerepository/FileRepositoryException.java new file mode 100755 index 0000000000000000000000000000000000000000..b3b1f66571f89a5ba3cba36f4140d611c9668abb --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/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.filerepository; + +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/filerepository/FileRepositorySettings.java b/bindings/java/src/org/sleuthkit/datamodel/filerepository/FileRepositorySettings.java new file mode 100755 index 0000000000000000000000000000000000000000..8bbfa07290531a7e8ebfd893ac7e954d27d33f2d --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/filerepository/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.filerepository; + +/** + * 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); + } +}