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]
