This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch TIKA-4809-stage-7 in repository https://gitbox.apache.org/repos/asf/tika.git
commit c8017866456671f9d8b1890adf40a03ab7fbdc6b Author: tallison <[email protected]> AuthorDate: Mon Aug 10 11:28:14 2026 -0400 TIKA-4809: Make silent config and load failures visible --- .../java/org/apache/tika/config/ServiceLoader.java | 9 +- .../charsoup/CharSoupMetadataFilter.java | 8 ++ .../metadatafilter/OpenNLPMetadataFilter.java | 8 ++ .../metadatafilter/OptimaizeMetadataFilter.java | 8 ++ .../apache/tika/server/core/TikaServerProcess.java | 95 +++++++++++++++------- 5 files changed, 99 insertions(+), 29 deletions(-) diff --git a/tika-core/src/main/java/org/apache/tika/config/ServiceLoader.java b/tika-core/src/main/java/org/apache/tika/config/ServiceLoader.java index ad2c616f0a..0bd661a445 100644 --- a/tika-core/src/main/java/org/apache/tika/config/ServiceLoader.java +++ b/tika-core/src/main/java/org/apache/tika/config/ServiceLoader.java @@ -35,6 +35,8 @@ import java.util.Set; import java.util.regex.Pattern; import aQute.bnd.annotation.spi.ServiceConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.tika.detect.Detector; import org.apache.tika.detect.EncodingDetector; @@ -59,6 +61,8 @@ import org.apache.tika.utils.ServiceLoaderUtils; @ServiceConsumer(MetadataFilter.class) public class ServiceLoader { + private static final Logger LOG = LoggerFactory.getLogger(ServiceLoader.class); + /** * The dynamic set of services available in an OSGi environment. * Managed by the {@link TikaActivator} class and used as an additional @@ -339,7 +343,10 @@ public class ServiceLoader { "Class " + name + " is not of type: " + iface); } } catch (Throwable t) { - //TODO: swallow, log, throw? + // Swallowed so one bad provider cannot break loading of the rest, + // but logged: an SPI class that silently fails to load looks exactly + // like one that was never registered. + LOG.warn("failed to load service provider {} for {}", name, iface, t); } } } diff --git a/tika-langdetect/tika-langdetect-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupMetadataFilter.java b/tika-langdetect/tika-langdetect-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupMetadataFilter.java index 64561ea00c..430e066b16 100644 --- a/tika-langdetect/tika-langdetect-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupMetadataFilter.java +++ b/tika-langdetect/tika-langdetect-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupMetadataFilter.java @@ -15,6 +15,9 @@ * limitations under the License. */ package org.apache.tika.langdetect.charsoup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.tika.annotation.TikaComponent; import org.apache.tika.langdetect.charsoup.core.CharSoupFeatureExtractor; import org.apache.tika.language.detect.LanguageResult; @@ -39,6 +42,8 @@ import org.apache.tika.metadata.filter.MetadataFilterBase; @TikaComponent(name = "charsoup-metadata-filter") public class CharSoupMetadataFilter extends MetadataFilterBase { + private static final Logger LOG = LoggerFactory.getLogger(CharSoupMetadataFilter.class); + private int maxLength = CharSoupFeatureExtractor.MAX_TEXT_LENGTH; public void setMaxLength(int maxLength) { @@ -49,6 +54,9 @@ public class CharSoupMetadataFilter extends MetadataFilterBase { public void filter(Metadata metadata) { String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); if (content == null || content.isEmpty()) { + // No tk:content to detect from -- e.g. the ignore handler, which /meta pins. + // Silence here reads as "no language found" rather than "never ran". + LOG.debug("no content to detect language from; filter is a no-op for this document"); return; } CharSoupLanguageDetector detector = new CharSoupLanguageDetector(); diff --git a/tika-langdetect/tika-langdetect-opennlp/src/main/java/org/apache/tika/langdetect/opennlp/metadatafilter/OpenNLPMetadataFilter.java b/tika-langdetect/tika-langdetect-opennlp/src/main/java/org/apache/tika/langdetect/opennlp/metadatafilter/OpenNLPMetadataFilter.java index 754261aec4..c448e288c0 100644 --- a/tika-langdetect/tika-langdetect-opennlp/src/main/java/org/apache/tika/langdetect/opennlp/metadatafilter/OpenNLPMetadataFilter.java +++ b/tika-langdetect/tika-langdetect-opennlp/src/main/java/org/apache/tika/langdetect/opennlp/metadatafilter/OpenNLPMetadataFilter.java @@ -16,6 +16,9 @@ */ package org.apache.tika.langdetect.opennlp.metadatafilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.tika.annotation.TikaComponent; import org.apache.tika.langdetect.opennlp.OpenNLPDetector; import org.apache.tika.language.detect.LanguageResult; @@ -26,6 +29,8 @@ import org.apache.tika.metadata.filter.MetadataFilterBase; @TikaComponent public class OpenNLPMetadataFilter extends MetadataFilterBase { + private static final Logger LOG = LoggerFactory.getLogger(OpenNLPMetadataFilter.class); + private int maxCharsForDetection = 10000; public void setMaxCharsForDetection(int maxCharsForDetection) { @@ -38,6 +43,9 @@ public class OpenNLPMetadataFilter extends MetadataFilterBase { detector.setMaxLength(maxCharsForDetection); String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); if (content == null) { + // No tk:content to detect from -- e.g. the ignore handler, which /meta pins. + // Silence here reads as "no language found" rather than "never ran". + LOG.debug("no content to detect language from; filter is a no-op for this document"); return; } LanguageResult r = detector.detect(content); diff --git a/tika-langdetect/tika-langdetect-optimaize/src/main/java/org/apache/tika/langdetect/optimaize/metadatafilter/OptimaizeMetadataFilter.java b/tika-langdetect/tika-langdetect-optimaize/src/main/java/org/apache/tika/langdetect/optimaize/metadatafilter/OptimaizeMetadataFilter.java index 44babda274..62f85959fc 100644 --- a/tika-langdetect/tika-langdetect-optimaize/src/main/java/org/apache/tika/langdetect/optimaize/metadatafilter/OptimaizeMetadataFilter.java +++ b/tika-langdetect/tika-langdetect-optimaize/src/main/java/org/apache/tika/langdetect/optimaize/metadatafilter/OptimaizeMetadataFilter.java @@ -16,6 +16,9 @@ */ package org.apache.tika.langdetect.optimaize.metadatafilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.tika.annotation.TikaComponent; import org.apache.tika.langdetect.optimaize.OptimaizeLangDetector; import org.apache.tika.language.detect.LanguageResult; @@ -26,6 +29,8 @@ import org.apache.tika.metadata.filter.MetadataFilterBase; @TikaComponent public class OptimaizeMetadataFilter extends MetadataFilterBase { + private static final Logger LOG = LoggerFactory.getLogger(OptimaizeMetadataFilter.class); + private int maxCharsForDetection = OptimaizeLangDetector.DEFAULT_MAX_CHARS_FOR_DETECTION; public void setMaxCharsForDetection(int maxCharsForDetection) { @@ -38,6 +43,9 @@ public class OptimaizeMetadataFilter extends MetadataFilterBase { detector.loadModels(); String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); if (content == null) { + // No tk:content to detect from -- e.g. the ignore handler, which /meta pins. + // Silence here reads as "no language found" rather than "never ran". + LOG.debug("no content to detect language from; filter is a no-op for this document"); return; } LanguageResult r = detector.detect(content); diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java index 93bde62770..99095fc5ad 100644 --- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java +++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java @@ -105,6 +105,10 @@ public class TikaServerProcess { private static final Logger LOG = LoggerFactory.getLogger(TikaServerProcess.class); public static int DO_NOT_RESTART_EXIT_VALUE = -100; + private static final List<String> VALID_ENDPOINTS = List.of("tika", "rmeta", "meta", + "unpack", "detect", "language", "translate", "mime", "mime-types", "detectors", + "parsers", "version", "status", "pipes", "async"); + private static Options getOptions() { Options options = new Options(); options.addOption("h", "host", true, "host name, use * for all)"); @@ -163,6 +167,35 @@ public class TikaServerProcess { LOG.info("Started Apache Tika server {} at {}", serverDetails.serverId, serverDetails.url); } + /** + * Warns for settings that widen what a caller can reach. The security prose already + * exists, but only in the message shown when a flag is *not* set -- i.e. only to the + * operators who did not enable the risky thing. + */ + private static void warnOnPermissiveConfig(TikaServerConfig config) { + if (config.isAllowPipes()) { + LOG.warn("allowPipes is enabled: /pipes and /async can fetch and emit through " + + "configured fetchers/emitters, reaching files and network resources at " + + "the server's privilege level. Expose only to trusted callers."); + } + if (config.isAllowPerRequestConfig()) { + LOG.warn("allowPerRequestConfig is enabled: callers may set any parser option, " + + "including options that spawn external processes such as OCR."); + } + if (config.isReturnStackTrace()) { + LOG.warn("returnStackTrace is enabled: error responses include server-side stack " + + "traces. Intended for development, not production."); + } + if ("*".equals(config.getCors())) { + LOG.warn("cors is '*': any origin may call this server from a browser."); + } + if ("0.0.0.0".equals(config.getHost()) || "*".equals(config.getHost())) { + LOG.warn("host is {}: the server is reachable on every interface. tika-server " + + "performs no authentication; restrict access at the network layer.", + config.getHost()); + } + } + //This returns the server, configured and ready to be started. private static ServerDetails initServer(TikaServerConfig tikaServerConfig) throws Exception { String host = tikaServerConfig.getHost(); @@ -204,6 +237,8 @@ public class TikaServerProcess { sf.setOutInterceptors(Collections.singletonList(new GZIPOutInterceptor())); sf.setInInterceptors(Collections.singletonList(new GZIPInInterceptor())); + warnOnPermissiveConfig(tikaServerConfig); + String protocol = tikaServerConfig .getTlsConfig() .isActive() ? "https" : "http"; @@ -370,34 +405,38 @@ public class TikaServerProcess { // this default set. } else { for (String endPoint : tikaServerConfig.getEndpoints()) { - if ("meta".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new MetadataResource(tikaResource))); - } else if ("rmeta".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new RecursiveMetadataResource(tikaResource))); - } else if ("detect".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new DetectorResource(serverStatus, tikaResource))); - } else if ("language".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new LanguageResource())); - } else if ("translate".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new TranslateResource(serverStatus))); - } else if ("tika".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(tikaResource)); - } else if ("unpack".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new UnpackerResource(tikaResource))); - } else if ("mime".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new TikaMimeTypes(tikaResource))); - } else if ("detectors".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new TikaDetectors(tikaResource))); - } else if ("parsers".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new TikaParsers(tikaResource))); - } else if ("version".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new TikaVersion())); - } else if ("pipes".equals(endPoint)) { - addPipesResource = true; - } else if ("async".equals(endPoint)) { - addAsyncResource = true; - } else if ("status".equals(endPoint)) { - resourceProviders.add(new SingletonResourceProvider(new TikaServerStatus(serverStatus))); + switch (endPoint) { + case "meta" -> resourceProviders.add( + new SingletonResourceProvider(new MetadataResource(tikaResource))); + case "rmeta" -> resourceProviders.add( + new SingletonResourceProvider(new RecursiveMetadataResource(tikaResource))); + case "detect" -> resourceProviders.add( + new SingletonResourceProvider(new DetectorResource(serverStatus, tikaResource))); + case "language" -> resourceProviders.add( + new SingletonResourceProvider(new LanguageResource())); + case "translate" -> resourceProviders.add( + new SingletonResourceProvider(new TranslateResource(serverStatus))); + case "tika" -> resourceProviders.add( + new SingletonResourceProvider(tikaResource)); + case "unpack" -> resourceProviders.add( + new SingletonResourceProvider(new UnpackerResource(tikaResource))); + // "mime-types" accepted too: that is the path, and it is the natural guess. + case "mime", "mime-types" -> resourceProviders.add( + new SingletonResourceProvider(new TikaMimeTypes(tikaResource))); + case "detectors" -> resourceProviders.add( + new SingletonResourceProvider(new TikaDetectors(tikaResource))); + case "parsers" -> resourceProviders.add( + new SingletonResourceProvider(new TikaParsers(tikaResource))); + case "version" -> resourceProviders.add( + new SingletonResourceProvider(new TikaVersion())); + case "status" -> resourceProviders.add( + new SingletonResourceProvider(new TikaServerStatus(serverStatus))); + case "pipes" -> addPipesResource = true; + case "async" -> addAsyncResource = true; + // Refuse rather than skip: a dropped name is a missing endpoint at + // runtime with nothing in the log pointing at the config. + default -> throw new TikaConfigException("Unrecognized endpoint '" + endPoint + + "'. Valid endpoints: " + VALID_ENDPOINTS); } } }
