Autopsy 4.22.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
SolrSearchService.java
Go to the documentation of this file.
1/*
2 * Autopsy Forensic Browser
3 *
4 * Copyright 2015-2021 Basis Technology Corp.
5 * Contact: carrier <at> sleuthkit <dot> org
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19package org.sleuthkit.autopsy.keywordsearch;
20
21import java.awt.Component;
22import java.io.File;
23import java.io.IOException;
24import java.io.Reader;
25import java.net.InetAddress;
26import java.util.ArrayList;
27import java.util.List;
28import java.util.MissingResourceException;
29import java.util.logging.Level;
30import javax.swing.JOptionPane;
31import org.apache.solr.client.solrj.SolrServerException;
32import org.openide.util.NbBundle;
33import org.openide.util.lookup.ServiceProvider;
34import org.openide.util.lookup.ServiceProviders;
35import org.openide.windows.WindowManager;
36import org.sleuthkit.autopsy.appservices.AutopsyService;
37import org.sleuthkit.autopsy.casemodule.Case;
38import org.sleuthkit.autopsy.casemodule.CaseMetadata;
39import org.sleuthkit.autopsy.core.RuntimeProperties;
40import org.sleuthkit.autopsy.coreutils.FileUtil;
41import org.sleuthkit.autopsy.coreutils.Logger;
42import org.sleuthkit.autopsy.ingest.IngestManager;
43import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService;
44import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException;
45import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator;
46import org.sleuthkit.autopsy.progress.ProgressIndicator;
47import org.sleuthkit.autopsy.textextractors.TextExtractor;
48import org.sleuthkit.autopsy.textextractors.TextExtractorFactory;
49import org.sleuthkit.datamodel.BlackboardArtifact;
50import org.sleuthkit.datamodel.Content;
51import org.sleuthkit.datamodel.TskCoreException;
52
61@ServiceProviders(value = {
62 @ServiceProvider(service = KeywordSearchService.class),
63 @ServiceProvider(service = AutopsyService.class)
64})
66
67 private static final String BAD_IP_ADDRESS_FORMAT = "ioexception occurred when talking to server"; //NON-NLS
68 private static final String SERVER_REFUSED_CONNECTION = "server refused connection"; //NON-NLS
69 private static final int IS_REACHABLE_TIMEOUT_MS = 1000;
70 private static final Logger logger = Logger.getLogger(SolrSearchService.class.getName());
71
84 @Override
85 public void index(Content content) throws TskCoreException {
86 if (content == null) {
87 return;
88 }
89 final Ingester ingester = Ingester.getDefault();
90 if (content instanceof BlackboardArtifact) {
91 BlackboardArtifact artifact = (BlackboardArtifact) content;
92 if (artifact.getArtifactID() > 0) {
93 /*
94 * Artifact indexing is only supported for artifacts that use
95 * negative artifact ids to avoid overlapping with the object
96 * ids of other types of Content.
97 */
98 return;
99 }
100 try {
101 Reader blackboardExtractedTextReader = KeywordSearchUtil.getReader(content);
102 String sourceName = artifact.getDisplayName() + "_" + artifact.getArtifactID();
103 ingester.indexMetaDataOnly(artifact, sourceName);
104 // Will not cause an inline search becauce the keyword list is null
105 ingester.search(blackboardExtractedTextReader, artifact.getArtifactID(), sourceName, content, null, true, true, null);
106 } catch (Exception ex) {
107 throw new TskCoreException("Error indexing artifact", ex);
108 }
109 } else {
110 try {
111
112 Reader reader = KeywordSearchUtil.getReader(content);
113 // Will not cause an inline search becauce the keyword list is null
114 ingester.search(reader, content.getId(), content.getName(), content, null, true, true, null);
115 } catch (Exception ex) {
116 throw new TskCoreException("Error indexing content", ex);
117 }
118 // only do a Solr commit if ingest is not running. If ingest is running, the changes will
119 // be committed via a periodic commit or via final commit after the ingest job has finished.
121 ingester.commit();
122 }
123 }
124 }
125
134 @Override
135 public void tryConnect(String host, int port) throws KeywordSearchServiceException {
136 if (host == null || host.isEmpty()) {
137 throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.MissingHostname")); //NON-NLS
138 }
139 try {
140 KeywordSearch.getServer().connectToSolrServer(host, Integer.toString(port));
141 } catch (SolrServerException ex) {
142 logger.log(Level.SEVERE, "Unable to connect to Solr server. Host: " + host + ", port: " + port, ex);
143 throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort")); //NON-NLS*/
144 } catch (IOException ex) {
145 logger.log(Level.SEVERE, "Unable to connect to Solr server. Host: " + host + ", port: " + port, ex);
146 String result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort"); //NON-NLS
147 String message = ex.getCause().getMessage().toLowerCase();
148 if (message.startsWith(SERVER_REFUSED_CONNECTION)) {
149 try {
150 if (InetAddress.getByName(host).isReachable(IS_REACHABLE_TIMEOUT_MS)) {
151 // if we can reach the host, then it's probably port problem
152 result = Bundle.SolrConnectionCheck_Port();
153 } else {
154 result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort"); //NON-NLS
155 }
156 } catch (IOException | MissingResourceException any) {
157 // it may be anything
158 result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort"); //NON-NLS
159 }
160 } else if (message.startsWith(BAD_IP_ADDRESS_FORMAT)) {
161 result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.Hostname"); //NON-NLS
162 }
163 throw new KeywordSearchServiceException(result);
164 } catch (NumberFormatException ex) {
165 logger.log(Level.SEVERE, "Unable to connect to Solr server. Host: " + host + ", port: " + port, ex);
166 throw new KeywordSearchServiceException(Bundle.SolrConnectionCheck_Port());
167 } catch (IllegalArgumentException ex) {
168 logger.log(Level.SEVERE, "Unable to connect to Solr server. Host: " + host + ", port: " + port, ex);
169 throw new KeywordSearchServiceException(ex.getMessage());
170 }
171 }
172
181 @Override
182 public void deleteDataSource(Long dataSourceId) throws KeywordSearchServiceException {
183
184 try {
185 Server ddsServer = KeywordSearch.getServer();
186 ddsServer.deleteDataSource(dataSourceId);
187 } catch (IOException | KeywordSearchModuleException | NoOpenCoreException | SolrServerException ex) {
188 logger.log(Level.WARNING, NbBundle.getMessage(SolrSearchService.class, "SolrSearchService.DeleteDataSource.msg", dataSourceId), ex);
189 throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrSearchService.DeleteDataSource.msg", dataSourceId), ex);
190 }
191 }
192
198 @NbBundle.Messages({
199 "# {0} - case directory", "SolrSearchService.exceptionMessage.noIndexMetadata=Unable to create IndexMetaData from case directory: {0}",
200 "SolrSearchService.exceptionMessage.noCurrentSolrCore=IndexMetadata did not contain a current Solr core so could not delete the case",
201 "# {0} - collection name", "SolrSearchService.exceptionMessage.unableToDeleteCollection=Unable to delete collection {0}",
202 "# {0} - index folder path", "SolrSearchService.exceptionMessage.failedToDeleteIndexFiles=Failed to delete text index files at {0}"
203 })
204 @Override
206 String caseDirectory = metadata.getCaseDirectory();
207 IndexMetadata indexMetadata;
208 try {
209 indexMetadata = new IndexMetadata(caseDirectory);
210 } catch (IndexMetadata.TextIndexMetadataException ex) {
211 logger.log(Level.WARNING, NbBundle.getMessage(SolrSearchService.class, "SolrSearchService.exceptionMessage.noIndexMetadata", caseDirectory), ex);
212 throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrSearchService.exceptionMessage.noIndexMetadata", caseDirectory), ex);
213 }
214
215 if (indexMetadata.getIndexes().isEmpty()) {
216 logger.log(Level.WARNING, NbBundle.getMessage(SolrSearchService.class,
217 "SolrSearchService.exceptionMessage.noCurrentSolrCore"));
218 throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class,
219 "SolrSearchService.exceptionMessage.noCurrentSolrCore"));
220 }
221
222 // delete index(es) for this case
223 for (Index index : indexMetadata.getIndexes()) {
224 try {
225 // Unload/delete the collection on the server and then delete the text index files.
226 KeywordSearch.getServer().deleteCollection(index.getIndexName(), metadata);
227 } catch (KeywordSearchModuleException ex) {
228 throw new KeywordSearchServiceException(Bundle.SolrSearchService_exceptionMessage_unableToDeleteCollection(index.getIndexName()), ex);
229 }
230 File indexDir = new File(index.getIndexPath()).getParentFile();
231 if (indexDir.exists()) {
232 if (!FileUtil.deleteDir(indexDir)) {
233 throw new KeywordSearchServiceException(Bundle.SolrSearchService_exceptionMessage_failedToDeleteIndexFiles(index.getIndexPath()));
234 }
235 }
236 }
237 }
238
239 @Override
240 public String getServiceName() {
241 return NbBundle.getMessage(this.getClass(), "SolrSearchService.ServiceName");
242 }
243
252 @Override
253 @NbBundle.Messages({
254 "SolrSearch.lookingForMetadata.msg=Looking for text index metadata file",
255 "SolrSearch.readingIndexes.msg=Reading text index metadata file",
256 "SolrSearch.findingIndexes.msg=Looking for existing text index directories",
257 "SolrSearch.creatingNewIndex.msg=Creating new text index",
258 "SolrSearch.checkingForLatestIndex.msg=Looking for text index with latest Solr and schema version",
259 "SolrSearch.indentifyingIndex.msg=Identifying text index to use",
260 "SolrSearch.openCore.msg=Opening text index. For large cases this may take several minutes.",
261 "# {0} - futureVersion", "# {1} - currentVersion",
262 "SolrSearch.futureIndexVersion.msg=The text index for the case is for Solr {0}. This version of Autopsy is compatible with Solr {1}.",
263 "SolrSearch.unableToFindIndex.msg=Unable to find index that can be used for this case",
264 "SolrSearch.complete.msg=Text index successfully opened"})
266 if (context.cancelRequested()) {
267 return;
268 }
269
271 int totalNumProgressUnits = 7;
272 int progressUnitsCompleted = 0;
273
274 String caseDirPath = context.getCase().getCaseDirectory();
275 Case theCase = context.getCase();
276 List<Index> indexes = new ArrayList<>();
277 progress.progress(Bundle.SolrSearch_lookingForMetadata_msg(), totalNumProgressUnits);
278 if (IndexMetadata.isMetadataFilePresent(caseDirPath)) {
279 try {
280 // metadata file exists, get list of existing Solr cores for this case
281 progressUnitsCompleted++;
282 progress.progress(Bundle.SolrSearch_findingIndexes_msg(), progressUnitsCompleted);
283 IndexMetadata indexMetadata = new IndexMetadata(caseDirPath);
284 indexes = indexMetadata.getIndexes();
285 } catch (IndexMetadata.TextIndexMetadataException ex) {
286 logger.log(Level.SEVERE, String.format("Unable to read text index metadata file"), ex);
287 throw new AutopsyServiceException("Unable to read text index metadata file", ex);
288 }
289 }
290
291 if (context.cancelRequested()) {
292 return;
293 }
294
295 // check if we found any existing indexes
296 Index currentVersionIndex = null;
297 if (indexes.isEmpty()) {
298 // new case that doesn't have an existing index. create new index folder
299 progressUnitsCompleted++;
300 progress.progress(Bundle.SolrSearch_creatingNewIndex_msg(), progressUnitsCompleted);
301 currentVersionIndex = IndexFinder.createLatestVersionIndex(theCase);
302 // add current index to the list of indexes that exist for this case
303 indexes.add(currentVersionIndex);
304 } else {
305 // check if one of the existing indexes is for latest Solr version and schema
306 progressUnitsCompleted++;
307 progress.progress(Bundle.SolrSearch_checkingForLatestIndex_msg(), progressUnitsCompleted);
308 currentVersionIndex = IndexFinder.findLatestVersionIndex(indexes);
309 if (currentVersionIndex == null) {
310 // found existing index(es) but none were for latest Solr version and schema version
311 progressUnitsCompleted++;
312 progress.progress(Bundle.SolrSearch_indentifyingIndex_msg(), progressUnitsCompleted);
313 Index indexToUse = IndexFinder.identifyIndexToUse(indexes);
314 if (indexToUse == null) {
315 // unable to find index that can be used. check if the available index is for a "future" version of Solr,
316 // i.e. the user is using an "old/legacy" version of Autopsy to open cases created by later versions of Autopsy.
317 String futureIndexVersion = IndexFinder.isFutureIndexPresent(indexes);
318 if (!futureIndexVersion.isEmpty()) {
319 throw new AutopsyServiceException(Bundle.SolrSearch_futureIndexVersion_msg(futureIndexVersion, IndexFinder.getCurrentSolrVersion()));
320 }
321 throw new AutopsyServiceException(Bundle.SolrSearch_unableToFindIndex_msg());
322 }
323
324
325
326 if (context.cancelRequested()) {
327 return;
328 }
329
330 if (!IndexFinder.getCurrentSolrVersion().equals(indexToUse.getSolrVersion())) {
331 Index prevIndex = indexToUse;
332 indexToUse = tryUpgradeSolrVersion(context, indexToUse);
333 if (indexToUse != prevIndex) {
334 indexes.add(indexToUse);
335 }
336 }
337
338 if (context.cancelRequested()) {
339 return;
340 }
341
342 // check if schema is compatible
343 if (!indexToUse.isCompatible(IndexFinder.getCurrentSchemaVersion())) {
344 String msg = "Text index schema version " + indexToUse.getSchemaVersion() + " is not compatible with current schema";
345 logger.log(Level.WARNING, msg);
346 throw new AutopsyServiceException(msg);
347 }
348 // proceed with case open
349 currentVersionIndex = indexToUse;
350 }
351 }
352
353 try {
354 // update text index metadata file
355 if (!indexes.isEmpty()) {
356 IndexMetadata indexMetadata = new IndexMetadata(caseDirPath, indexes);
357 }
358 } catch (IndexMetadata.TextIndexMetadataException ex) {
359 throw new AutopsyServiceException("Failed to save Solr core info in text index metadata file", ex);
360 }
361
362 // open core
363 try {
364 progress.progress(Bundle.SolrSearch_openCore_msg(), totalNumProgressUnits - 1);
365 KeywordSearch.getServer().openCoreForCase(theCase, currentVersionIndex);
366 } catch (KeywordSearchModuleException ex) {
367 throw new AutopsyServiceException(String.format("Failed to open or create core for %s", caseDirPath), ex);
368 }
369 if (context.cancelRequested()) {
370 return;
371 }
372
373 theCase.getSleuthkitCase().registerForEvents(this);
374
375 progress.progress(Bundle.SolrSearch_complete_msg(), totalNumProgressUnits);
376 }
377
378
379 private static final long WAIT_TIME_MILLIS = 2000;
380
388 @NbBundle.Messages({
389 "Server_configureSolrConnection_unsupportedSolrTitle=Unsupported Keyword Search in Case",
390 "# {0} - solrVersion",
391 "# {1} - caseName",
392 "Server_configureSolrConnection_unsupportedSolrDesc=<html><body><p style=\"width: 400px\">This case was made with an older version of Keyword Search that is no longer supported. You can continue without upgrading, but some Keyword Search functionality will not be usable while the case is open, and you will encounter errors. You can also choose to upgrade the Keyword Search version for the case. If you choose to do this, you will need to run Keyword Search with Solr indexing selected in order to use features like ad hoc search with images in the case.</p></body></html>",
393 "Server_configureSolrConnection_unsupportedSolrDisableOpt=Continue",
394 "Server_configureSolrConnection_unsupportedSolrUpgradeOpt=Upgrade Solr Core"
395 })
396 private Index tryUpgradeSolrVersion(CaseContext context, Index index) throws AutopsyServiceException {
397 // if not, attempt to fix issue
399 Component parentComponent = WindowManager.getDefault().getMainWindow();
400 if (context.getProgressIndicator() instanceof ModalDialogProgressIndicator progInd && progInd.getDialog() != null) {
401 parentComponent = progInd.getDialog();
402
403 }
404
405 if (context.cancelRequested()) {
406 return index;
407 }
408
409 try {
410 // progress updates occur right before this in the same window, so there is the possibility that
411 // the progress window will update just after the option pane is shown causing the option pane to
412 // not be visible or selectable. This sleep is added to give the window enough time to finish
413 Thread.sleep(WAIT_TIME_MILLIS);
414 } catch (InterruptedException ex) {
415 // just proceed if interrupted
416 }
417
418 if (context.cancelRequested()) {
419 return index;
420 }
421
422 int selection = JOptionPane.showOptionDialog(
423 parentComponent,
424 Bundle.Server_configureSolrConnection_unsupportedSolrDesc(index.getSolrVersion(), context.getCase().getDisplayName()),
425 Bundle.Server_configureSolrConnection_unsupportedSolrTitle(),
426 JOptionPane.YES_NO_OPTION,
427 JOptionPane.WARNING_MESSAGE,
428 null,
429 new Object[]{
430 Bundle.Server_configureSolrConnection_unsupportedSolrDisableOpt(),
431 Bundle.Server_configureSolrConnection_unsupportedSolrUpgradeOpt()
432 },
433 Bundle.Server_configureSolrConnection_unsupportedSolrDisableOpt());
434
435 if (selection == 1) {
436 return IndexFinder.createLatestVersionIndex(context.getCase());
437 }
438 }
439
440 throw new AutopsyServiceException("Unsupported Keyword Search (Solr " + index.getSolrVersion() + ")");
441 }
442
451 @Override
453 /*
454 * TODO (JIRA 2525): The following code KeywordSearch.CaseChangeListener
455 * gambles that any BlackboardResultWriters (SwingWorkers) will complete
456 * in less than roughly two seconds. This stuff should be reworked using
457 * an ExecutorService and tasks with Futures.
458 */
459 AdHocSearchChildFactory.BlackboardResultWriter.stopAllWriters();
460 try {
461 Thread.sleep(2000);
462 } catch (InterruptedException ex) {
463 logger.log(Level.SEVERE, "Unexpected interrupt while waiting for BlackboardResultWriters to terminate", ex);
464 }
465
466 try {
467 KeywordSearch.getServer().closeCore();
468 } catch (KeywordSearchModuleException ex) {
469 throw new AutopsyServiceException(String.format("Failed to close core for %s", context.getCase().getCaseDirectory()), ex);
470 }
471
472 if (context.getCase().getSleuthkitCase() != null) {
473 context.getCase().getSleuthkitCase().unregisterForEvents(this);
474 }
475 }
476
486 @Deprecated
487 @Override
488 public void indexArtifact(BlackboardArtifact artifact) throws TskCoreException {
489 if (artifact == null) {
490 return;
491 }
492
493 // We only support artifact indexing for Autopsy versions that use
494 // the negative range for artifact ids.
495 if (artifact.getArtifactID() > 0) {
496 return;
497 }
498 final Ingester ingester = Ingester.getDefault();
499
500 try {
501 String sourceName = artifact.getDisplayName() + "_" + artifact.getArtifactID();
502 TextExtractor blackboardExtractor = TextExtractorFactory.getExtractor(artifact, null);
503 Reader blackboardExtractedTextReader = blackboardExtractor.getReader();
504 ingester.indexMetaDataOnly(artifact, sourceName);
505 ingester.search(blackboardExtractedTextReader, artifact.getId(), sourceName, artifact, null, true, true, null);
506 } catch (Exception ex) {
507 throw new TskCoreException(ex.getCause().getMessage(), ex);
508 }
509 }
510}
static boolean deleteDir(File dirPath)
Definition FileUtil.java:47
synchronized static Logger getLogger(String name)
Definition Logger.java:124
static synchronized IngestManager getInstance()
Index tryUpgradeSolrVersion(CaseContext context, Index index)
static TextExtractor getExtractor(Content content, Lookup context)

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