This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-jspf.git

commit 15cf09adeb048b773387ed0390b04d916d39d7d2
Author: Quan Tran <[email protected]>
AuthorDate: Thu Nov 5 02:51:42 2026 +0700

    [FIX] DNSServiceXBillImpl.getRecordsAsync: no unchecked exception
    
    dnsjava's LookupSession fails with ClassCastException on some malformed
    answers, as an UPDATE message holding a CNAME or DNAME without data.
    getRecordsAsync passed that failure on as is, where it reports the
    other failed lookups as a LookupFailedException (or an IOException):
    AsynchronousSPFExecutor read it as a lookup error, but any other caller
    of getRecordsAsync got that unchecked exception. So did a caller whose
    resolver throws when LookupSession sends it the first query, as one
    whose executor is shut down (RejectedExecutionException): the exception
    escaped getRecordsAsync itself, before it returned a stage.
    
    Fail the lookup with a LookupFailedException of the name and type looked
    up, a DNS error, so a temperror (RFC 7208 2.6.6), as getRecords reports
    it as a TimeoutException, and log it with the DNS type.
---
 .../jspf/executor/AsynchronousSPFExecutor.java     |  2 +-
 .../james/jspf/impl/DNSServiceXBillImpl.java       | 36 ++++++++++++++-
 .../apache/james/jspf/DNSServiceXBillImplTest.java | 51 ++++++++++++++++++++++
 3 files changed, 86 insertions(+), 3 deletions(-)

diff --git 
a/resolver/src/main/java/org/apache/james/jspf/executor/AsynchronousSPFExecutor.java
 
b/resolver/src/main/java/org/apache/james/jspf/executor/AsynchronousSPFExecutor.java
index ea6fe91..cef29e6 100644
--- 
a/resolver/src/main/java/org/apache/james/jspf/executor/AsynchronousSPFExecutor.java
+++ 
b/resolver/src/main/java/org/apache/james/jspf/executor/AsynchronousSPFExecutor.java
@@ -111,7 +111,7 @@ public class AsynchronousSPFExecutor implements SPFExecutor 
{
                     if (e instanceof TimeoutException) {
                         onResponse(session, result, cont, new 
DNSResponse((TimeoutException) e), checker);
                     } else if (e instanceof Exception) {
-                        // a lookup error, as dnsjava's on an answer it cannot 
handle: temperror (RFC 7208 4.4)
+                        // another lookup error, as an unchecked exception of 
a DNSService: temperror (RFC 7208 4.4)
                         LOGGER.warn("Unexpected error of the DNS lookup of 
{}", cont.getRequest().getHostname(), e);
                         onResponse(session, result, cont, new DNSResponse(new 
TimeoutException(e.toString())), checker);
                     } else {
diff --git 
a/resolver/src/main/java/org/apache/james/jspf/impl/DNSServiceXBillImpl.java 
b/resolver/src/main/java/org/apache/james/jspf/impl/DNSServiceXBillImpl.java
index d92bfe5..76d2c20 100644
--- a/resolver/src/main/java/org/apache/james/jspf/impl/DNSServiceXBillImpl.java
+++ b/resolver/src/main/java/org/apache/james/jspf/impl/DNSServiceXBillImpl.java
@@ -28,6 +28,7 @@ import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -55,6 +56,7 @@ import org.xbill.DNS.TXTRecord;
 import org.xbill.DNS.TextParseException;
 import org.xbill.DNS.Type;
 import org.xbill.DNS.lookup.LookupFailedException;
+import org.xbill.DNS.lookup.LookupResult;
 import org.xbill.DNS.lookup.LookupSession;
 import org.xbill.DNS.lookup.NoSuchRRSetException;
 
@@ -214,6 +216,32 @@ public class DNSServiceXBillImpl implements DNSService {
         }
     }
 
+    /**
+     * @return the failure of an asynchronous lookup, as LookupSession reports 
one: an unchecked exception dnsjava
+     *         throws on an answer it cannot handle becomes a 
LookupFailedException, as getRecords reports it as
+     *         a TimeoutException
+     */
+    private static CompletionException asLookupFailure(Throwable e, Name name, 
int type) {
+        Throwable cause = e instanceof CompletionException && e.getCause() != 
null ? e.getCause() : e;
+        if (cause instanceof RuntimeException && !(cause instanceof 
LookupFailedException)) {
+            LOGGER.warn("Unexpected error of the DNS lookup of {} (type {})", 
name, Type.string(type), cause);
+            return new CompletionException(new 
LookupFailedException(cause.toString(), name, type));
+        }
+        return e instanceof CompletionException ? (CompletionException) e : 
new CompletionException(e);
+    }
+
+    /**
+     * @return the lookup of the name and type: an unchecked exception thrown 
before the stage is returned, as
+     *         by a resolver whose executor is shut down 
(RejectedExecutionException), fails the stage instead
+     */
+    private static CompletionStage<LookupResult> startLookup(LookupSession 
session, Name name, int type) {
+        try {
+            return session.lookupAsync(name, type);
+        } catch (RuntimeException e) {
+            return CompletableFuture.failedFuture(e);
+        }
+    }
+
     /**
      * @return the records of the given type: an answer can hold others
      */
@@ -362,8 +390,12 @@ public class DNSServiceXBillImpl implements DNSService {
         final LookupSession lookupSession = 
LookupSession.defaultBuilder().resolver(new 
BoundedResolver(this.resolver)).build();
 
         try {
-            return 
lookupSession.lookupAsync(Name.fromString(request.getHostname(), Name.root), 
dnsJavaType)
-                .thenApply(result -> 
convertRecordsToList(ofType(result.getRecords().toArray(new Record[0]), 
dnsJavaType)));
+            Name name = Name.fromString(request.getHostname(), Name.root);
+            return startLookup(lookupSession, name, dnsJavaType)
+                .thenApply(result -> 
convertRecordsToList(ofType(result.getRecords().toArray(new Record[0]), 
dnsJavaType)))
+                .exceptionally(e -> {
+                    throw asLookupFailure(e, name, dnsJavaType);
+                });
         } catch (TextParseException e) {
             // no record, as getRecords answers
             LOGGER.debug("No {} Record found for host: {}", 
recordTypeDescription, request.getHostname());
diff --git 
a/resolver/src/test/java/org/apache/james/jspf/DNSServiceXBillImplTest.java 
b/resolver/src/test/java/org/apache/james/jspf/DNSServiceXBillImplTest.java
index ff02b21..0bf0980 100644
--- a/resolver/src/test/java/org/apache/james/jspf/DNSServiceXBillImplTest.java
+++ b/resolver/src/test/java/org/apache/james/jspf/DNSServiceXBillImplTest.java
@@ -39,16 +39,21 @@ import org.xbill.DNS.Name;
 import org.xbill.DNS.Record;
 import org.xbill.DNS.SPFRecord;
 import org.xbill.DNS.Section;
+import org.xbill.DNS.SimpleResolver;
 import org.xbill.DNS.TXTRecord;
 import org.xbill.DNS.Type;
 import org.xbill.DNS.lookup.LookupFailedException;
 import org.xbill.DNS.lookup.LookupSession;
 
+import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.Base64;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
 
 import static org.junit.Assert.assertEquals;
@@ -123,6 +128,52 @@ public class DNSServiceXBillImplTest {
         }
     }
 
+    @Test(timeout = 10000)
+    public void 
getRecordsAsyncShouldReportADnsErrorForAnAnswerDnsjavaCannotProcess() throws 
Exception {
+        // opcode UPDATE, and the answer "example.com CNAME" of RDLENGTH 0: 
dnsjava's LookupSession fails with
+        // ClassCastException
+        byte[] message = 
Base64.getDecoder().decode("AAErAAABAAEAAAAAB2V4YW1wbGUDY29tAAAQAAHADAAFAAAAAA5RAABj");
+        DNSServiceXBillImpl dns = new DNSServiceXBillImpl(new 
AnsweringResolver(message));
+
+        CompletableFuture<List<String>> records = dns.getRecordsAsync(new 
DNSRequest("example.com", DNSRequest.TXT))
+            .toCompletableFuture();
+
+        try {
+            records.get(5, TimeUnit.SECONDS);
+            fail("the lookup should fail");
+        } catch (ExecutionException e) {
+            // a failed lookup of that name and type, not one of no data: a 
DNS error (RFC 7208 4.4)
+            assertEquals(e.getCause().toString(), LookupFailedException.class, 
e.getCause().getClass());
+            assertEquals(Name.fromString("example.com."), 
((LookupFailedException) e.getCause()).getName());
+            assertEquals(Type.TXT, ((LookupFailedException) 
e.getCause()).getType());
+        }
+    }
+
+    @Test(timeout = 10000)
+    public void getRecordsAsyncShouldReportADnsErrorWhenTheResolverThrows() 
throws Exception {
+        // a resolver whose executor is shut down: it throws before it returns 
the stage of the query
+        SimpleResolver resolver = new 
SimpleResolver(InetAddress.getLoopbackAddress()) {
+            @Override
+            public CompletionStage<Message> sendAsync(Message query, Executor 
executor) {
+                throw new RejectedExecutionException("executor shut down");
+            }
+        };
+        DNSServiceXBillImpl dns = new DNSServiceXBillImpl(resolver);
+
+        CompletableFuture<List<String>> records = dns.getRecordsAsync(new 
DNSRequest("example.com", DNSRequest.TXT))
+            .toCompletableFuture();
+
+        try {
+            records.get(5, TimeUnit.SECONDS);
+            fail("the lookup should fail");
+        } catch (ExecutionException e) {
+            // a failed lookup of that name and type, as for an exception of 
the resolver once the lookup started
+            assertEquals(e.getCause().toString(), LookupFailedException.class, 
e.getCause().getClass());
+            assertEquals(Name.fromString("example.com."), 
((LookupFailedException) e.getCause()).getName());
+            assertEquals(Type.TXT, ((LookupFailedException) 
e.getCause()).getType());
+        }
+    }
+
     @Test(timeout = 10000)
     public void 
getRecordsAsyncShouldEndALookupWhoseAnswerHoldsACnameOfAnotherName() throws 
Exception {
         AnsweringResolver resolver = new 
AnsweringResolver(Base64.getDecoder().decode(CNAME_OF_THE_ROOT));


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to