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>


Reply via email to