Autopsy  4.21.0
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  */
19 package org.sleuthkit.autopsy.keywordsearch;
20 
21 import java.awt.Component;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.Reader;
25 import java.net.InetAddress;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.MissingResourceException;
29 import java.util.logging.Level;
30 import javax.swing.JOptionPane;
31 import org.apache.solr.client.solrj.SolrServerException;
32 import org.openide.util.NbBundle;
33 import org.openide.util.lookup.ServiceProvider;
34 import org.openide.util.lookup.ServiceProviders;
35 import org.openide.windows.WindowManager;
49 import org.sleuthkit.datamodel.BlackboardArtifact;
50 import org.sleuthkit.datamodel.Content;
51 import 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 
270  ProgressIndicator progress = context.getProgressIndicator();
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 synchronized IngestManager getInstance()
static TextExtractor getExtractor(Content content, Lookup context)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static boolean deleteDir(File dirPath)
Definition: FileUtil.java:47
Index tryUpgradeSolrVersion(CaseContext context, Index index)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Feb 6 2024
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.