Modify case display name/unique name handling

This commit is contained in:
Richard Cordovano
2017-04-21 17:39:26 -04:00
parent 179da095c4
commit 440fac07fe
6 changed files with 96 additions and 152 deletions
@@ -113,10 +113,8 @@ import org.sleuthkit.datamodel.TskCoreException;
*/
public class Case {
private static final int NAME_LOCK_TIMOUT_HOURS = 12;
private static final int SHARED_DIR_LOCK_TIMOUT_HOURS = 12;
private static final int RESOURCE_LOCK_TIMOUT_HOURS = 12;
private static final int MAX_CASEDB_NAME_LEN_MINUS_TIMESTAMP = 47; //Truncate to 63-16=47 chars to accomodate the timestamp
private static final int DIR_LOCK_TIMOUT_HOURS = 12;
private static final int RESOURCES_LOCK_TIMOUT_HOURS = 12;
private static final String SINGLE_USER_CASE_DB_NAME = "autopsy.db";
private static final String EVENT_CHANNEL_NAME = "%s-Case-Events"; //NON-NLS
private static final String CACHE_FOLDER = "Cache"; //NON-NLS
@@ -475,18 +473,21 @@ public class Case {
}
if (null != currentCase) {
String previousCaseDisplayName = currentCase.getDisplayName();
String previousCaseName = currentCase.getName();
String previousCaseDir = currentCase.getCaseDirectory();
try {
closeCurrentCase();
} catch (CaseActionException ex) {
logger.log(Level.SEVERE, "Error closing the previous current case", ex); //NON-NLS
logger.log(Level.SEVERE, String.format("Error closing the previous current case %s (%s) in %s", previousCaseDisplayName, previousCaseName, previousCaseDir), ex); //NON-NLS
}
}
logger.log(Level.INFO, "Creating current case with display name {0} in {1}", new Object[]{caseDisplayName, caseDir}); //NON-NLS
logger.log(Level.INFO, "Creating current case {0} in {1}", new Object[]{caseDisplayName, caseDir}); //NON-NLS
Case newCurrentCase = new Case();
newCurrentCase.open(caseDir, caseDisplayName, caseNumber, examiner, caseType);
newCurrentCase.create(caseType, caseDir, caseDisplayName, caseNumber, examiner);
currentCase = newCurrentCase;
logger.log(Level.INFO, "Created currrent case {0} (display name {1}) in {2}", new Object[]{newCurrentCase.getName(), caseDisplayName, caseDir}); //NON-NLS
logger.log(Level.INFO, "Created currrent case {0} ({1}) in {2}", new Object[]{newCurrentCase.getDisplayName(), newCurrentCase.getName(), newCurrentCase.getCaseDirectory()}); //NON-NLS
if (RuntimeProperties.runningWithGUI()) {
updateGUIForCaseOpened(newCurrentCase);
}
@@ -723,83 +724,44 @@ public class Case {
}
/**
* Transforms the display name for a case to make a suitable case name for
* use in case directory paths, coordination service locks, Active MQ
* message channels, etc.
*
* ActiveMQ:
* http://activemq.2283324.n4.nabble.com/What-are-limitations-restrictions-on-destination-name-td4664141.html
* may not be ?
* Transforms a case display name into a unique case name that can be used
* to identify the case even if the display name is changed.
*
* @param caseDisplayName A case display name.
*
* @return The case display name transformed into a corresponding case name.
* @return The unique case name.
*
* @throws org.sleuthkit.autopsy.casemodule.Case.IllegalCaseNameException
* @throws IllegalCaseNameException If the transformation fails.
*/
public static String displayNameToCaseName(String caseDisplayName) throws IllegalCaseNameException {
private static String displayNameToUniqueName(String caseDisplayName) throws IllegalCaseNameException {
/*
* Replace all non-ASCII characters.
*/
String uniqueCaseName = caseDisplayName.replaceAll("[^\\p{ASCII}]", "_"); //NON-NLS
/*
* Remove all non-ASCII characters.
* Replace all control characters.
*/
String caseName = caseDisplayName.replaceAll("[^\\p{ASCII}]", "_"); //NON-NLS
uniqueCaseName = uniqueCaseName.replaceAll("[\\p{Cntrl}]", "_"); //NON-NLS
/*
* Remove all control characters.
* Replace /, \, :, ?, space, ' ".
*/
caseName = caseName.replaceAll("[\\p{Cntrl}]", "_"); //NON-NLS
/*
* Remove /, \, :, ?, space, ' ".
*/
caseName = caseName.replaceAll("[ /?:'\"\\\\]", "_"); //NON-NLS
uniqueCaseName = uniqueCaseName.replaceAll("[ /?:'\"\\\\]", "_"); //NON-NLS
/*
* Make it all lowercase.
*/
caseName = caseName.toLowerCase();
if (caseName.isEmpty()) {
throw new IllegalCaseNameException(String.format("Failed to convert case name '%s'", caseDisplayName));
}
return caseName;
}
/**
* Transforms a case name into a name for a PostgreSQL database that can be
* safely used in SQL commands as described at
* http://www.postgresql.org/docs/9.4/static/sql-syntax-lexical.html: 63
* chars max, must start with a letter or underscore, following chars can be
* letters, underscores, or digits. A timestamp suffix is added to ensure
* uniqueness.
*
* @param caseName The case name.
*
* @return The case name transformed into a corresponding PostgreSQL case
* database name.
*/
private static String caseNameToCaseDbName(String caseName) throws IllegalCaseNameException {
/*
* Must start with letter or underscore. If not, prepend an underscore.
*/
String dbName = caseName;
if (dbName.length() > 0 && !(Character.isLetter(dbName.codePointAt(0))) && !(dbName.codePointAt(0) == '_')) {
dbName = "_" + dbName;
}
uniqueCaseName = uniqueCaseName.toLowerCase();
/*
* Truncate to 63-16=47 chars to accomodate the timestamp, then add the
* timestamp.
* Add a time stamp for uniqueness.
*/
if (dbName.length() > MAX_CASEDB_NAME_LEN_MINUS_TIMESTAMP) {
dbName = dbName.substring(0, MAX_CASEDB_NAME_LEN_MINUS_TIMESTAMP);
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
Date date = new Date();
dbName = dbName + "_" + dateFormat.format(date);
uniqueCaseName = uniqueCaseName + "_" + dateFormat.format(date);
return dbName;
return uniqueCaseName;
}
/**
@@ -973,7 +935,7 @@ public class Case {
private static CoordinationService.Lock acquireExclusiveCaseResourcesLock(String caseDir) throws CaseActionException {
try {
String resourcesNodeName = caseDir + "_resources";
Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, resourcesNodeName, RESOURCE_LOCK_TIMOUT_HOURS, TimeUnit.HOURS);
Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, resourcesNodeName, RESOURCES_LOCK_TIMOUT_HOURS, TimeUnit.HOURS);
if (null == lock) {
throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireResourcesLock());
}
@@ -1585,6 +1547,7 @@ public class Case {
}
/**
* @param caseType The type of case (single-user or multi-user).
* @param caseDir The full path of the case directory. The directory
* will be created if it doesn't already exist; if it
* exists, it is ASSUMED it was created by calling
@@ -1594,7 +1557,6 @@ public class Case {
* @param caseNumber The case number, can be the empty string.
* @param examiner The examiner to associate with the case, can be
* the empty string.
* @param caseType The type of case (single-user or multi-user).
*
* @throws CaseActionException if there is a problem creating the case. The
* exception will have a user-friendly message
@@ -1602,24 +1564,12 @@ public class Case {
* exception.
*/
@Messages({
"Case.exceptionMessage.illegalCaseName=Case name contains illegal characters.",
"Case.progressIndicatorTitle.creatingCase=Creating Case",
"Case.progressIndicatorCancelButton.label=Cancel",
"Case.progressMessage.preparing=Preparing...",
"Case.progressMessage.openingCaseResources=<html>Preparing to open case resources.<br>This may take time if another user is upgrading the case.</html>"
})
private void open(String caseDir, String caseDisplayName, String caseNumber, String examiner, CaseType caseType) throws CaseActionException {
/*
* Clean up the display name for the case to make a suitable immutable
* case name.
*/
String caseName;
try {
caseName = displayNameToCaseName(caseDisplayName);
} catch (IllegalCaseNameException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(Bundle.Case_exceptionMessage_illegalCaseName()), ex);
}
private void create(CaseType caseType, String caseDir, String caseDisplayName, String caseNumber, String examiner) throws CaseActionException {
/*
* Set up either a GUI progress indicator or a logging progress
* indicator.
@@ -1648,7 +1598,7 @@ public class Case {
caseLockingExecutor = Executors.newSingleThreadExecutor();
Future<Void> future = caseLockingExecutor.submit(() -> {
if (CaseType.SINGLE_USER_CASE == caseType) {
open(caseDir, caseName, caseDisplayName, caseNumber, examiner, caseType, progressIndicator);
create(caseType, caseDir, caseDisplayName, caseNumber, examiner, progressIndicator);
} else {
/*
* Acquire a shared case directory lock that will be held as
@@ -1657,6 +1607,7 @@ public class Case {
*/
progressIndicator.start(Bundle.Case_progressMessage_openingCaseResources());
acquireSharedCaseDirLock(caseDir);
/*
* Acquire an exclusive case resources lock to ensure only one
* node at a time can create/open/upgrade/close the case
@@ -1665,14 +1616,14 @@ public class Case {
try (CoordinationService.Lock resourcesLock = acquireExclusiveCaseResourcesLock(caseDir)) {
assert (null != resourcesLock);
try {
open(caseDir, caseName, caseDisplayName, caseNumber, examiner, caseType, progressIndicator);
create(caseType, caseDir, caseDisplayName, caseNumber, examiner, progressIndicator);
} catch (CaseActionException ex) {
/*
* Release the case directory lock immediately if there
* was a problem opening the case.
*/
if (CaseType.MULTI_USER_CASE == caseType) {
releaseSharedCaseDirLock(caseName);
releaseSharedCaseDirLock(caseDir);
}
throw ex;
}
@@ -1696,7 +1647,6 @@ public class Case {
*/
try {
future.get();
} catch (InterruptedException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getMessage()), ex);
} catch (ExecutionException ex) {
@@ -1711,18 +1661,20 @@ public class Case {
}
/**
* Creates and opens a new case.
* Creates a new case.
*
* @param caseDir The full path of the case directory. The directory
* will be created if it doesn't already exist; if it
* exists, it is ASSUMED it was created by calling
* createCaseDirectory.
* @param caseDisplayName The display name of case, which may be changed
* later by the user.
* @param caseNumber The case number, can be the empty string.
* @param examiner The examiner to associate with the case, can be
* the empty string.
* @param caseType The type of case (single-user or multi-user).
* @param caseType The type of case (single-user or multi-user).
* @param caseDir The full path of the case directory. The
* directory will be created if it doesn't already
* exist; if it exists, it is ASSUMED it was
* created by calling createCaseDirectory.
* @param caseName The case name.
* @param caseDisplayName The display name of case, which may be changed
* later by the user.
* @param caseNumber The case number, can be the empty string.
* @param examiner The examiner to associate with the case, can be
* the empty string.
* @param progressIndicator A progress indicator.
*
* @throws CaseActionException if there is a problem creating the case. The
* exception will have a user-friendly message
@@ -1730,6 +1682,7 @@ public class Case {
* exception.
*/
@Messages({
"Case.exceptionMessage.illegalCaseName=Case name contains one or more illegal characters (\\,/,:,*,?,<,>,|).",
"Case.progressMessage.creatingCaseDirectory=Creating case directory...",
"Case.progressMessage.creatingCaseDatabase=Creating case database...",
"Case.exceptionMessage.couldNotCreateCaseDatabaseName=Failed to create case database name from case name.",
@@ -1737,7 +1690,7 @@ public class Case {
"Case.exceptionMessage.couldNotCreateMetadataFile=Failed to create case metadata file.",
"Case.exceptionMessage.couldNotCreateCaseDatabase=Failed to create case database."
})
private void open(String caseDir, String caseName, String caseDisplayName, String caseNumber, String examiner, CaseType caseType, ProgressIndicator progressIndicator) throws CaseActionException {
private void create(CaseType caseType, String caseDir, String caseDisplayName, String caseNumber, String examiner, ProgressIndicator progressIndicator) throws CaseActionException {
/*
* Create the case directory, if it does not already exist.
*
@@ -1763,37 +1716,41 @@ public class Case {
Case.createCaseDirectory(caseDir, caseType);
}
/*
* Create a unique and immutable case name from the case display name.
*/
String caseName;
try {
caseName = displayNameToUniqueName(caseDisplayName);
} catch (IllegalCaseNameException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(Bundle.Case_exceptionMessage_illegalCaseName()), ex);
}
/*
* Create the case database.
*/
progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDatabase());
String dbName = null;
try {
if (CaseType.SINGLE_USER_CASE == caseType) {
dbName = SINGLE_USER_CASE_DB_NAME;
} else if (CaseType.MULTI_USER_CASE == caseType) {
dbName = caseNameToCaseDbName(caseName);
}
} catch (IllegalCaseNameException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseDatabaseName(), ex);
}
try {
if (CaseType.SINGLE_USER_CASE == caseType) {
/*
* For single-user cases, the case database is a SQLite database
* physically located in the root of the case directory.
* with a standard name, physically located in the root of the
* case directory.
*/
dbName = SINGLE_USER_CASE_DB_NAME;
this.caseDb = SleuthkitCase.newCase(Paths.get(caseDir, SINGLE_USER_CASE_DB_NAME).toString());
} else if (CaseType.MULTI_USER_CASE == caseType) {
/*
* For multi-user cases, the case database is a PostgreSQL
* database physically located on the database server.
* database with a name derived from the case display name,
* physically located on the database server.
*/
this.caseDb = SleuthkitCase.newCase(dbName, UserPreferences.getDatabaseConnectionInfo(), caseDir);
this.caseDb = SleuthkitCase.newCase(caseDisplayName, UserPreferences.getDatabaseConnectionInfo(), caseDir);
dbName = this.caseDb.getDatabaseName();
}
} catch (TskCoreException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseDatabase(), ex);
} catch (UserPreferencesException ex) {
throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex);
}
@@ -1814,7 +1771,7 @@ public class Case {
/**
* Opens an existing case.
*
* @param caseMetadataFilePath The apth to the case metadata file.
* @param caseMetadataFilePath The path to the case metadata file.
*
* @throws CaseActionException if there is a problem creating the case. The
* exception will have a user-friendly message
@@ -1980,7 +1937,8 @@ public class Case {
"Case.progressMessage.settingUpNetworkCommunications=Setting up network communications...",})
private void openServices(ProgressIndicator progressIndicator) throws CaseActionException {
/*
* Switch to writing to the application logs in the logs subdirectory.
* Switch to writing to the application logs in the logs subdirectory of
* the case directory.
*/
progressIndicator.progress(Bundle.Case_progressMessage_switchingLogDirectory());
Logger.setLogDirectory(getLogDirectoryPath());
@@ -1989,12 +1947,11 @@ public class Case {
* Hook up a SleuthKit layer error reporter.
*/
progressIndicator.progress(Bundle.Case_progressMessage_settingUpTskErrorReporting());
this.sleuthkitErrorReporter = new SleuthkitErrorReporter(MIN_SECS_BETWEEN_TSK_ERROR_REPORTS, NbBundle.getMessage(Case.class, "IntervalErrorReport.ErrorText"));
this.caseDb.addErrorObserver(this.sleuthkitErrorReporter);
/*
* Clear the temp subdirectory.
* Clear the temp subdirectory of the case directory.
*/
progressIndicator.progress(Bundle.Case_progressMessage_clearingTempDirectory());
Case.clearTempSubDir(this.getTempDirectory());
@@ -2335,7 +2292,7 @@ public class Case {
@Messages({"Case.creationException.couldNotAcquireDirLock=Failed to get lock on case directory."})
private void acquireSharedCaseDirLock(String caseDir) throws CaseActionException {
try {
caseDirLock = CoordinationService.getInstance().tryGetSharedLock(CategoryNode.CASES, caseDir, SHARED_DIR_LOCK_TIMOUT_HOURS, TimeUnit.HOURS);
caseDirLock = CoordinationService.getInstance().tryGetSharedLock(CategoryNode.CASES, caseDir, DIR_LOCK_TIMOUT_HOURS, TimeUnit.HOURS);
if (null == caseDirLock) {
throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireDirLock());
}
@@ -2404,7 +2361,7 @@ public class Case {
* An exception to throw when a case name with invalid characters is
* encountered.
*/
public final static class IllegalCaseNameException extends Exception {
final static class IllegalCaseNameException extends Exception {
private static final long serialVersionUID = 1L;
@@ -125,7 +125,7 @@ public final class CaseMetadata {
* created.
*/
CaseMetadata(String caseDirectory, Case.CaseType caseType, String caseName, String caseDisplayName, String caseNumber, String examiner, String caseDatabase) throws CaseMetadataException {
metadataFilePath = Paths.get(caseDirectory, caseName + FILE_EXTENSION);
metadataFilePath = Paths.get(caseDirectory, caseDisplayName + FILE_EXTENSION);
this.caseType = caseType;
this.caseName = caseName;
this.caseDisplayName = caseDisplayName;
@@ -204,25 +204,19 @@ class NewCaseWizardPanel1 implements WizardDescriptor.ValidatingPanel<WizardDesc
@Override
public void validate() throws WizardValidationException {
String caseDisplayName = getComponent().getCaseName();
String caseParentDir = getComponent().getCaseParentDir();
// check if case Name contain one of this following symbol:
// \ / : * ? " < > |
if (!Case.isValidName(caseDisplayName)) {
/*
* Check whether or not the case name is valid. To be valid, the case
* name must not contain any characters that are not allowed in file
* names, since it will be used as the name of the case directory.
*/
String caseName = getComponent().getCaseName();
if (!Case.isValidName(caseName)) {
String errorMsg = NbBundle
.getMessage(this.getClass(), "NewCaseWizardPanel1.validate.errMsg.invalidSymbols");
validationError(errorMsg);
} else {
String caseName = "";
try {
caseName = Case.displayNameToCaseName(caseDisplayName);
} catch (Case.IllegalCaseNameException ex) {
String errorMsg = NbBundle
.getMessage(this.getClass(), "NewCaseWizardPanel1.validate.errMsg.invalidSymbols");
validationError(errorMsg);
}
String caseParentDir = getComponent().getCaseParentDir();
String caseDirPath = caseParentDir + caseName;
// check if the directory exist
@@ -43,7 +43,7 @@ import org.sleuthkit.datamodel.TskData;
/**
* Import a case from single-user to multi-user.
*
*
* DO NOT USE, NEEDS TO BE UPDATED
*/
public class SingleUserCaseConverter {
@@ -175,9 +175,15 @@ public class SingleUserCaseConverter {
}
// Create sanitized names for PostgreSQL and Solr
/*
* RJC: Removed package access sanitizeCaseName method, so this is no
* longer correct, but this whole class is currently out-of-date (out of
* synch with case database schema) and probably belongs in the TSK
* layer anyway, see JIRA-1984.
*/
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); //NON-NLS
Date date = new Date();
String dbName = Case.displayNameToCaseName(icd.getNewCaseName()) + "_" + dateFormat.format(date); //NON-NLS
String dbName = icd.getNewCaseName() + "_" + dateFormat.format(date); //NON-NLS
icd.setPostgreSQLDbName(dbName);
// Copy items to new hostname folder structure
@@ -493,12 +499,12 @@ public class SingleUserCaseConverter {
if (value > biggestPK) {
biggestPK = value;
}
// If the entry contains an encoding type, copy it. Otherwise use NONE.
// The test on column count can be removed if we upgrade the database before conversion.
int encoding = TskData.EncodingType.NONE.getType();
ResultSetMetaData rsMetaData = inputResultSet.getMetaData();
if(rsMetaData.getColumnCount() == 3){
if (rsMetaData.getColumnCount() == 3) {
encoding = inputResultSet.getInt(3);
}
outputStatement.executeUpdate("INSERT INTO tsk_files_path (obj_id, path, encoding_type) VALUES (" //NON-NLS
@@ -64,7 +64,6 @@ import javax.annotation.concurrent.ThreadSafe;
import org.openide.util.Lookup;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.Case.CaseType;
import org.sleuthkit.autopsy.casemodule.Case.IllegalCaseNameException;
import org.sleuthkit.autopsy.casemodule.CaseActionException;
import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
@@ -1880,14 +1879,8 @@ public final class AutoIngestManager extends Observable implements PropertyChang
*/
private Case openCase() throws CoordinationServiceException, CaseManagementException, InterruptedException {
Manifest manifest = currentJob.getManifest();
String caseDisplayName = manifest.getCaseName();
String caseName;
try {
caseName = Case.displayNameToCaseName(caseDisplayName);
} catch (IllegalCaseNameException ex) {
throw new CaseManagementException(String.format("Error creating or opening case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex);
}
SYS_LOGGER.log(Level.INFO, "Opening case {0} ({1}) for {2}", new Object[]{caseDisplayName, caseName, manifest.getFilePath()});
String caseName = manifest.getCaseName();
SYS_LOGGER.log(Level.INFO, "Opening case {0} for {2}", new Object[]{caseName, manifest.getFilePath()});
currentJob.setStage(AutoIngestJob.Stage.OPENING_CASE);
/*
* Acquire and hold a case name lock so that only one node at as
@@ -1917,16 +1910,16 @@ public final class AutoIngestManager extends Observable implements PropertyChang
return caseForJob;
} catch (CaseActionException ex) {
throw new CaseManagementException(String.format("Error creating or opening case %s (%s) for %s", manifest.getCaseName(), caseName, manifest.getFilePath()), ex);
throw new CaseManagementException(String.format("Error creating or opening case %s for %s", caseName, manifest.getFilePath()), ex);
} catch (IllegalStateException ex) {
/*
* Deal with the unfortunate fact that
* Case.getCurrentCase throws IllegalStateException.
*/
throw new CaseManagementException(String.format("Error getting current case %s (%s) for %s", caseName, manifest.getCaseName(), manifest.getFilePath()), ex);
throw new CaseManagementException(String.format("Error getting current case %s for %s", caseName, manifest.getFilePath()), ex);
}
} else {
throw new CaseManagementException(String.format("Timed out acquiring case name lock for %s for %s", manifest.getCaseName(), manifest.getFilePath()));
throw new CaseManagementException(String.format("Timed out acquiring case name lock for %s for %s", caseName, manifest.getFilePath()));
}
}
}
@@ -46,18 +46,12 @@ final class PathUtils {
* @return The path of the case folder, or null if it is not found.
*/
static Path findCaseDirectory(Path folderToSearch, String caseName) {
String sanitizedCaseName;
try {
sanitizedCaseName = Case.displayNameToCaseName(caseName);
} catch (Case.IllegalCaseNameException unused) {
return null;
}
File searchFolder = new File(folderToSearch.toString());
if (!searchFolder.isDirectory()) {
return null;
}
Path caseFolderPath = null;
String[] candidateFolders = searchFolder.list(new CaseFolderFilter(sanitizedCaseName));
String[] candidateFolders = searchFolder.list(new CaseFolderFilter(caseName));
long mostRecentModified = 0;
for (String candidateFolder : candidateFolders) {
File file = new File(candidateFolder);