This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 979136ba1e fix(security): upgrade Jetty 11.0.26 -> 12.0.35 to fix
CVE-2026-2332 (#2894)
979136ba1e is described below
commit 979136ba1e2bacbbed45db5da3602256d02b8115
Author: srujana-kuntumalla
<[email protected]>
AuthorDate: Tue Jun 23 06:27:35 2026 -0400
fix(security): upgrade Jetty 11.0.26 -> 12.0.35 to fix CVE-2026-2332 (#2894)
---
...PipesBiDirectionalStreamingIntegrationTest.java | 5 +-
.../tika-pipes-solr-integration-tests/pom.xml | 12 ++
.../pipes/solr/tests/TikaPipesSolrTestBase.java | 10 +-
tika-parent/pom.xml | 67 ++++---
.../org/apache/tika/client/HttpClientFactory.java | 4 +
.../tika-pipes-plugins/tika-pipes-solr/pom.xml | 15 ++
.../tika/pipes/emitter/solr/SolrEmitter.java | 32 ++--
.../tika/pipes/emitter/solr/SolrEmitterConfig.java | 8 +-
.../pipes/iterator/solr/SolrPipesIterator.java | 38 ++--
.../iterator/solr/SolrPipesIteratorConfig.java | 7 +
.../tika/pipes/plugin/solr/SolrClientHelper.java | 136 +++++++++++++
.../pipes/plugin/solr/SolrClientHelperTest.java | 213 +++++++++++++++++++++
tika-server/tika-server-core/pom.xml | 6 +-
13 files changed, 471 insertions(+), 82 deletions(-)
diff --git
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
index 9a02b603f8..508c672e73 100644
---
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
+++
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
@@ -43,7 +43,6 @@ import org.apache.commons.io.FileUtils;
import org.awaitility.Awaitility;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ResourceHandler;
-import org.eclipse.jetty.util.resource.PathResource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
@@ -92,9 +91,7 @@ class PipesBiDirectionalStreamingIntegrationTest {
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setDirAllowed(true);
- // TODO when using jetty 12:
- //
resourceHandler.setBaseResourceAsString("src/test/resources/test-files")
- resourceHandler.setBaseResource(new PathResource(Paths.get("src",
"test", "resources", "test-files")));
+
resourceHandler.setBaseResourceAsString("src/test/resources/test-files");
httpServer.setHandler(resourceHandler);
httpServer.start();
diff --git a/tika-integration-tests/tika-pipes-solr-integration-tests/pom.xml
b/tika-integration-tests/tika-pipes-solr-integration-tests/pom.xml
index 67f29d4a78..b869781774 100644
--- a/tika-integration-tests/tika-pipes-solr-integration-tests/pom.xml
+++ b/tika-integration-tests/tika-pipes-solr-integration-tests/pom.xml
@@ -76,6 +76,18 @@
<version>${solrj.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.solr</groupId>
+ <artifactId>solr-solrj-jetty</artifactId>
+ <version>${solrj.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.solr</groupId>
+ <artifactId>solr-solrj-zookeeper</artifactId>
+ <version>${solrj.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
diff --git
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
index a4fc8f3abc..35ab7a42e1 100644
---
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
+++
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
@@ -36,8 +36,8 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrQuery;
-import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
+import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.common.SolrInputDocument;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach;
@@ -190,7 +190,7 @@ public abstract class TikaPipesSolrTestBase {
}
LOG.info("Created Solr collection '{}': {}", collection,
createResult.getStdout().trim());
- try (SolrClient solrClient = new
Http2SolrClient.Builder(solrEndpoint).build()) {
+ try (SolrClient solrClient = new
HttpJettySolrClient.Builder(solrEndpoint).build()) {
addBasicSchemaFields(solrEndpoint + "/" + collection);
addSchemaFieldsForNestedDocs(solrEndpoint + "/" + collection);
@@ -262,7 +262,7 @@ public abstract class TikaPipesSolrTestBase {
TikaCLI.main(new String[]{"-a", "-c",
tikaConfigFile.toAbsolutePath().toString()});
- try (SolrClient solrClient = new
Http2SolrClient.Builder(solrEndpoint).build()) {
+ try (SolrClient solrClient = new
HttpJettySolrClient.Builder(solrEndpoint).build()) {
solrClient.commit(collection, true, true);
assertEquals(numDocs, solrClient.query(collection,
new SolrQuery("mime_s:text/html*")).getResults()
@@ -296,7 +296,7 @@ public abstract class TikaPipesSolrTestBase {
TikaCLI.main(new String[]{"-a", "-c",
tikaConfigFile.toAbsolutePath().toString()});
- try (SolrClient solrClient = new
Http2SolrClient.Builder(solrEndpoint).build()) {
+ try (SolrClient solrClient = new
HttpJettySolrClient.Builder(solrEndpoint).build()) {
solrClient.commit(collection, true, true);
assertEquals(numDocs, solrClient.query(collection,
new SolrQuery("mime_s:text/html*")).getResults()
diff --git a/tika-parent/pom.xml b/tika-parent/pom.xml
index c175ef3d5a..32c7ae4ee0 100644
--- a/tika-parent/pom.xml
+++ b/tika-parent/pom.xml
@@ -342,9 +342,9 @@
<commons.math3.version>3.6.1</commons.math3.version>
<commons.net.version>3.12.0</commons.net.version>
<ctakes.version>6.0.0</ctakes.version>
- <!-- can't use 4.1.0, fails in tika server core because it uses jetty 12,
which we can't
- use due to the problems explained there -->
- <cxf.version>4.0.11</cxf.version>
+ <!-- Upgraded to 4.1.7 (from 4.0.11) as part of the Jetty 12 migration
(CVE-2026-2332).
+ CXF 4.1.x ships cxf-rt-transports-http-jetty built against Jetty 12.
-->
+ <cxf.version>4.1.7</cxf.version>
<ddplist.version>1.29</ddplist.version>
<dl4j.version>1.0.0-M2.1</dl4j.version>
<fakeload.version>0.7.0</fakeload.version>
@@ -372,6 +372,7 @@
<jai.imageio.core.version>1.4.0</jai.imageio.core.version>
<jakarta.activation.version>2.1.4</jakarta.activation.version>
<jakarta.annotation.version>3.0.0</jakarta.annotation.version>
+ <jakarta.servlet.version>6.0.0</jakarta.servlet.version>
<jakarta.ws.rs.version>4.0.0</jakarta.ws.rs.version>
<jakarta.xml.bind.version>4.0.5</jakarta.xml.bind.version>
<jakarta.xml.soap.version>3.0.2</jakarta.xml.soap.version>
@@ -382,18 +383,14 @@
<jcommander.version>1.82</jcommander.version>
<jdom2.version>2.0.6.1</jdom2.version>
<jempbox.version>1.8.17</jempbox.version>
- <!-- can't update to jetty 12 because of problem in solr integration tests
- due to Http2SolrClient (solrj 9.10.0)
- (only on the CI, not on local Windows with Docker, see comment in
TIKA-4327 on 14.12.2024)
- expecting org.eclipse.jetty.client.util.InputStreamResponseListener
which is only available
- in Jetty up to 11.0.26
- but this class is now in org.eclipse.jetty.client, see also
+ <!-- Upgraded from 11.0.26 to 12.0.35 to fix CVE-2026-2332 (HTTP/1.1
request smuggling,
+ CVSS 9.1 Critical). Jetty 11.x is EOL and 11.0.28 was never released;
the fix
+ is only available in the Jetty 12 line (>=12.0.33). Jetty 12.0.x
requires Java 17,
+ the same minimum as this project. Migration guide:
https://jetty.org/docs/jetty/12/programming-guide/migration/11-to-12.html
- when updating, see also TODO in
PipesBiDirectionalStreamingIntegrationTest
- and add jakarta.servlet jakarta.servlet-api 6.0.0 to tika-server-core
-->
- <jetty.version>11.0.26</jetty.version>
- <jetty.http2.version>11.0.26</jetty.http2.version>
+ <jetty.version>12.0.35</jetty.version>
+ <jetty.http2.version>12.0.35</jetty.http2.version>
<jhighlight.version>2.0.0</jhighlight.version>
<jna.version>5.19.1</jna.version>
<json.simple.version>1.1.1</json.simple.version>
@@ -441,13 +438,14 @@
<slf4j.version>2.0.18</slf4j.version>
<sis.version>1.6</sis.version>
<snappy.version>1.1.10.8</snappy.version>
- <!-- TODO (incomplete) if updating to solrj 10:
-
https://solr.apache.org/guide/solr/latest/upgrade-notes/major-changes-in-solr-10.html
- add solr-solrj-jetty artifact, add log4j
- Http2SolrClient -> HttpJettySolrClient
- LBHttpSolrClient -> LBHttp2SolrClient -> LBJettySolrClient (doesn't work)
- -->
- <solrj.version>9.10.1</solrj.version>
+ <!-- Upgraded to 10.0.0 (from 9.10.1): SolrJ 9.x bundles Jetty 11 HTTP
client APIs
+ internally (Http2SolrClient / solr-solrj-jetty), which conflict with
Jetty 12.
+ SolrJ 10 renames Http2SolrClient -> HttpJettySolrClient (in
solr-solrj-jetty)
+ and LBHttpSolrClient -> LBJettySolrClient; the solr-solrj-jetty
artifact must
+ be added alongside solr-solrj wherever those classes are used.
+ NOTE: proxy and basic auth are wired via HttpJettySolrClient.Builder;
+ only basic auth is supported (non-basic schemes are rejected at
startup). -->
+ <solrj.version>10.0.0</solrj.version>
<spring.version>7.0.8</spring.version>
<sqlite.version>3.53.2.0</sqlite.version>
<stax.ex.version>2.1.0</stax.ex.version>
@@ -574,30 +572,30 @@
<type>pom</type>
<scope>import</scope>
</dependency>
- <!-- avoid outdated http2 dependencies when using solr 8 -->
+ <!-- Jetty 12 renamed all http2 artifacts with a jetty- prefix -->
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-http-client-transport</artifactId>
+ <artifactId>jetty-http2-client-transport</artifactId>
<version>${jetty.http2.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-hpack</artifactId>
+ <artifactId>jetty-http2-hpack</artifactId>
<version>${jetty.http2.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-client</artifactId>
+ <artifactId>jetty-http2-client</artifactId>
<version>${jetty.http2.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-common</artifactId>
+ <artifactId>jetty-http2-common</artifactId>
<version>${jetty.http2.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-server</artifactId>
+ <artifactId>jetty-http2-server</artifactId>
<version>${jetty.http2.version}</version>
</dependency>
<dependency>
@@ -821,6 +819,11 @@
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jakarta.xml.bind.version}</version>
</dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>${jakarta.servlet.version}</version>
+ </dependency>
<dependency>
<groupId>jakarta.xml.soap</groupId>
<artifactId>jakarta.xml.soap-api</artifactId>
@@ -1361,17 +1364,11 @@
<version>3.2.0</version>
<configuration>
<excludeCoordinates>
- <!-- solr emitter -->
- <coordinate>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-http</artifactId>
- <version>11.0.26</version>
- </coordinate>
- <!-- [CVE-2025-1948], used in the Solr emitter. No apparent
upgrade available yet. -->
+ <!-- [CVE-2025-1948] check if still present in
${jetty.http2.version}; CVE-2026-2332 is fixed. -->
<coordinate>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-common</artifactId>
- <version>11.0.26</version>
+ <artifactId>jetty-http2-common</artifactId>
+ <version>${jetty.http2.version}</version>
</coordinate>
<!-- used only in tests and in tika-eval, and this problem
requires the use of the console.
https://github.com/h2database/h2database/issues/1294 -->
diff --git
a/tika-pipes/tika-httpclient-commons/src/main/java/org/apache/tika/client/HttpClientFactory.java
b/tika-pipes/tika-httpclient-commons/src/main/java/org/apache/tika/client/HttpClientFactory.java
index f7bc3304cc..51e58ca9b4 100644
---
a/tika-pipes/tika-httpclient-commons/src/main/java/org/apache/tika/client/HttpClientFactory.java
+++
b/tika-pipes/tika-httpclient-commons/src/main/java/org/apache/tika/client/HttpClientFactory.java
@@ -238,6 +238,10 @@ public class HttpClientFactory {
this.disableContentCompression = disableContentCompression;
}
+ public boolean isVerifySsl() {
+ return verifySsl;
+ }
+
public void setVerifySsl(boolean verifySsl) {
this.verifySsl = verifySsl;
}
diff --git a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
index 70bccf4e04..417d986f49 100644
--- a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
+++ b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/pom.xml
@@ -69,10 +69,25 @@
<artifactId>solr-solrj</artifactId>
<version>${solrj.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.solr</groupId>
+ <artifactId>solr-solrj-jetty</artifactId>
+ <version>${solrj.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.solr</groupId>
+ <artifactId>solr-solrj-zookeeper</artifactId>
+ <version>${solrj.version}</version>
+ </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitter.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitter.java
index 56b20d8200..c7a4477282 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitter.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitter.java
@@ -25,8 +25,7 @@ import java.util.concurrent.TimeUnit;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.Http2SolrClient;
-import org.apache.solr.client.solrj.impl.LBHttpSolrClient;
+import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.common.SolrInputDocument;
@@ -39,6 +38,7 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.emitter.AbstractEmitter;
import org.apache.tika.pipes.api.emitter.EmitData;
+import org.apache.tika.pipes.plugin.solr.SolrClientHelper;
import org.apache.tika.plugins.ExtensionConfig;
import org.apache.tika.utils.StringUtils;
@@ -105,29 +105,29 @@ public class SolrEmitter extends AbstractEmitter {
if (config.proxyPort() != null && config.proxyPort() > 0) {
httpClientFactory.setProxyPort(config.proxyPort());
}
+ httpClientFactory.setVerifySsl(config.verifySslOrDefault());
if (config.solrUrls() == null || config.solrUrls().isEmpty()) {
// Use ZooKeeper-based CloudSolrClient
- Http2SolrClient.Builder http2SolrClientBuilder = new
Http2SolrClient.Builder();
- if (!StringUtils.isBlank(httpClientFactory.getUserName())) {
-
http2SolrClientBuilder.withBasicAuthCredentials(httpClientFactory.getUserName(),
httpClientFactory.getPassword());
- }
- http2SolrClientBuilder
+ HttpJettySolrClient.Builder jettyClientBuilder = new
HttpJettySolrClient.Builder();
+ SolrClientHelper.applyClientSettings(jettyClientBuilder,
httpClientFactory);
+ jettyClientBuilder
.withRequestTimeout(httpClientFactory.getRequestTimeoutMillis(),
TimeUnit.MILLISECONDS)
-
.withConnectionTimeout(config.getConnectionTimeoutMillisOrDefault(),
TimeUnit.MILLISECONDS);
+
.withConnectionTimeout(config.getConnectionTimeoutMillisOrDefault(),
TimeUnit.MILLISECONDS)
+ .withIdleTimeout(config.getSocketTimeoutMillisOrDefault(),
TimeUnit.MILLISECONDS);
- Http2SolrClient http2SolrClient = http2SolrClientBuilder.build();
return new CloudSolrClient.Builder(config.solrZkHosts(),
Optional.ofNullable(config.solrZkChroot()))
- .withHttpClient(http2SolrClient)
+ .withHttpClientBuilder(jettyClientBuilder)
.build();
} else {
- // Use direct URL-based LBHttpSolrClient
- return new LBHttpSolrClient.Builder()
+ // Use direct URL-based LBJettySolrClient
+ HttpJettySolrClient.Builder jettyClientBuilder = new
HttpJettySolrClient.Builder();
+ SolrClientHelper.applyClientSettings(jettyClientBuilder,
httpClientFactory);
+ jettyClientBuilder
+
.withRequestTimeout(httpClientFactory.getRequestTimeoutMillis(),
TimeUnit.MILLISECONDS)
.withConnectionTimeout(config.getConnectionTimeoutMillisOrDefault(),
TimeUnit.MILLISECONDS)
-
.withSocketTimeout(config.getSocketTimeoutMillisOrDefault(),
TimeUnit.MILLISECONDS)
- .withHttpClient(httpClientFactory.build())
- .withBaseEndpoints(config.solrUrls().toArray(new
String[]{}))
- .build();
+ .withIdleTimeout(config.getSocketTimeoutMillisOrDefault(),
TimeUnit.MILLISECONDS);
+ return SolrClientHelper.buildLbClient(jettyClientBuilder,
config.solrUrls());
}
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
index 9ba5279b86..8fda64a7b5 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/emitter/solr/SolrEmitterConfig.java
@@ -41,7 +41,8 @@ public record SolrEmitterConfig(
String password,
String authScheme,
String proxyHost,
- Integer proxyPort
+ Integer proxyPort,
+ @JsonProperty(defaultValue = "true") Boolean verifySsl
) {
public enum AttachmentStrategy {
@@ -109,4 +110,9 @@ public record SolrEmitterConfig(
public String getEmbeddedFileFieldNameOrDefault() {
return embeddedFileFieldName != null ? embeddedFileFieldName :
"embedded";
}
+
+ /** SSL verification is on unless explicitly disabled (unset == secure). */
+ public boolean verifySslOrDefault() {
+ return verifySsl == null || verifySsl;
+ }
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIterator.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIterator.java
index 1cffca33f9..a701c93a7d 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIterator.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIterator.java
@@ -26,11 +26,10 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.Http2SolrClient;
-import org.apache.solr.client.solrj.impl.LBHttpSolrClient;
+import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
+import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.params.CursorMarkParams;
@@ -45,6 +44,7 @@ import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.api.emitter.EmitKey;
import org.apache.tika.pipes.api.fetcher.FetchKey;
import org.apache.tika.pipes.pipesiterator.PipesIteratorBase;
+import org.apache.tika.pipes.plugin.solr.SolrClientHelper;
import org.apache.tika.plugins.ExtensionConfig;
import org.apache.tika.utils.StringUtils;
@@ -113,6 +113,7 @@ public class SolrPipesIterator extends PipesIteratorBase {
if (config.getProxyPort() > 0) {
httpClientFactory.setProxyPort(config.getProxyPort());
}
+ httpClientFactory.setVerifySsl(config.isVerifySsl());
}
@Override
@@ -180,27 +181,24 @@ public class SolrPipesIterator extends PipesIteratorBase {
List<String> solrZkHosts = config.getSolrZkHosts() != null ?
config.getSolrZkHosts() : Collections.emptyList();
if (solrUrls.isEmpty()) {
- //TODO -- there's more that we need to pass through, including ssl
etc.
- Http2SolrClient.Builder http2SolrClientBuilder = new
Http2SolrClient.Builder();
- if (!StringUtils.isBlank(httpClientFactory.getUserName())) {
-
http2SolrClientBuilder.withBasicAuthCredentials(httpClientFactory.getUserName(),
httpClientFactory.getPassword());
- }
- http2SolrClientBuilder
+ HttpJettySolrClient.Builder jettyClientBuilder = new
HttpJettySolrClient.Builder();
+ SolrClientHelper.applyClientSettings(jettyClientBuilder,
httpClientFactory);
+ jettyClientBuilder
.withRequestTimeout(httpClientFactory.getRequestTimeoutMillis(),
TimeUnit.MILLISECONDS)
-
.withConnectionTimeout(config.getConnectionTimeoutMillis(),
TimeUnit.MILLISECONDS);
-
+
.withConnectionTimeout(config.getConnectionTimeoutMillis(),
TimeUnit.MILLISECONDS)
+ .withIdleTimeout(config.getSocketTimeoutMillis(),
TimeUnit.MILLISECONDS);
- Http2SolrClient http2SolrClient = http2SolrClientBuilder.build();
return new CloudSolrClient.Builder(solrZkHosts,
Optional.ofNullable(config.getSolrZkChroot()))
- .withHttpClient(http2SolrClient)
+ .withHttpClientBuilder(jettyClientBuilder)
.build();
-
}
- return new LBHttpSolrClient.Builder()
- .withConnectionTimeout(config.getConnectionTimeoutMillis())
- .withSocketTimeout(config.getSocketTimeoutMillis())
- .withHttpClient(httpClientFactory.build())
- .withBaseSolrUrls(solrUrls.toArray(new String[]{}))
- .build();
+ HttpJettySolrClient.Builder jettyClientBuilder = new
HttpJettySolrClient.Builder();
+ SolrClientHelper.applyClientSettings(jettyClientBuilder,
httpClientFactory);
+ jettyClientBuilder
+
.withRequestTimeout(httpClientFactory.getRequestTimeoutMillis(),
TimeUnit.MILLISECONDS)
+ .withConnectionTimeout(config.getConnectionTimeoutMillis(),
TimeUnit.MILLISECONDS)
+ .withIdleTimeout(config.getSocketTimeoutMillis(),
TimeUnit.MILLISECONDS);
+ return SolrClientHelper.buildLbClient(jettyClientBuilder, solrUrls);
}
+
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
index abf928d31c..0764bf21d8 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/iterator/solr/SolrPipesIteratorConfig.java
@@ -59,6 +59,7 @@ public class SolrPipesIteratorConfig extends
PipesIteratorConfig {
private String authScheme;
private String proxyHost;
private int proxyPort = 0;
+ private boolean verifySsl = true;
public String getSolrCollection() {
return solrCollection;
@@ -132,6 +133,10 @@ public class SolrPipesIteratorConfig extends
PipesIteratorConfig {
return proxyPort;
}
+ public boolean isVerifySsl() {
+ return verifySsl;
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof SolrPipesIteratorConfig that)) {
@@ -144,6 +149,7 @@ public class SolrPipesIteratorConfig extends
PipesIteratorConfig {
connectionTimeoutMillis == that.connectionTimeoutMillis &&
socketTimeoutMillis == that.socketTimeoutMillis &&
proxyPort == that.proxyPort &&
+ verifySsl == that.verifySsl &&
Objects.equals(solrCollection, that.solrCollection) &&
Objects.equals(solrUrls, that.solrUrls) &&
Objects.equals(solrZkHosts, that.solrZkHosts) &&
@@ -181,6 +187,7 @@ public class SolrPipesIteratorConfig extends
PipesIteratorConfig {
result = 31 * result + Objects.hashCode(authScheme);
result = 31 * result + Objects.hashCode(proxyHost);
result = 31 * result + proxyPort;
+ result = 31 * result + Boolean.hashCode(verifySsl);
return result;
}
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/plugin/solr/SolrClientHelper.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/plugin/solr/SolrClientHelper.java
new file mode 100644
index 0000000000..4a1872390b
--- /dev/null
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/main/java/org/apache/tika/pipes/plugin/solr/SolrClientHelper.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.tika.pipes.plugin.solr;
+
+import java.util.List;
+
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.LBSolrClient;
+import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
+import org.apache.solr.client.solrj.jetty.LBJettySolrClient;
+import org.apache.solr.client.solrj.jetty.SSLConfig;
+import org.apache.solr.common.util.IOUtils;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import org.apache.tika.client.HttpClientFactory;
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.utils.StringUtils;
+
+public final class SolrClientHelper {
+
+ // SolrJ signature is withProxyConfiguration(host, port, isSocks4,
isSecure). Tika only ever
+ // configured a plain HTTP proxy (HttpClientFactory.addProxy used new
HttpHost(host, port)).
+ private static final boolean PROXY_IS_SOCKS4 = false;
+ private static final boolean PROXY_IS_SECURE = false;
+
+ private SolrClientHelper() {}
+
+ /**
+ * Applies authentication, proxy and SSL settings from {@code factory} to a
+ * {@link HttpJettySolrClient.Builder}.
+ * <ul>
+ * <li>Auth: when a username is configured, only the basic scheme is
supported; any other
+ * scheme throws {@link TikaConfigException}. When no username is set,
auth is skipped
+ * regardless of the configured scheme (matching the original
Http2SolrClient code).</li>
+ * <li>Proxy: configured only when both a non-blank host and a positive
port are present.</li>
+ * <li>SSL: verified by default. When {@code factory.isVerifySsl()} is
false, all certificates
+ * are trusted and hostname verification is disabled (opt-in; preserves
the pre-Jetty-12
+ * HttpClientFactory behaviour for self-signed Solr endpoints).</li>
+ * </ul>
+ */
+ public static void applyClientSettings(HttpJettySolrClient.Builder builder,
+ HttpClientFactory factory) throws
TikaConfigException {
+ if (!StringUtils.isBlank(factory.getUserName())) {
+ if (!"basic".equalsIgnoreCase(factory.getAuthScheme())) {
+ throw new TikaConfigException(
+ "Only 'basic' auth scheme is supported by
HttpJettySolrClient; got: '"
+ + factory.getAuthScheme() + "'");
+ }
+ builder.withBasicAuthCredentials(factory.getUserName(),
factory.getPassword());
+ }
+ if (!StringUtils.isBlank(factory.getProxyHost()) &&
factory.getProxyPort() > 0) {
+ builder.withProxyConfiguration(factory.getProxyHost(),
factory.getProxyPort(),
+ PROXY_IS_SOCKS4, PROXY_IS_SECURE);
+ }
+ if (!factory.isVerifySsl()) {
+ builder.withSSLConfig(trustAllSslConfig());
+ }
+ }
+
+ /**
+ * An {@link SSLConfig} whose client context factory trusts all
certificates and skips hostname
+ * verification. Stock {@link SSLConfig} can only point at a
keystore/truststore, so we override
+ * {@link SSLConfig#createClientContextFactory()} to mirror the old
accept-all behaviour.
+ */
+ private static SSLConfig trustAllSslConfig() {
+ return new SSLConfig(true, false, null, null, null, null) {
+ @Override
+ public SslContextFactory.Client createClientContextFactory() {
+ SslContextFactory.Client clientFactory = new
SslContextFactory.Client();
+ clientFactory.setTrustAll(true);
+ clientFactory.setEndpointIdentificationAlgorithm(null);
+ return clientFactory;
+ }
+ };
+ }
+
+ /**
+ * Builds a direct-URL (load-balanced) Solr client from a configured
builder and the list of
+ * base Solr URLs.
+ * <p>
+ * The returned client closes the underlying {@link HttpJettySolrClient}
when it is closed.
+ * The stock {@link LBJettySolrClient} does not own its delegate client
(see
+ * {@link LBSolrClient#close()}), so without this the Jetty thread pool
and selectors created
+ * by the delegate would leak every time a caller closes the load-balanced
client.
+ */
+ public static SolrClient buildLbClient(HttpJettySolrClient.Builder
builder, List<String> solrUrls) {
+ return buildLbClient(builder.build(), solrUrls);
+ }
+
+ // package-private overload that takes an already-built (or mocked)
delegate, for testing
+ static SolrClient buildLbClient(HttpJettySolrClient httpClient,
List<String> solrUrls) {
+ LBSolrClient.Endpoint[] endpoints = solrUrls.stream()
+ .map(LBSolrClient.Endpoint::new)
+ .toArray(LBSolrClient.Endpoint[]::new);
+ return new ClosingLBJettySolrClient(new
LBJettySolrClient.Builder(httpClient, endpoints),
+ httpClient);
+ }
+
+ /**
+ * An {@link LBJettySolrClient} that also closes the delegate {@link
HttpJettySolrClient} it
+ * was built with, which the stock client does not do.
+ */
+ private static final class ClosingLBJettySolrClient extends
LBJettySolrClient {
+
+ private final HttpJettySolrClient httpClient;
+
+ private ClosingLBJettySolrClient(LBJettySolrClient.Builder builder,
+ HttpJettySolrClient httpClient) {
+ super(builder);
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public void close() {
+ try {
+ super.close();
+ } finally {
+ IOUtils.closeQuietly(httpClient);
+ }
+ }
+ }
+}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/test/java/org/apache/tika/pipes/plugin/solr/SolrClientHelperTest.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/test/java/org/apache/tika/pipes/plugin/solr/SolrClientHelperTest.java
new file mode 100644
index 0000000000..aecadb8acc
--- /dev/null
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-solr/src/test/java/org/apache/tika/pipes/plugin/solr/SolrClientHelperTest.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.tika.pipes.plugin.solr;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
+
+import org.apache.tika.client.HttpClientFactory;
+import org.apache.tika.exception.TikaConfigException;
+
+class SolrClientHelperTest {
+
+ private HttpJettySolrClient.Builder builder;
+ private HttpClientFactory factory;
+
+ @BeforeEach
+ void setUp() {
+ builder = mock(HttpJettySolrClient.Builder.class,
Answers.RETURNS_SELF);
+ factory = new HttpClientFactory();
+ }
+
+ // ── auth: basic credentials applied ─────────────────────────────────────
+
+ @Test
+ void basicAuthApplied_whenUsernameAndPasswordSet() throws
TikaConfigException {
+ factory.setUserName("alice");
+ factory.setPassword("secret");
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder).withBasicAuthCredentials("alice", "secret");
+ }
+
+ @Test
+ void basicAuthApplied_whenSchemeExplicitlyBasic() throws
TikaConfigException {
+ factory.setUserName("alice");
+ factory.setPassword("secret");
+ factory.setAuthScheme("basic");
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder).withBasicAuthCredentials("alice", "secret");
+ }
+
+ @Test
+ void basicAuthApplied_whenSchemeCaseInsensitive() throws
TikaConfigException {
+ factory.setUserName("alice");
+ factory.setPassword("secret");
+ factory.setAuthScheme("BASIC");
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder).withBasicAuthCredentials("alice", "secret");
+ }
+
+ // ── auth: non-basic scheme rejected when credentials present
─────────────
+
+ @Test
+ void nonBasicSchemeThrows_whenUsernameSet() {
+ factory.setUserName("alice");
+ factory.setPassword("secret");
+ factory.setAuthScheme("ntlm");
+
+ assertThrows(TikaConfigException.class,
+ () -> SolrClientHelper.applyClientSettings(builder, factory));
+ }
+
+ @Test
+ void nonBasicSchemeThrows_whenSchemeIsDigest() {
+ factory.setUserName("alice");
+ factory.setPassword("secret");
+ factory.setAuthScheme("digest");
+
+ assertThrows(TikaConfigException.class,
+ () -> SolrClientHelper.applyClientSettings(builder, factory));
+ }
+
+ // ── auth: no credentials when username is blank
──────────────────────────
+
+ @Test
+ void noAuthApplied_whenUsernameBlank() throws TikaConfigException {
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder, never()).withBasicAuthCredentials(any(), any());
+ }
+
+ @Test
+ void noExceptionAndNoAuth_whenNonBasicSchemeButNoUsername() {
+ // authScheme mismatch is only enforced when credentials are present;
+ // without a username there is nothing to authenticate, matching the
+ // behaviour of the original Http2SolrClient code on main.
+ factory.setAuthScheme("ntlm");
+
+ assertDoesNotThrow(() -> SolrClientHelper.applyClientSettings(builder,
factory));
+ }
+
+ // ── proxy: applied when host and positive port are set ──────────────────
+
+ @Test
+ void proxyApplied_whenHostAndPortSet() throws TikaConfigException {
+ factory.setProxyHost("proxy.example.com");
+ factory.setProxyPort(8080);
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder).withProxyConfiguration("proxy.example.com", 8080,
false, false);
+ }
+
+ @Test
+ void proxyNotApplied_whenHostBlank() throws TikaConfigException {
+ factory.setProxyPort(8080);
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder, never()).withProxyConfiguration(any(), anyInt(),
anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ void proxyNotApplied_whenPortZero() throws TikaConfigException {
+ factory.setProxyHost("proxy.example.com");
+ // proxyPort defaults to 0 — do not set it
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder, never()).withProxyConfiguration(any(), anyInt(),
anyBoolean(), anyBoolean());
+ }
+
+ @Test
+ void proxyNotApplied_whenPortNegative() throws TikaConfigException {
+ factory.setProxyHost("proxy.example.com");
+ factory.setProxyPort(-1);
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder, never()).withProxyConfiguration(any(), anyInt(),
anyBoolean(), anyBoolean());
+ }
+
+ // ── combined: auth + proxy together
──────────────────────────────────────
+
+ @Test
+ void authAndProxyBothApplied_whenAllConfigured() throws
TikaConfigException {
+ factory.setUserName("alice");
+ factory.setPassword("secret");
+ factory.setProxyHost("proxy.example.com");
+ factory.setProxyPort(3128);
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder).withBasicAuthCredentials("alice", "secret");
+ verify(builder).withProxyConfiguration("proxy.example.com", 3128,
false, false);
+ }
+
+ // ── ssl: trust-all is opt-in (off by default, on when verifySsl=false)
───
+
+ @Test
+ void sslConfigApplied_whenVerifySslFalse() throws TikaConfigException {
+ factory.setVerifySsl(false);
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder).withSSLConfig(any());
+ }
+
+ @Test
+ void noSslConfig_whenVerifySslTrue() throws TikaConfigException {
+ factory.setVerifySsl(true);
+
+ SolrClientHelper.applyClientSettings(builder, factory);
+
+ verify(builder, never()).withSSLConfig(any());
+ }
+
+ // ── direct-URL client owns and closes its delegate http client (leak
fix) ─
+
+ @Test
+ void lbClientClosesUnderlyingHttpClient() throws Exception {
+ HttpJettySolrClient httpClient = mock(HttpJettySolrClient.class);
+
+ SolrClient lbClient =
+ SolrClientHelper.buildLbClient(httpClient,
List.of("http://localhost:8983/solr"));
+ lbClient.close();
+
+ verify(httpClient).close();
+ }
+}
diff --git a/tika-server/tika-server-core/pom.xml
b/tika-server/tika-server-core/pom.xml
index 5bb242070a..d2b602b94e 100644
--- a/tika-server/tika-server-core/pom.xml
+++ b/tika-server/tika-server-core/pom.xml
@@ -95,7 +95,11 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
- <artifactId>http2-server</artifactId>
+ <artifactId>jetty-http2-server</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>