From 7d3fb6c4b95124b59a3a1ddcdfb0a0a3986d50ed Mon Sep 17 00:00:00 2001 From: Arne Date: Sat, 9 Nov 2024 20:34:43 +0100 Subject: [PATCH] Integrate MiniDNS and change static DNS server --- build.gradle | 5 +- .../de/monocles/chat/test/ScreenshotTest.java | 169 --- ....android-boot-classpath-conventions.gradle | 6 + .../org.minidns.android-conventions.gradle | 10 + ...org.minidns.application-conventions.gradle | 12 + .../org.minidns.common-conventions.gradle | 36 + .../org.minidns.java-conventions.gradle | 302 ++++ .../org.minidns.javadoc-conventions.gradle | 24 + .../java/org/minidns/AbstractDnsClient.java | 477 ++++++ src/main/java/org/minidns/DnsCache.java | 50 + src/main/java/org/minidns/DnsClient.java | 492 +++++++ .../org/minidns/MiniDnsConfiguration.java | 19 + .../java/org/minidns/MiniDnsException.java | 120 ++ src/main/java/org/minidns/MiniDnsFuture.java | 282 ++++ .../org/minidns/MiniDnsInitialization.java | 48 + src/main/java/org/minidns/RrSet.java | 99 ++ .../org/minidns/cache/ExtendedLruCache.java | 129 ++ .../java/org/minidns/cache/FullLruCache.java | 39 + src/main/java/org/minidns/cache/LruCache.java | 169 +++ .../minidns/cache/MiniDnsCacheFactory.java | 19 + .../org/minidns/constants/DnsRootServer.java | 107 ++ .../minidns/constants/DnssecConstants.java | 99 ++ .../dane/DaneCertificateException.java | 65 + .../java/org/minidns/dane/DaneVerifier.java | 272 ++++ .../minidns/dane/ExpectingTrustManager.java | 63 + .../minidns/dane/X509TrustManagerUtil.java | 44 + .../dane/java7/DaneExtendedTrustManager.java | 172 +++ .../java/org/minidns/dnslabel/ALabel.java | 25 + .../java/org/minidns/dnslabel/DnsLabel.java | 280 ++++ .../java/org/minidns/dnslabel/FakeALabel.java | 19 + .../java/org/minidns/dnslabel/LdhLabel.java | 81 ++ .../LeadingOrTrailingHyphenLabel.java | 37 + .../org/minidns/dnslabel/NonLdhLabel.java | 35 + .../minidns/dnslabel/NonReservedLdhLabel.java | 34 + .../minidns/dnslabel/OtherNonLdhLabel.java | 23 + .../minidns/dnslabel/ReservedLdhLabel.java | 36 + .../org/minidns/dnslabel/UnderscoreLabel.java | 26 + .../java/org/minidns/dnslabel/XnLabel.java | 50 + .../org/minidns/dnsmessage/DnsMessage.java | 1281 +++++++++++++++++ .../java/org/minidns/dnsmessage/Question.java | 180 +++ .../java/org/minidns/dnsname/DnsName.java | 646 +++++++++ .../dnsname/InvalidDnsNameException.java | 66 + .../dnsqueryresult/CachedDnsQueryResult.java | 28 + .../DirectCachedDnsQueryResult.java | 21 + .../dnsqueryresult/DnsQueryResult.java | 52 + .../StandardDnsQueryResult.java | 29 + .../SynthesizedCachedDnsQueryResult.java | 21 + .../org/minidns/dnssec/DigestCalculator.java | 15 + .../java/org/minidns/dnssec/DnssecClient.java | 573 ++++++++ .../org/minidns/dnssec/DnssecQueryResult.java | 53 + .../DnssecResultNotAuthenticException.java | 48 + .../dnssec/DnssecUnverifiedReason.java | 175 +++ .../DnssecValidationFailedException.java | 153 ++ ...nssecValidatorInitializationException.java | 19 + .../org/minidns/dnssec/SignatureVerifier.java | 18 + .../java/org/minidns/dnssec/Verifier.java | 219 +++ .../dnssec/algorithms/AlgorithmMap.java | 122 ++ .../algorithms/DsaSignatureVerifier.java | 132 ++ .../algorithms/EcdsaSignatureVerifier.java | 133 ++ .../algorithms/EcgostSignatureVerifier.java | 86 ++ .../algorithms/JavaSecDigestCalculator.java | 29 + .../algorithms/JavaSecSignatureVerifier.java | 62 + .../algorithms/RsaSignatureVerifier.java | 67 + .../AbstractDnsServerLookupMechanism.java | 60 + .../dnsserverlookup/AndroidUsingExec.java | 115 ++ .../AndroidUsingReflection.java | 98 ++ .../DnsServerLookupMechanism.java | 34 + .../UnixUsingEtcResolvConf.java | 110 ++ .../android21/AndroidUsingLinkProperties.java | 135 ++ src/main/java/org/minidns/edns/Edns.java | 235 +++ .../java/org/minidns/edns/EdnsOption.java | 83 ++ src/main/java/org/minidns/edns/Nsid.java | 47 + .../org/minidns/edns/UnknownEdnsOption.java | 37 + .../org/minidns/hla/DnssecResolverApi.java | 124 ++ .../hla/ResolutionUnsuccessfulException.java | 32 + .../java/org/minidns/hla/ResolverApi.java | 222 +++ .../java/org/minidns/hla/ResolverResult.java | 178 +++ .../org/minidns/hla/SrvResolverResult.java | 204 +++ .../java/org/minidns/hla/srv/SrvProto.java | 29 + .../java/org/minidns/hla/srv/SrvService.java | 44 + .../org/minidns/hla/srv/SrvServiceProto.java | 27 + .../java/org/minidns/hla/srv/SrvType.java | 28 + .../idna/DefaultIdnaTransformator.java | 36 + .../org/minidns/idna/IdnaTransformator.java | 19 + .../java/org/minidns/idna/MiniDnsIdna.java | 31 + .../minidns/integrationtest/AsyncApiTest.java | 56 + .../org/minidns/integrationtest/CoreTest.java | 58 + .../org/minidns/integrationtest/DaneTest.java | 45 + .../minidns/integrationtest/DnssecTest.java | 65 + .../org/minidns/integrationtest/HlaTest.java | 57 + .../integrationtest/IntegrationTest.java | 22 + .../IntegrationTestHelper.java | 133 ++ .../integrationtest/IntegrationTestTools.java | 53 + .../integrationtest/IterativeDnssecTest.java | 55 + .../org/minidns/integrationtest/NsidTest.java | 52 + .../iterative/IterativeClientException.java | 93 ++ .../minidns/iterative/IterativeDnsClient.java | 502 +++++++ .../minidns/iterative/ReliableDnsClient.java | 190 +++ .../minidns/iterative/ResolutionState.java | 53 + src/main/java/org/minidns/jul/MiniDnsJul.java | 121 ++ .../org/minidns/minidnsrepl/DnssecStats.java | 65 + .../org/minidns/minidnsrepl/MiniDnsRepl.java | 86 ++ .../org/minidns/minidnsrepl/MiniDnsStats.java | 105 ++ src/main/java/org/minidns/record/A.java | 68 + src/main/java/org/minidns/record/AAAA.java | 67 + src/main/java/org/minidns/record/CNAME.java | 42 + src/main/java/org/minidns/record/DLV.java | 43 + src/main/java/org/minidns/record/DNAME.java | 44 + src/main/java/org/minidns/record/DNSKEY.java | 194 +++ src/main/java/org/minidns/record/DS.java | 48 + src/main/java/org/minidns/record/Data.java | 107 ++ .../minidns/record/DelegatingDnssecRR.java | 157 ++ .../org/minidns/record/InternetAddressRR.java | 78 + src/main/java/org/minidns/record/MX.java | 76 + src/main/java/org/minidns/record/NS.java | 38 + src/main/java/org/minidns/record/NSEC.java | 161 +++ src/main/java/org/minidns/record/NSEC3.java | 212 +++ .../java/org/minidns/record/NSEC3PARAM.java | 102 ++ .../java/org/minidns/record/OPENPGPKEY.java | 60 + src/main/java/org/minidns/record/OPT.java | 72 + src/main/java/org/minidns/record/PTR.java | 42 + src/main/java/org/minidns/record/RRSIG.java | 199 +++ .../java/org/minidns/record/RRWithTarget.java | 50 + src/main/java/org/minidns/record/Record.java | 628 ++++++++ src/main/java/org/minidns/record/SOA.java | 116 ++ src/main/java/org/minidns/record/SRV.java | 100 ++ src/main/java/org/minidns/record/TLSA.java | 203 +++ src/main/java/org/minidns/record/TXT.java | 107 ++ src/main/java/org/minidns/record/UNKNOWN.java | 45 + .../minidns/source/AbstractDnsDataSource.java | 113 ++ .../org/minidns/source/DnsDataSource.java | 47 + .../org/minidns/source/NetworkDataSource.java | 158 ++ .../NetworkDataSourceWithAccounting.java | 160 ++ .../minidns/source/async/AsyncDnsRequest.java | 547 +++++++ .../source/async/AsyncNetworkDataSource.java | 311 ++++ .../source/async/ChannelSelectedHandler.java | 44 + src/main/java/org/minidns/util/Base32.java | 41 + src/main/java/org/minidns/util/Base64.java | 38 + .../org/minidns/util/CallbackRecipient.java | 25 + .../org/minidns/util/CollectionsUtil.java | 28 + .../org/minidns/util/ExceptionCallback.java | 17 + src/main/java/org/minidns/util/Hex.java | 22 + .../org/minidns/util/InetAddressUtil.java | 116 ++ .../org/minidns/util/MultipleIoException.java | 69 + src/main/java/org/minidns/util/NameUtil.java | 38 + .../org/minidns/util/PlatformDetection.java | 28 + .../org/minidns/util/SafeCharSequence.java | 36 + src/main/java/org/minidns/util/SrvUtil.java | 114 ++ .../org/minidns/util/SuccessCallback.java | 17 + .../.keep-minidns-dnssec-main-resources | 0 src/main/resources/de.measite.minidns/.keep | 0 151 files changed, 16799 insertions(+), 171 deletions(-) delete mode 100644 src/androidTest/java/de/monocles/chat/test/ScreenshotTest.java create mode 100644 src/main/groovy/org.minidns.android-boot-classpath-conventions.gradle create mode 100644 src/main/groovy/org.minidns.android-conventions.gradle create mode 100644 src/main/groovy/org.minidns.application-conventions.gradle create mode 100644 src/main/groovy/org.minidns.common-conventions.gradle create mode 100644 src/main/groovy/org.minidns.java-conventions.gradle create mode 100644 src/main/groovy/org.minidns.javadoc-conventions.gradle create mode 100644 src/main/java/org/minidns/AbstractDnsClient.java create mode 100644 src/main/java/org/minidns/DnsCache.java create mode 100644 src/main/java/org/minidns/DnsClient.java create mode 100644 src/main/java/org/minidns/MiniDnsConfiguration.java create mode 100644 src/main/java/org/minidns/MiniDnsException.java create mode 100644 src/main/java/org/minidns/MiniDnsFuture.java create mode 100644 src/main/java/org/minidns/MiniDnsInitialization.java create mode 100644 src/main/java/org/minidns/RrSet.java create mode 100644 src/main/java/org/minidns/cache/ExtendedLruCache.java create mode 100644 src/main/java/org/minidns/cache/FullLruCache.java create mode 100644 src/main/java/org/minidns/cache/LruCache.java create mode 100644 src/main/java/org/minidns/cache/MiniDnsCacheFactory.java create mode 100644 src/main/java/org/minidns/constants/DnsRootServer.java create mode 100644 src/main/java/org/minidns/constants/DnssecConstants.java create mode 100644 src/main/java/org/minidns/dane/DaneCertificateException.java create mode 100644 src/main/java/org/minidns/dane/DaneVerifier.java create mode 100644 src/main/java/org/minidns/dane/ExpectingTrustManager.java create mode 100644 src/main/java/org/minidns/dane/X509TrustManagerUtil.java create mode 100644 src/main/java/org/minidns/dane/java7/DaneExtendedTrustManager.java create mode 100644 src/main/java/org/minidns/dnslabel/ALabel.java create mode 100644 src/main/java/org/minidns/dnslabel/DnsLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/FakeALabel.java create mode 100644 src/main/java/org/minidns/dnslabel/LdhLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/NonLdhLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/UnderscoreLabel.java create mode 100644 src/main/java/org/minidns/dnslabel/XnLabel.java create mode 100644 src/main/java/org/minidns/dnsmessage/DnsMessage.java create mode 100644 src/main/java/org/minidns/dnsmessage/Question.java create mode 100644 src/main/java/org/minidns/dnsname/DnsName.java create mode 100644 src/main/java/org/minidns/dnsname/InvalidDnsNameException.java create mode 100644 src/main/java/org/minidns/dnsqueryresult/CachedDnsQueryResult.java create mode 100644 src/main/java/org/minidns/dnsqueryresult/DirectCachedDnsQueryResult.java create mode 100644 src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java create mode 100644 src/main/java/org/minidns/dnsqueryresult/StandardDnsQueryResult.java create mode 100644 src/main/java/org/minidns/dnsqueryresult/SynthesizedCachedDnsQueryResult.java create mode 100644 src/main/java/org/minidns/dnssec/DigestCalculator.java create mode 100644 src/main/java/org/minidns/dnssec/DnssecClient.java create mode 100644 src/main/java/org/minidns/dnssec/DnssecQueryResult.java create mode 100644 src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java create mode 100644 src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java create mode 100644 src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java create mode 100644 src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java create mode 100644 src/main/java/org/minidns/dnssec/SignatureVerifier.java create mode 100644 src/main/java/org/minidns/dnssec/Verifier.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java create mode 100644 src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java create mode 100644 src/main/java/org/minidns/dnsserverlookup/AbstractDnsServerLookupMechanism.java create mode 100644 src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java create mode 100644 src/main/java/org/minidns/dnsserverlookup/AndroidUsingReflection.java create mode 100644 src/main/java/org/minidns/dnsserverlookup/DnsServerLookupMechanism.java create mode 100644 src/main/java/org/minidns/dnsserverlookup/UnixUsingEtcResolvConf.java create mode 100644 src/main/java/org/minidns/dnsserverlookup/android21/AndroidUsingLinkProperties.java create mode 100644 src/main/java/org/minidns/edns/Edns.java create mode 100644 src/main/java/org/minidns/edns/EdnsOption.java create mode 100644 src/main/java/org/minidns/edns/Nsid.java create mode 100644 src/main/java/org/minidns/edns/UnknownEdnsOption.java create mode 100644 src/main/java/org/minidns/hla/DnssecResolverApi.java create mode 100644 src/main/java/org/minidns/hla/ResolutionUnsuccessfulException.java create mode 100644 src/main/java/org/minidns/hla/ResolverApi.java create mode 100644 src/main/java/org/minidns/hla/ResolverResult.java create mode 100644 src/main/java/org/minidns/hla/SrvResolverResult.java create mode 100644 src/main/java/org/minidns/hla/srv/SrvProto.java create mode 100644 src/main/java/org/minidns/hla/srv/SrvService.java create mode 100644 src/main/java/org/minidns/hla/srv/SrvServiceProto.java create mode 100644 src/main/java/org/minidns/hla/srv/SrvType.java create mode 100644 src/main/java/org/minidns/idna/DefaultIdnaTransformator.java create mode 100644 src/main/java/org/minidns/idna/IdnaTransformator.java create mode 100644 src/main/java/org/minidns/idna/MiniDnsIdna.java create mode 100644 src/main/java/org/minidns/integrationtest/AsyncApiTest.java create mode 100644 src/main/java/org/minidns/integrationtest/CoreTest.java create mode 100644 src/main/java/org/minidns/integrationtest/DaneTest.java create mode 100644 src/main/java/org/minidns/integrationtest/DnssecTest.java create mode 100644 src/main/java/org/minidns/integrationtest/HlaTest.java create mode 100644 src/main/java/org/minidns/integrationtest/IntegrationTest.java create mode 100644 src/main/java/org/minidns/integrationtest/IntegrationTestHelper.java create mode 100644 src/main/java/org/minidns/integrationtest/IntegrationTestTools.java create mode 100644 src/main/java/org/minidns/integrationtest/IterativeDnssecTest.java create mode 100644 src/main/java/org/minidns/integrationtest/NsidTest.java create mode 100644 src/main/java/org/minidns/iterative/IterativeClientException.java create mode 100644 src/main/java/org/minidns/iterative/IterativeDnsClient.java create mode 100644 src/main/java/org/minidns/iterative/ReliableDnsClient.java create mode 100644 src/main/java/org/minidns/iterative/ResolutionState.java create mode 100644 src/main/java/org/minidns/jul/MiniDnsJul.java create mode 100644 src/main/java/org/minidns/minidnsrepl/DnssecStats.java create mode 100644 src/main/java/org/minidns/minidnsrepl/MiniDnsRepl.java create mode 100644 src/main/java/org/minidns/minidnsrepl/MiniDnsStats.java create mode 100644 src/main/java/org/minidns/record/A.java create mode 100644 src/main/java/org/minidns/record/AAAA.java create mode 100644 src/main/java/org/minidns/record/CNAME.java create mode 100644 src/main/java/org/minidns/record/DLV.java create mode 100644 src/main/java/org/minidns/record/DNAME.java create mode 100644 src/main/java/org/minidns/record/DNSKEY.java create mode 100644 src/main/java/org/minidns/record/DS.java create mode 100644 src/main/java/org/minidns/record/Data.java create mode 100644 src/main/java/org/minidns/record/DelegatingDnssecRR.java create mode 100644 src/main/java/org/minidns/record/InternetAddressRR.java create mode 100644 src/main/java/org/minidns/record/MX.java create mode 100644 src/main/java/org/minidns/record/NS.java create mode 100644 src/main/java/org/minidns/record/NSEC.java create mode 100644 src/main/java/org/minidns/record/NSEC3.java create mode 100644 src/main/java/org/minidns/record/NSEC3PARAM.java create mode 100644 src/main/java/org/minidns/record/OPENPGPKEY.java create mode 100644 src/main/java/org/minidns/record/OPT.java create mode 100644 src/main/java/org/minidns/record/PTR.java create mode 100644 src/main/java/org/minidns/record/RRSIG.java create mode 100644 src/main/java/org/minidns/record/RRWithTarget.java create mode 100644 src/main/java/org/minidns/record/Record.java create mode 100644 src/main/java/org/minidns/record/SOA.java create mode 100644 src/main/java/org/minidns/record/SRV.java create mode 100644 src/main/java/org/minidns/record/TLSA.java create mode 100644 src/main/java/org/minidns/record/TXT.java create mode 100644 src/main/java/org/minidns/record/UNKNOWN.java create mode 100644 src/main/java/org/minidns/source/AbstractDnsDataSource.java create mode 100644 src/main/java/org/minidns/source/DnsDataSource.java create mode 100644 src/main/java/org/minidns/source/NetworkDataSource.java create mode 100644 src/main/java/org/minidns/source/NetworkDataSourceWithAccounting.java create mode 100644 src/main/java/org/minidns/source/async/AsyncDnsRequest.java create mode 100644 src/main/java/org/minidns/source/async/AsyncNetworkDataSource.java create mode 100644 src/main/java/org/minidns/source/async/ChannelSelectedHandler.java create mode 100644 src/main/java/org/minidns/util/Base32.java create mode 100644 src/main/java/org/minidns/util/Base64.java create mode 100644 src/main/java/org/minidns/util/CallbackRecipient.java create mode 100644 src/main/java/org/minidns/util/CollectionsUtil.java create mode 100644 src/main/java/org/minidns/util/ExceptionCallback.java create mode 100644 src/main/java/org/minidns/util/Hex.java create mode 100644 src/main/java/org/minidns/util/InetAddressUtil.java create mode 100644 src/main/java/org/minidns/util/MultipleIoException.java create mode 100644 src/main/java/org/minidns/util/NameUtil.java create mode 100644 src/main/java/org/minidns/util/PlatformDetection.java create mode 100644 src/main/java/org/minidns/util/SafeCharSequence.java create mode 100644 src/main/java/org/minidns/util/SrvUtil.java create mode 100644 src/main/java/org/minidns/util/SuccessCallback.java create mode 100644 src/main/resources/.keep-minidns-dnssec-main-resources create mode 100644 src/main/resources/de.measite.minidns/.keep diff --git a/build.gradle b/build.gradle index aa18ff070..60fa7c7fc 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,8 @@ configurations { } dependencies { + implementation 'org.junit.jupiter:junit-jupiter:5.8.1' + implementation 'junit:junit:4.13.2' androidTestImplementation 'tools.fastlane:screengrab:2.1.1' androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.6.2' @@ -82,7 +84,6 @@ dependencies { implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1' implementation 'com.google.zxing:core:3.5.3' - implementation 'org.minidns:minidns-hla:1.1.1' implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation "com.wefika:flowlayout:0.4.1" @@ -307,7 +308,7 @@ android { } packagingOptions { resources { - excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'] + excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF', 'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md'] } } lint { diff --git a/src/androidTest/java/de/monocles/chat/test/ScreenshotTest.java b/src/androidTest/java/de/monocles/chat/test/ScreenshotTest.java deleted file mode 100644 index a35c96379..000000000 --- a/src/androidTest/java/de/monocles/chat/test/ScreenshotTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package de.monocles.chat.test; - -import java.util.concurrent.TimeoutException; -import java.lang.Thread; -import java.util.Arrays; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; - -import androidx.test.InstrumentationRegistry; -import androidx.test.core.app.ActivityScenario; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.rules.ActivityScenarioRule; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.ServiceTestRule; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withId; - -import tools.fastlane.screengrab.Screengrab; -import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar; -import tools.fastlane.screengrab.locale.LocaleTestRule; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; -import eu.siacs.conversations.entities.TransferablePlaceholder; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.test.R; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.StartConversationActivity; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -@RunWith(AndroidJUnit4.class) -public class ScreenshotTest { - - static String pkg = InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); - static XmppConnectionService xmppConnectionService; - static Account account; - - @ClassRule - public static final LocaleTestRule localeTestRule = new LocaleTestRule(); - - @ClassRule - public static final ServiceTestRule xmppServiceRule = new ServiceTestRule(); - - @BeforeClass - public static void setup() throws TimeoutException { - CleanStatusBar.enableWithDefaults(); - - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), XmppConnectionService.class); - intent.setAction("ui"); - xmppConnectionService = ((XmppConnectionBinder) xmppServiceRule.bindService(intent)).getService(); - account = xmppConnectionService.findAccountByJid(Jid.of("carrot@chaosah.hereva")); - if (account == null) { - account = new Account( - Jid.of("carrot@chaosah.hereva"), - "orangeandfurry" - ); - xmppConnectionService.createAccount(account); - } - - Uri avatarUri = Uri.parse("android.resource://" + pkg + "/" + String.valueOf(R.drawable.carrot)); - final Avatar avatar = xmppConnectionService.getFileBackend().getPepAvatar(avatarUri, 192, Bitmap.CompressFormat.WEBP); - xmppConnectionService.getFileBackend().save(avatar); - account.setAvatar(avatar.getFilename()); - - Contact cheogram = account.getRoster().getContact(Jid.of("cheogram.com")); - cheogram.setOption(Contact.Options.IN_ROSTER); - cheogram.setPhotoUri("android.resource://" + pkg + "/" + String.valueOf(R.drawable.cheogram)); - Presence cheogramPresence = Presence.parse(null, null, ""); - IqPacket discoPacket = new IqPacket(IqPacket.TYPE.RESULT); - Element query = discoPacket.addChild("query", "http://jabber.org/protocol/disco#info"); - Element identity = query.addChild("identity"); - identity.setAttribute("category", "gateway"); - identity.setAttribute("type", "pstn"); - cheogramPresence.setServiceDiscoveryResult(new ServiceDiscoveryResult(discoPacket)); - cheogram.updatePresence("gw", cheogramPresence); - } - - @AfterClass - public static void teardown() { - CleanStatusBar.disable(); - } - - @Test - public void testConversation() throws FileBackend.FileCopyException, InterruptedException { - Conversation conversation = xmppConnectionService.findOrCreateConversation(account, Jid.of("+15550737737@cheogram.com"), false, false); - conversation.getContact().setOption(Contact.Options.IN_ROSTER); - conversation.getContact().setSystemName("Pepper"); - conversation.getContact().setPhotoUri("android.resource://" + pkg + "/" + String.valueOf(R.drawable.pepper)); - - Message voicemail = new Message(conversation, "", 0, Message.STATUS_RECEIVED); - voicemail.setOob("https://example.com/thing.mp3"); - voicemail.setFileParams(new Message.FileParams("https://example.com/thing.mp3|5000|0|0|10000")); - voicemail.setType(Message.TYPE_FILE); - voicemail.setSubject("Voicemail Recording"); - - Message transcript = new Message(conversation, "Where are you?", 0, Message.STATUS_RECEIVED); - transcript.setSubject("Voicemail Transcription"); - - Message picture = new Message(conversation, "", 0, Message.STATUS_SEND_RECEIVED); - picture.setOob("https://example.com/thing.webp"); - picture.setType(Message.TYPE_FILE); - xmppConnectionService.getFileBackend().copyFileToPrivateStorage( - picture, - Uri.parse("android.resource://" + pkg + "/" + String.valueOf(R.drawable.komona)), - "image/webp" - ); - xmppConnectionService.getFileBackend().updateFileParams(picture); - - conversation.addAll(0, Arrays.asList( - voicemail, - transcript, - new Message(conversation, "Meow", 0, Message.STATUS_SEND_RECEIVED), - picture, - new Message(conversation, "👍", 0, Message.STATUS_RECEIVED) - )); - - ActivityScenario scenario = ActivityScenario.launch(ConversationsActivity.class); - scenario.onActivity((Activity activity) -> { - ((ConversationsActivity) activity).switchToConversation(conversation); - }); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - Thread.sleep(100); // ImageView not paited yet after waitForIdleSync - Screengrab.screenshot("conversation"); - } - - @Test - public void testStartConversation() throws InterruptedException { - ActivityScenario scenario = ActivityScenario.launch(StartConversationActivity.class); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - Thread.sleep(100); // ImageView not paited yet after waitForIdleSync - Screengrab.screenshot("startConversation"); - } - - @Test - public void testAddContact() throws InterruptedException { - ActivityScenario scenario = ActivityScenario.launch(StartConversationActivity.class); - onView(withId(eu.siacs.conversations.R.id.speed_dial)).perform(click()); - Screengrab.screenshot("startConversationOptions"); - - // Not actually online, so can't screenshot the gateway selector yet - /*onView(withId(eu.siacs.conversations.R.id.create_contact)).perform(click()); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - Thread.sleep(10000); // ImageView not paited yet after waitForIdleSync - Screengrab.screenshot("addContact");*/ - } -} diff --git a/src/main/groovy/org.minidns.android-boot-classpath-conventions.gradle b/src/main/groovy/org.minidns.android-boot-classpath-conventions.gradle new file mode 100644 index 000000000..c14cb3199 --- /dev/null +++ b/src/main/groovy/org.minidns.android-boot-classpath-conventions.gradle @@ -0,0 +1,6 @@ +compileJava { + options.bootstrapClasspath = files(androidBootClasspath) +} +javadoc { + classpath += files(androidBootClasspath) +} diff --git a/src/main/groovy/org.minidns.android-conventions.gradle b/src/main/groovy/org.minidns.android-conventions.gradle new file mode 100644 index 000000000..019c943c8 --- /dev/null +++ b/src/main/groovy/org.minidns.android-conventions.gradle @@ -0,0 +1,10 @@ +plugins { + id 'ru.vyarus.animalsniffer' + id 'org.minidns.common-conventions' +} +dependencies { + signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:4.4.2_r4@signature" +} +animalsniffer { + sourceSets = [sourceSets.main] +} diff --git a/src/main/groovy/org.minidns.application-conventions.gradle b/src/main/groovy/org.minidns.application-conventions.gradle new file mode 100644 index 000000000..fa4c7011e --- /dev/null +++ b/src/main/groovy/org.minidns.application-conventions.gradle @@ -0,0 +1,12 @@ +plugins { + id 'application' +} + +application { + applicationDefaultJvmArgs = ["-enableassertions"] +} + +run { + // Pass all system properties down to the "application" run + systemProperties System.getProperties() +} diff --git a/src/main/groovy/org.minidns.common-conventions.gradle b/src/main/groovy/org.minidns.common-conventions.gradle new file mode 100644 index 000000000..6e63cff3e --- /dev/null +++ b/src/main/groovy/org.minidns.common-conventions.gradle @@ -0,0 +1,36 @@ +ext { + javaVersion = JavaVersion.VERSION_11 + javaMajor = javaVersion.getMajorVersion() + minAndroidSdk = 19 + + androidBootClasspath = getAndroidRuntimeJar(minAndroidSdk) + + // Export the function by turning it into a closure. + // https://stackoverflow.com/a/23290820/194894 + getAndroidRuntimeJar = this.&getAndroidRuntimeJar +} + +repositories { + mavenLocal() + mavenCentral() +} + +def getAndroidRuntimeJar(androidApiLevel) { + def androidHome = getAndroidHome() + def androidJar = new File("$androidHome/platforms/android-${androidApiLevel}/android.jar") + if (androidJar.isFile()) { + return androidJar + } else { + throw new Exception("Can't find android.jar for API level ${androidApiLevel}. Please install corresponding SDK platform package") + } +} + +def getAndroidHome() { + def androidHomeEnv = System.getenv("ANDROID_HOME") + if (androidHomeEnv == null) { + throw new Exception("ANDROID_HOME environment variable is not set") + } + def androidHome = new File(androidHomeEnv) + if (!androidHome.isDirectory()) throw new Exception("Environment variable ANDROID_HOME is not pointing to a directory") + return androidHome +} diff --git a/src/main/groovy/org.minidns.java-conventions.gradle b/src/main/groovy/org.minidns.java-conventions.gradle new file mode 100644 index 000000000..1c14b39ca --- /dev/null +++ b/src/main/groovy/org.minidns.java-conventions.gradle @@ -0,0 +1,302 @@ +plugins { + id 'biz.aQute.bnd.builder' + id 'checkstyle' + id 'eclipse' + id 'idea' + id 'jacoco' + id 'java' + id 'java-library' + id 'java-test-fixtures' + id 'maven-publish' + id 'net.ltgt.errorprone' + id 'signing' + + id 'jacoco-report-aggregation' + id 'test-report-aggregation' + + id 'org.minidns.common-conventions' + id 'org.minidns.javadoc-conventions' +} + +version readVersionFile() + +ext { + isSnapshot = version.endsWith('-SNAPSHOT') + gitCommit = getGitCommit() + rootConfigDir = new File(rootDir, 'config') + sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword') + isReleaseVersion = !isSnapshot + isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) + signingRequired = !(isSnapshot || isContinuousIntegrationEnvironment) + sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots' + sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' + builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) + + junitVersion = '5.9.2' + + if (project.hasProperty("useSonatype")) { + useSonatype = project.getProperty("useSonatype").toBoolean() + } else { + // Default to true + useSonatype = true + } +} + +group = 'org.minidns' + +java { + sourceCompatibility = javaVersion + targetCompatibility = sourceCompatibility +} + +eclipse { + classpath { + downloadJavadoc = true + } +} + +// Make all project's 'test' target depend on javadoc, so that +// javadoc is also linted. +test.dependsOn javadoc + +tasks.withType(JavaCompile) { + // Some systems may not have set their platform default + // converter to 'utf8', but we use unicode in our source + // files. Therefore ensure that javac uses unicode + options.encoding = "utf8" + options.compilerArgs = [ + '-Xlint:all', + // Set '-options' because a non-java7 javac will emit a + // warning if source/target is set to 1.7 and + // bootclasspath is *not* set. + '-Xlint:-options', + // TODO: Enable xlint serial + '-Xlint:-serial', + '-Werror', + ] + options.release = Integer.valueOf(javaMajor) +} + +jacoco { + toolVersion = "0.8.12" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + testFixturesApi "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + // https://stackoverflow.com/a/77274251/194894 + testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.0" + + errorprone 'com.google.errorprone:error_prone_core:2.32.0' +} + +test { + useJUnitPlatform() + + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + + // Enable full stacktraces of failed tests. Especially handy + // for CI environments. + testLogging { + events "failed" + exceptionFormat "full" + } +} + +jar { + manifest { + attributes( + 'Implementation-Version': version, + 'Implementation-GitRevision': gitCommit, + 'Built-JDK': System.getProperty('java.version'), + 'Built-Gradle': gradle.gradleVersion, + 'Built-By': System.getProperty('user.name') + ) + } + + bundle { + bnd( + '-removeheaders': 'Tool, Bnd-*', + '-exportcontents': 'org.minidns.*', + 'Import-Package': '!android,*' + ) + } +} + +checkstyle { + toolVersion = '10.18.2' + + configProperties.checkstyleLicenseHeader = "header" +} +task sourcesJar(type: Jar, dependsOn: classes) { + archiveClassifier = 'sources' + from sourceSets.main.allSource +} +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} +task testsJar(type: Jar) { + archiveClassifier = 'tests' + from sourceSets.test.output +} +configurations { + testRuntime +} +artifacts { + // Add a 'testRuntime' configuration including the tests so that + // it can be consumed by other projects. See + // http://stackoverflow.com/a/21946676/194894 + testRuntime testsJar +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + artifact testsJar + pom { + name = 'MiniDNS' + packaging = 'jar' + inceptionYear = '2014' + url = 'https://github.com/minidns/minidns' + afterEvaluate { + description = project.description + } + + scm { + url = 'https://github.com/minidns/minidns' + connection = 'scm:https://github.com/minidns/minidns' + developerConnection = 'scm:git://github.com/minidns/minidns.git' + } + + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + + developers { + developer { + id = 'rtreffer' + name = 'Rene Treffer' + email = 'treffer@measite.de' + } + developer { + id = 'flow' + name = 'Florian Schmaus' + email = 'flow@geekplace.eu' + } + } + } + } + } + repositories { + if (sonatypeCredentialsAvailable && useSonatype) { + maven { + url isSnapshot ? sonatypeSnapshotUrl : sonatypeStagingUrl + credentials { + username = sonatypeUsername + password = sonatypePassword + } + } + } + // Use + // gradle publish -P customRepoUrl=https://www.igniterealtime.org/archiva/repository/maven -P customRepoUsername=bamboo -P customRepoPassword=hidden -P useSonatype=false + // to deploy to this repo. + if (project.hasProperty("customRepoUrl")) { + maven { + name 'customRepo' + url customRepoUrl + if (project.hasProperty("customRepoUsername")) { + credentials { + username customRepoUsername + password customRepoPassword + } + } + } + } + } +} + +// Workaround for gpg signatory not supporting the 'required' option +// See https://github.com/gradle/gradle/issues/5064#issuecomment-381924984 +// Note what we use 'signing.gnupg.keyName' instead of 'signing.keyId'. +tasks.withType(Sign) { + onlyIf { + project.hasProperty('signing.gnupg.keyName') + } +} +signing { + required { signingRequired } + useGpgCmd() + sign publishing.publications.mavenJava +} + +tasks.withType(JavaCompile) { + options.errorprone { + disableWarningsInGeneratedCode = true + excludedPaths = ".*/jmh_generated/.*" + error( + "UnusedVariable", + "UnusedMethod", + "MethodCanBeStatic", + ) + errorproneArgs = [ + // Disable MissingCasesInEnumSwitch error prone check + // because this check is already done by javac as incomplete-switch. + '-Xep:MissingCasesInEnumSwitch:OFF', + '-Xep:StringSplitter:OFF', + '-Xep:JavaTimeDefaultTimeZone:OFF', + '-Xep:InlineMeSuggester:OFF', + ] + } +} + +// Work around https://github.com/gradle/gradle/issues/4046 +task copyJavadocDocFiles(type: Copy) { + from('src/javadoc') + into 'build/docs/javadoc' + include '**/doc-files/*.*' +} +javadoc.dependsOn copyJavadocDocFiles + +def getGitCommit() { + def projectDirFile = new File("$projectDir") + + def cmd = 'git describe --always --tags --dirty=+' + def proc = cmd.execute(null, projectDirFile) + + def exitStatus = proc.waitFor() + if (exitStatus != 0) return "non-git build" + + def gitCommit = proc.text.trim() + assert !gitCommit.isEmpty() + gitCommit +} + +def readVersionFile() { + def versionFile = new File(rootDir, 'version') + if (!versionFile.isFile()) { + throw new Exception("Could not find version file") + } + if (versionFile.text.isEmpty()) { + throw new Exception("Version file does not contain a version") + } + versionFile.text.trim() +} diff --git a/src/main/groovy/org.minidns.javadoc-conventions.gradle b/src/main/groovy/org.minidns.javadoc-conventions.gradle new file mode 100644 index 000000000..49b0e8f61 --- /dev/null +++ b/src/main/groovy/org.minidns.javadoc-conventions.gradle @@ -0,0 +1,24 @@ +plugins { + // Javadoc linking requires repositories to bet configured. And + // those are declared in common-conventions, hence we add it here. + id 'org.minidns.common-conventions' +} + + +tasks.withType(Javadoc) { + // The '-quiet' as second argument is actually a hack, + // since the one parameter addStringOption doesn't seem to + // work, we extra add '-quiet', which is added anyway by + // gradle. + // We disable 'missing' as we do most of javadoc checking via checkstyle. + options.addStringOption('Xdoclint:all,-missing', '-quiet') + // Abort on javadoc warnings. + // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) + // for information about the -Xwerror option. + options.addStringOption('Xwerror', '-quiet') + options.addStringOption('-release', javaMajor) +} + +tasks.withType(Javadoc) { + options.charSet = "UTF-8" +} diff --git a/src/main/java/org/minidns/AbstractDnsClient.java b/src/main/java/org/minidns/AbstractDnsClient.java new file mode 100644 index 000000000..7b264a53e --- /dev/null +++ b/src/main/java/org/minidns/AbstractDnsClient.java @@ -0,0 +1,477 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import org.minidns.MiniDnsFuture.InternalMiniDnsFuture; +import org.minidns.cache.LruCache; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.Data; +import org.minidns.record.NS; +import org.minidns.record.Record; +import org.minidns.record.Record.CLASS; +import org.minidns.record.Record.TYPE; +import org.minidns.source.DnsDataSource; +import org.minidns.source.NetworkDataSource; + +import java.io.IOException; +import java.net.InetAddress; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support. + * This circumvents the missing javax.naming package on android. + */ +public abstract class AbstractDnsClient { + + protected static final LruCache DEFAULT_CACHE = new LruCache(); + + protected static final Logger LOGGER = Logger.getLogger(AbstractDnsClient.class.getName()); + + /** + * This callback is used by the synchronous query() method and by the asynchronous queryAync() method in order to update the + * cache. In the asynchronous case, hand this callback into the async call, so that it can get called once the result is available. + */ + private final DnsDataSource.OnResponseCallback onResponseCallback = new DnsDataSource.OnResponseCallback() { + @Override + public void onResponse(DnsMessage requestMessage, DnsQueryResult responseMessage) { + final Question q = requestMessage.getQuestion(); + if (cache != null && isResponseCacheable(q, responseMessage)) { + cache.put(requestMessage.asNormalizedVersion(), responseMessage); + } + } + }; + + /** + * The internal random class for sequence generation. + */ + protected final Random random; + + protected final Random insecureRandom = new Random(); + + /** + * The internal DNS cache. + */ + protected final DnsCache cache; + + protected DnsDataSource dataSource = new NetworkDataSource(); + + public enum IpVersionSetting { + + v4only(true, false), + v6only(false, true), + v4v6(true, true), + v6v4(true, true), + ; + + public final boolean v4; + public final boolean v6; + + IpVersionSetting(boolean v4, boolean v6) { + this.v4 = v4; + this.v6 = v6; + } + + } + + protected static IpVersionSetting DEFAULT_IP_VERSION_SETTING = IpVersionSetting.v4v6; + + public static void setDefaultIpVersion(IpVersionSetting preferedIpVersion) { + if (preferedIpVersion == null) { + throw new IllegalArgumentException(); + } + AbstractDnsClient.DEFAULT_IP_VERSION_SETTING = preferedIpVersion; + } + + protected IpVersionSetting ipVersionSetting = DEFAULT_IP_VERSION_SETTING; + + public void setPreferedIpVersion(IpVersionSetting preferedIpVersion) { + if (preferedIpVersion == null) { + throw new IllegalArgumentException(); + } + ipVersionSetting = preferedIpVersion; + } + + public IpVersionSetting getPreferedIpVersion() { + return ipVersionSetting; + } + + /** + * Create a new DNS client with the given DNS cache. + * + * @param cache The backend DNS cache. + */ + protected AbstractDnsClient(DnsCache cache) { + Random random; + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e1) { + random = new SecureRandom(); + } + this.random = random; + this.cache = cache; + } + + /** + * Create a new DNS client using the global default cache. + */ + protected AbstractDnsClient() { + this(DEFAULT_CACHE); + } + + /** + * Query the system nameservers for a single entry of any class. + * + * This can be used to determine the name server version, if name + * is version.bind, type is TYPE.TXT and clazz is CLASS.CH. + * + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @return The response (or null on timeout/error). + * @throws IOException if an IO error occurs. + */ + public final DnsQueryResult query(String name, TYPE type, CLASS clazz) throws IOException { + Question q = new Question(name, type, clazz); + return query(q); + } + + /** + * Query the system nameservers for a single entry of the class IN + * (which is used for MX, SRV, A, AAAA and most other RRs). + * + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @return The response (or null on timeout/error). + * @throws IOException if an IO error occurs. + */ + public final DnsQueryResult query(DnsName name, TYPE type) throws IOException { + Question q = new Question(name, type, CLASS.IN); + return query(q); + } + + /** + * Query the system nameservers for a single entry of the class IN + * (which is used for MX, SRV, A, AAAA and most other RRs). + * + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @return The response (or null on timeout/error). + * @throws IOException if an IO error occurs. + */ + public final DnsQueryResult query(CharSequence name, TYPE type) throws IOException { + Question q = new Question(name, type, CLASS.IN); + return query(q); + } + + public DnsQueryResult query(Question q) throws IOException { + DnsMessage.Builder query = buildMessage(q); + return query(query); + } + + /** + * Send a query request to the DNS system. + * + * @param query The query to send to the server. + * @return The response (or null). + * @throws IOException if an IO error occurs. + */ + protected abstract DnsQueryResult query(DnsMessage.Builder query) throws IOException; + + public final MiniDnsFuture queryAsync(CharSequence name, TYPE type) { + Question q = new Question(name, type, CLASS.IN); + return queryAsync(q); + } + + public final MiniDnsFuture queryAsync(Question q) { + DnsMessage.Builder query = buildMessage(q); + return queryAsync(query); + } + + /** + * Default implementation of an asynchronous DNS query which just wraps the synchronous case. + *

+ * Subclasses override this method to support true asynchronous queries. + *

+ * + * @param query the query. + * @return a future for this query. + */ + protected MiniDnsFuture queryAsync(DnsMessage.Builder query) { + InternalMiniDnsFuture future = new InternalMiniDnsFuture<>(); + DnsQueryResult result; + try { + result = query(query); + } catch (IOException e) { + future.setException(e); + return future; + } + future.setResult(result); + return future; + } + + public final DnsQueryResult query(Question q, InetAddress server, int port) throws IOException { + DnsMessage query = getQueryFor(q); + return query(query, server, port); + } + + public final DnsQueryResult query(DnsMessage requestMessage, InetAddress address, int port) throws IOException { + // See if we have the answer to this question already cached + DnsQueryResult responseMessage = (cache == null) ? null : cache.get(requestMessage); + if (responseMessage != null) { + return responseMessage; + } + + final Question q = requestMessage.getQuestion(); + + final Level TRACE_LOG_LEVEL = Level.FINE; + LOGGER.log(TRACE_LOG_LEVEL, "Asking {0} on {1} for {2} with:\n{3}", new Object[] { address, port, q, requestMessage }); + + try { + responseMessage = dataSource.query(requestMessage, address, port); + } catch (IOException e) { + LOGGER.log(TRACE_LOG_LEVEL, "IOException {0} on {1} while resolving {2}: {3}", new Object[] { address, port, q, e}); + throw e; + } + + LOGGER.log(TRACE_LOG_LEVEL, "Response from {0} on {1} for {2}:\n{3}", new Object[] { address, port, q, responseMessage }); + + onResponseCallback.onResponse(requestMessage, responseMessage); + + return responseMessage; + } + + public final MiniDnsFuture queryAsync(DnsMessage requestMessage, InetAddress address, int port) { + // See if we have the answer to this question already cached + DnsQueryResult responseMessage = (cache == null) ? null : cache.get(requestMessage); + if (responseMessage != null) { + return MiniDnsFuture.from(responseMessage); + } + + final Question q = requestMessage.getQuestion(); + + final Level TRACE_LOG_LEVEL = Level.FINE; + LOGGER.log(TRACE_LOG_LEVEL, "Asynchronusly asking {0} on {1} for {2} with:\n{3}", new Object[] { address, port, q, requestMessage }); + + return dataSource.queryAsync(requestMessage, address, port, onResponseCallback); + } + + /** + * Whether a response from the DNS system should be cached or not. + * + * @param q The question the response message should answer. + * @param result The DNS query result. + * @return True, if the response should be cached, false otherwise. + */ + protected boolean isResponseCacheable(Question q, DnsQueryResult result) { + DnsMessage dnsMessage = result.response; + for (Record record : dnsMessage.answerSection) { + if (record.isAnswer(q)) { + return true; + } + } + return false; + } + + /** + * Builds a {@link DnsMessage} object carrying the given Question. + * + * @param question {@link Question} to be put in the DNS request. + * @return A {@link DnsMessage} requesting the answer for the given Question. + */ + final DnsMessage.Builder buildMessage(Question question) { + DnsMessage.Builder message = DnsMessage.builder(); + message.setQuestion(question); + message.setId(random.nextInt()); + message = newQuestion(message); + return message; + } + + protected abstract DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage); + + /** + * Query a nameserver for a single entry. + * + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @param address The DNS server address. + * @param port The DNS server port. + * @return The response (or null on timeout / failure). + * @throws IOException On IO Errors. + */ + public DnsQueryResult query(String name, TYPE type, CLASS clazz, InetAddress address, int port) + throws IOException { + Question q = new Question(name, type, clazz); + return query(q, address, port); + } + + /** + * Query a nameserver for a single entry. + * + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param clazz The class of the request (usually IN for Internet). + * @param address The DNS server host. + * @return The response (or null on timeout / failure). + * @throws IOException On IO Errors. + */ + public DnsQueryResult query(String name, TYPE type, CLASS clazz, InetAddress address) + throws IOException { + Question q = new Question(name, type, clazz); + return query(q, address); + } + + /** + * Query a nameserver for a single entry of class IN. + * + * @param name The DNS name to request. + * @param type The DNS type to request (SRV, A, AAAA, ...). + * @param address The DNS server host. + * @return The response (or null on timeout / failure). + * @throws IOException On IO Errors. + */ + public DnsQueryResult query(String name, TYPE type, InetAddress address) + throws IOException { + Question q = new Question(name, type, CLASS.IN); + return query(q, address); + } + + public final DnsQueryResult query(DnsMessage query, InetAddress host) throws IOException { + return query(query, host, 53); + } + + /** + * Query a specific server for one entry. + * + * @param q The question section of the DNS query. + * @param address The dns server address. + * @return The a DNS query result. + * @throws IOException On IOErrors. + */ + public DnsQueryResult query(Question q, InetAddress address) throws IOException { + return query(q, address, 53); + } + + public final MiniDnsFuture queryAsync(DnsMessage query, InetAddress dnsServer) { + return queryAsync(query, dnsServer, 53); + } + + /** + * Returns the currently used {@link DnsDataSource}. See {@link #setDataSource(DnsDataSource)} for details. + * + * @return The currently used {@link DnsDataSource} + */ + public DnsDataSource getDataSource() { + return dataSource; + } + + /** + * Set a {@link DnsDataSource} to be used by the DnsClient. + * The default implementation will direct all queries directly to the Internet. + * + * This can be used to define a non-default handling for outgoing data. This can be useful to redirect the requests + * to a proxy or to modify requests after or responses before they are handled by the DnsClient implementation. + * + * @param dataSource An implementation of DNSDataSource that shall be used. + */ + public void setDataSource(DnsDataSource dataSource) { + if (dataSource == null) { + throw new IllegalArgumentException(); + } + this.dataSource = dataSource; + } + + /** + * Get the cache used by this DNS client. + * + * @return the cached used by this DNS client or null. + */ + public DnsCache getCache() { + return cache; + } + + protected DnsMessage getQueryFor(Question q) { + DnsMessage.Builder messageBuilder = buildMessage(q); + DnsMessage query = messageBuilder.build(); + return query; + } + + private Set getCachedRecordsFor(DnsName dnsName, TYPE type) { + if (cache == null) + return Collections.emptySet(); + + Question dnsNameNs = new Question(dnsName, type); + DnsMessage queryDnsNameNs = getQueryFor(dnsNameNs); + DnsQueryResult cachedResult = cache.get(queryDnsNameNs); + + if (cachedResult == null) + return Collections.emptySet(); + + return cachedResult.response.getAnswersFor(dnsNameNs); + } + + public Set getCachedNameserverRecordsFor(DnsName dnsName) { + return getCachedRecordsFor(dnsName, TYPE.NS); + } + + public Set getCachedIPv4AddressesFor(DnsName dnsName) { + return getCachedRecordsFor(dnsName, TYPE.A); + } + + public Set getCachedIPv6AddressesFor(DnsName dnsName) { + return getCachedRecordsFor(dnsName, TYPE.AAAA); + } + + @SuppressWarnings("unchecked") + private Set getCachedIPNameserverAddressesFor(DnsName dnsName, TYPE type) { + Set nsSet = getCachedNameserverRecordsFor(dnsName); + if (nsSet.isEmpty()) + return Collections.emptySet(); + + Set res = new HashSet<>(3 * nsSet.size()); + for (NS ns : nsSet) { + Set addresses; + switch (type) { + case A: + addresses = (Set) getCachedIPv4AddressesFor(ns.target); + break; + case AAAA: + addresses = (Set) getCachedIPv6AddressesFor(ns.target); + break; + default: + throw new AssertionError(); + } + res.addAll(addresses); + } + + return res; + } + + public Set getCachedIPv4NameserverAddressesFor(DnsName dnsName) { + return getCachedIPNameserverAddressesFor(dnsName, TYPE.A); + } + + public Set getCachedIPv6NameserverAddressesFor(DnsName dnsName) { + return getCachedIPNameserverAddressesFor(dnsName, TYPE.AAAA); + } +} diff --git a/src/main/java/org/minidns/DnsCache.java b/src/main/java/org/minidns/DnsCache.java new file mode 100644 index 000000000..fe0f32ab0 --- /dev/null +++ b/src/main/java/org/minidns/DnsCache.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.CachedDnsQueryResult; +import org.minidns.dnsqueryresult.DnsQueryResult; + +/** + * Cache for DNS Entries. Implementations must be thread safe. + */ +public abstract class DnsCache { + + public static final int DEFAULT_CACHE_SIZE = 512; + + /** + * Add an an dns answer/response for a given dns question. Implementations + * should honor the ttl / receive timestamp. + * @param query The query message containing a question. + * @param result The DNS query result. + */ + public final void put(DnsMessage query, DnsQueryResult result) { + putNormalized(query.asNormalizedVersion(), result); + } + + protected abstract void putNormalized(DnsMessage normalizedQuery, DnsQueryResult result); + + public abstract void offer(DnsMessage query, DnsQueryResult result, DnsName authoritativeZone); + + /** + * Request a cached dns response. + * @param query The query message containing a question. + * @return The dns message. + */ + public final CachedDnsQueryResult get(DnsMessage query) { + return getNormalized(query.asNormalizedVersion()); + } + + protected abstract CachedDnsQueryResult getNormalized(DnsMessage normalizedQuery); + +} diff --git a/src/main/java/org/minidns/DnsClient.java b/src/main/java/org/minidns/DnsClient.java new file mode 100644 index 000000000..00d84712f --- /dev/null +++ b/src/main/java/org/minidns/DnsClient.java @@ -0,0 +1,492 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import org.minidns.MiniDnsException.ErrorResponseException; +import org.minidns.MiniDnsException.NoQueryPossibleException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsserverlookup.AndroidUsingExec; +import org.minidns.dnsserverlookup.AndroidUsingReflection; +import org.minidns.dnsserverlookup.DnsServerLookupMechanism; +import org.minidns.dnsserverlookup.UnixUsingEtcResolvConf; +import org.minidns.record.Record.TYPE; +import org.minidns.util.CollectionsUtil; +import org.minidns.util.InetAddressUtil; +import org.minidns.util.MultipleIoException; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.logging.Level; + +/** + * A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support. + * This circumvents the missing javax.naming package on android. + */ +public class DnsClient extends AbstractDnsClient { + + static final List LOOKUP_MECHANISMS = new CopyOnWriteArrayList<>(); + + static final Set STATIC_IPV4_DNS_SERVERS = new CopyOnWriteArraySet<>(); + static final Set STATIC_IPV6_DNS_SERVERS = new CopyOnWriteArraySet<>(); + + static { + addDnsServerLookupMechanism(AndroidUsingExec.INSTANCE); + addDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE); + addDnsServerLookupMechanism(UnixUsingEtcResolvConf.INSTANCE); + + try { + Inet4Address dnsforgeV4Dns = InetAddressUtil.ipv4From("176.9.93.198"); + STATIC_IPV4_DNS_SERVERS.add(dnsforgeV4Dns); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e); + } + + try { + Inet6Address dnsforgeV6Dns = InetAddressUtil.ipv6From("[2a01:4f8:151:34aa::198]"); + STATIC_IPV6_DNS_SERVERS.add(dnsforgeV6Dns); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "Could not add static IPv6 DNS Server", e); + } + } + + private static final Set blacklistedDnsServers = Collections.newSetFromMap(new ConcurrentHashMap(4)); + + private final Set nonRaServers = Collections.newSetFromMap(new ConcurrentHashMap(4)); + + private boolean askForDnssec = false; + private boolean disableResultFilter = false; + + private boolean useHardcodedDnsServers = true; + + /** + * Create a new DNS client using the global default cache. + */ + public DnsClient() { + super(); + } + + public DnsClient(DnsCache dnsCache) { + super(dnsCache); + } + + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { + message.setRecursionDesired(true); + message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(askForDnssec); + return message; + } + + private List getServerAddresses() { + List dnsServerAddresses = findDnsAddresses(); + + if (useHardcodedDnsServers) { + InetAddress primaryHardcodedDnsServer, secondaryHardcodedDnsServer = null; + switch (ipVersionSetting) { + case v4v6: + primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer(); + secondaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer(); + break; + case v6v4: + primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer(); + secondaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer(); + break; + case v4only: + primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer(); + break; + case v6only: + primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer(); + break; + default: + throw new AssertionError("Unknown ipVersionSetting: " + ipVersionSetting); + } + + dnsServerAddresses.add(primaryHardcodedDnsServer); + if (secondaryHardcodedDnsServer != null) { + dnsServerAddresses.add(secondaryHardcodedDnsServer); + } + } + + return dnsServerAddresses; + } + + @Override + public DnsQueryResult query(DnsMessage.Builder queryBuilder) throws IOException { + DnsMessage q = newQuestion(queryBuilder).build(); + // While this query method does in fact re-use query(Question, String) + // we still do a cache lookup here in order to avoid unnecessary + // findDNS()calls, which are expensive on Android. Note that we do not + // put the results back into the Cache, as this is already done by + // query(Question, String). + DnsQueryResult dnsQueryResult = (cache == null) ? null : cache.get(q); + if (dnsQueryResult != null) { + return dnsQueryResult; + } + + List dnsServerAddresses = getServerAddresses(); + + List ioExceptions = new ArrayList<>(dnsServerAddresses.size()); + for (InetAddress dns : dnsServerAddresses) { + if (nonRaServers.contains(dns)) { + LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\""); + continue; + } + + try { + dnsQueryResult = query(q, dns); + } catch (IOException ioe) { + ioExceptions.add(ioe); + continue; + } + + DnsMessage responseMessage = dnsQueryResult.response; + if (!responseMessage.recursionAvailable) { + boolean newRaServer = nonRaServers.add(dns); + if (newRaServer) { + LOGGER.warning("The DNS server " + dns + + " returned a response without the \"recursion available\" (RA) flag set. This likely indicates a misconfiguration because the server is not suitable for DNS resolution"); + } + continue; + } + + if (disableResultFilter) { + return dnsQueryResult; + } + + switch (responseMessage.responseCode) { + case NO_ERROR: + case NX_DOMAIN: + break; + default: + String warning = "Response from " + dns + " asked for " + q.getQuestion() + " with error code: " + + responseMessage.responseCode + '.'; + if (!LOGGER.isLoggable(Level.FINE)) { + // Only append the responseMessage is log level is not fine. If it is fine or higher, the + // response has already been logged. + warning += "\n" + responseMessage; + } + LOGGER.warning(warning); + + ErrorResponseException exception = new ErrorResponseException(q, dnsQueryResult); + ioExceptions.add(exception); + continue; + } + + return dnsQueryResult; + } + MultipleIoException.throwIfRequired(ioExceptions); + + // TODO: Shall we add the attempted DNS servers to the exception? + throw new NoQueryPossibleException(q); + } + + @Override + protected MiniDnsFuture queryAsync(DnsMessage.Builder queryBuilder) { + DnsMessage q = newQuestion(queryBuilder).build(); + // While this query method does in fact re-use query(Question, String) + // we still do a cache lookup here in order to avoid unnecessary + // findDNS()calls, which are expensive on Android. Note that we do not + // put the results back into the Cache, as this is already done by + // query(Question, String). + DnsQueryResult responseMessage = (cache == null) ? null : cache.get(q); + if (responseMessage != null) { + return MiniDnsFuture.from(responseMessage); + } + + final List dnsServerAddresses = getServerAddresses(); + + // Filter loop. + Iterator it = dnsServerAddresses.iterator(); + while (it.hasNext()) { + InetAddress dns = it.next(); + if (nonRaServers.contains(dns)) { + it.remove(); + LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\""); + continue; + } + } + + List> futures = new ArrayList<>(dnsServerAddresses.size()); + // "Main" loop. + for (InetAddress dns : dnsServerAddresses) { + MiniDnsFuture f = queryAsync(q, dns); + futures.add(f); + } + + return MiniDnsFuture.anySuccessfulOf(futures); + } + + /** + * Retrieve a list of currently configured DNS servers IP addresses. This method does verify that only IP addresses are returned and + * nothing else (e.g. DNS names). + *

+ * The addresses are discovered by using one (or more) of the configured {@link DnsServerLookupMechanism}s. + *

+ * + * @return A list of DNS server IP addresses configured for this system. + */ + public static List findDNS() { + List res = null; + final Level TRACE_LOG_LEVEL = Level.FINE; + for (DnsServerLookupMechanism mechanism : LOOKUP_MECHANISMS) { + try { + res = mechanism.getDnsServerAddresses(); + } catch (SecurityException exception) { + LOGGER.log(Level.WARNING, "Could not lookup DNS server", exception); + } + if (res == null) { + LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism '" + mechanism.getName() + "' did not return any DNS server"); + continue; + } + + if (LOGGER.isLoggable(TRACE_LOG_LEVEL)) { + // TODO: Use String.join() once MiniDNS is Android API 26 (or higher). + StringBuilder sb = new StringBuilder(); + for (Iterator it = res.iterator(); it.hasNext();) { + String s = it.next(); + sb.append(s); + if (it.hasNext()) { + sb.append(", "); + } + } + String dnsServers = sb.toString(); + LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism {0} returned the following DNS servers: {1}", + new Object[] { mechanism.getName(), dnsServers }); + } + + assert !res.isEmpty(); + + // We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial + // though, because the list returned by the server mechanism is rather short. + + // Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else, + // especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using + // getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside + // of its DNSSEC guarantees. + Iterator it = res.iterator(); + while (it.hasNext()) { + String potentialDnsServer = it.next(); + if (!InetAddressUtil.isIpAddress(potentialDnsServer)) { + LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName() + + "' returned an invalid non-IP address result: '" + potentialDnsServer + "'"); + it.remove(); + } else if (blacklistedDnsServers.contains(potentialDnsServer)) { + LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName() + + "' returned a blacklisted result: '" + potentialDnsServer + "'"); + it.remove(); + } + } + + if (!res.isEmpty()) { + break; + } + + LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName() + + "' returned not a single valid IP address after sanitazion"); + res = null; + } + + return res; + } + + /** + * Retrieve a list of currently configured DNS server addresses. + *

+ * Note that unlike {@link #findDNS()}, the list returned by this method + * will take the IP version setting into account, and order the list by the + * preferred address types (IPv4/v6). The returned list is modifiable. + *

+ * + * @return A list of DNS server addresses. + * @see #findDNS() + */ + public static List findDnsAddresses() { + // The findDNS() method contract guarantees that only IP addresses will be returned. + List res = findDNS(); + + if (res == null) { + return new ArrayList<>(); + } + + final IpVersionSetting setting = DEFAULT_IP_VERSION_SETTING; + + List ipv4DnsServer = null; + List ipv6DnsServer = null; + if (setting.v4) { + ipv4DnsServer = new ArrayList<>(res.size()); + } + if (setting.v6) { + ipv6DnsServer = new ArrayList<>(res.size()); + } + + int validServerAddresses = 0; + for (String dnsServerString : res) { + // The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings + // representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being + // involved. Something we want to avoid. + assert InetAddressUtil.isIpAddress(dnsServerString); + + InetAddress dnsServerAddress; + try { + dnsServerAddress = InetAddress.getByName(dnsServerString); + } catch (UnknownHostException e) { + LOGGER.log(Level.SEVERE, "Could not transform '" + dnsServerString + "' to InetAddress", e); + continue; + } + if (dnsServerAddress instanceof Inet4Address) { + if (!setting.v4) { + continue; + } + Inet4Address ipv4DnsServerAddress = (Inet4Address) dnsServerAddress; + ipv4DnsServer.add(ipv4DnsServerAddress); + } else if (dnsServerAddress instanceof Inet6Address) { + if (!setting.v6) { + continue; + } + Inet6Address ipv6DnsServerAddress = (Inet6Address) dnsServerAddress; + ipv6DnsServer.add(ipv6DnsServerAddress); + } else { + throw new AssertionError("The address '" + dnsServerAddress + "' is neither of type Inet(4|6)Address"); + } + + validServerAddresses++; + } + + List dnsServers = new ArrayList<>(validServerAddresses); + + switch (setting) { + case v4v6: + dnsServers.addAll(ipv4DnsServer); + dnsServers.addAll(ipv6DnsServer); + break; + case v6v4: + dnsServers.addAll(ipv6DnsServer); + dnsServers.addAll(ipv4DnsServer); + break; + case v4only: + dnsServers.addAll(ipv4DnsServer); + break; + case v6only: + dnsServers.addAll(ipv6DnsServer); + break; + } + return dnsServers; + } + + public static void addDnsServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) { + if (!dnsServerLookup.isAvailable()) { + LOGGER.fine("Not adding " + dnsServerLookup.getName() + " as it is not available."); + return; + } + synchronized (LOOKUP_MECHANISMS) { + // We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace + // LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see + // http://stackoverflow.com/a/34827492/194894 + // TODO: Remove that workaround once MiniDNS is Java 8 only. + ArrayList tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1); + tempList.addAll(LOOKUP_MECHANISMS); + tempList.add(dnsServerLookup); + + // Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7. + Collections.sort(tempList); + + LOOKUP_MECHANISMS.clear(); + LOOKUP_MECHANISMS.addAll(tempList); + } + } + + public static boolean removeDNSServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) { + synchronized (LOOKUP_MECHANISMS) { + return LOOKUP_MECHANISMS.remove(dnsServerLookup); + } + } + + public static boolean addBlacklistedDnsServer(String dnsServer) { + return blacklistedDnsServers.add(dnsServer); + } + + public static boolean removeBlacklistedDnsServer(String dnsServer) { + return blacklistedDnsServers.remove(dnsServer); + } + + public boolean isAskForDnssec() { + return askForDnssec; + } + + public void setAskForDnssec(boolean askForDnssec) { + this.askForDnssec = askForDnssec; + } + + public boolean isDisableResultFilter() { + return disableResultFilter; + } + + public void setDisableResultFilter(boolean disableResultFilter) { + this.disableResultFilter = disableResultFilter; + } + + public boolean isUseHardcodedDnsServersEnabled() { + return useHardcodedDnsServers; + } + + public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) { + this.useHardcodedDnsServers = useHardcodedDnsServers; + } + + public InetAddress getRandomHardcodedIpv4DnsServer() { + return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom); + } + + public InetAddress getRandomHarcodedIpv6DnsServer() { + return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom); + } + + private static Question getReverseIpLookupQuestionFor(DnsName dnsName) { + return new Question(dnsName, TYPE.PTR); + } + + public static Question getReverseIpLookupQuestionFor(Inet4Address inet4Address) { + DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet4Address); + DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IN_ADDR_ARPA); + return getReverseIpLookupQuestionFor(dnsName); + } + + public static Question getReverseIpLookupQuestionFor(Inet6Address inet6Address) { + DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet6Address); + DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IP6_ARPA); + return getReverseIpLookupQuestionFor(dnsName); + } + + public static Question getReverseIpLookupQuestionFor(InetAddress inetAddress) { + if (inetAddress instanceof Inet4Address) { + return getReverseIpLookupQuestionFor((Inet4Address) inetAddress); + } else if (inetAddress instanceof Inet6Address) { + return getReverseIpLookupQuestionFor((Inet6Address) inetAddress); + } else { + throw new IllegalArgumentException("The provided inetAddress '" + inetAddress + + "' is neither of type Inet4Address nor Inet6Address"); + } + } + +} diff --git a/src/main/java/org/minidns/MiniDnsConfiguration.java b/src/main/java/org/minidns/MiniDnsConfiguration.java new file mode 100644 index 000000000..f02298f51 --- /dev/null +++ b/src/main/java/org/minidns/MiniDnsConfiguration.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +public class MiniDnsConfiguration { + + public static String getVersion() { + return MiniDnsInitialization.VERSION; + } + +} diff --git a/src/main/java/org/minidns/MiniDnsException.java b/src/main/java/org/minidns/MiniDnsException.java new file mode 100644 index 000000000..6b4efdd92 --- /dev/null +++ b/src/main/java/org/minidns/MiniDnsException.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import java.io.IOException; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; + +public abstract class MiniDnsException extends IOException { + /** + * + */ + private static final long serialVersionUID = 1L; + + protected MiniDnsException(String message) { + super(message); + } + + public static class IdMismatch extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage request; + private final DnsMessage response; + + public IdMismatch(DnsMessage request, DnsMessage response) { + super(getString(request, response)); + assert request.id != response.id; + this.request = request; + this.response = response; + } + + public DnsMessage getRequest() { + return request; + } + + public DnsMessage getResponse() { + return response; + } + + private static String getString(DnsMessage request, DnsMessage response) { + return "The response's ID doesn't matches the request ID. Request: " + request.id + ". Response: " + response.id; + } + } + + public static class NullResultException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage request; + + public NullResultException(DnsMessage request) { + super("The request yielded a 'null' result while resolving."); + this.request = request; + } + + public DnsMessage getRequest() { + return request; + } + } + + public static class ErrorResponseException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage request; + private final DnsQueryResult result; + + public ErrorResponseException(DnsMessage request, DnsQueryResult result) { + super("Received " + result.response.responseCode + " error response\n" + result); + this.request = request; + this.result = result; + } + + public DnsMessage getRequest() { + return request; + } + + public DnsQueryResult getResult() { + return result; + } + } + + public static class NoQueryPossibleException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage request; + + public NoQueryPossibleException(DnsMessage request) { + super("No DNS server could be queried"); + this.request = request; + } + + public DnsMessage getRequest() { + return request; + } + } +} diff --git a/src/main/java/org/minidns/MiniDnsFuture.java b/src/main/java/org/minidns/MiniDnsFuture.java new file mode 100644 index 000000000..ca183b363 --- /dev/null +++ b/src/main/java/org/minidns/MiniDnsFuture.java @@ -0,0 +1,282 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.minidns.util.CallbackRecipient; +import org.minidns.util.ExceptionCallback; +import org.minidns.util.MultipleIoException; +import org.minidns.util.SuccessCallback; + +public abstract class MiniDnsFuture implements Future, CallbackRecipient { + + private boolean cancelled; + + protected V result; + + protected E exception; + + private SuccessCallback successCallback; + + private ExceptionCallback exceptionCallback; + + @Override + public synchronized boolean cancel(boolean mayInterruptIfRunning) { + if (isDone()) { + return false; + } + + cancelled = true; + + if (mayInterruptIfRunning) { + notifyAll(); + } + + return true; + } + + @Override + public final synchronized boolean isCancelled() { + return cancelled; + } + + @Override + public final synchronized boolean isDone() { + return hasResult() || hasException(); + } + + public final synchronized boolean hasResult() { + return result != null; + } + + public final synchronized boolean hasException() { + return exception != null; + } + + @Override + public CallbackRecipient onSuccess(SuccessCallback successCallback) { + this.successCallback = successCallback; + maybeInvokeCallbacks(); + return this; + } + + @Override + public CallbackRecipient onError(ExceptionCallback exceptionCallback) { + this.exceptionCallback = exceptionCallback; + maybeInvokeCallbacks(); + return this; + } + + private V getOrThrowExecutionException() throws ExecutionException { + assert result != null || exception != null || cancelled; + if (result != null) { + return result; + } + if (exception != null) { + throw new ExecutionException(exception); + } + + assert cancelled; + throw new CancellationException(); + } + + @Override + public final synchronized V get() throws InterruptedException, ExecutionException { + while (result == null && exception == null && !cancelled) { + wait(); + } + + return getOrThrowExecutionException(); + } + + public final synchronized V getOrThrow() throws E { + while (result == null && exception == null && !cancelled) { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + if (exception != null) { + throw exception; + } + + if (cancelled) { + throw new CancellationException(); + } + + assert result != null; + return result; + } + + @Override + public final synchronized V get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + final long deadline = System.currentTimeMillis() + unit.toMillis(timeout); + while (result != null && exception != null && !cancelled) { + final long waitTimeRemaining = deadline - System.currentTimeMillis(); + if (waitTimeRemaining > 0) { + wait(waitTimeRemaining); + } + } + + if (cancelled) { + throw new CancellationException(); + } + + if (result == null || exception == null) { + throw new TimeoutException(); + } + + return getOrThrowExecutionException(); + } + + private static final ExecutorService EXECUTOR_SERVICE; + + static { + ThreadFactory threadFactory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setDaemon(true); + thread.setName("MiniDnsFuture Thread"); + return thread; + } + }; + BlockingQueue blockingQueue = new ArrayBlockingQueue<>(128); + RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + } + }; + int cores = Runtime.getRuntime().availableProcessors(); + int maximumPoolSize = cores <= 4 ? 2 : cores; + ExecutorService executorService = new ThreadPoolExecutor(0, maximumPoolSize, 60L, TimeUnit.SECONDS, blockingQueue, threadFactory, + rejectedExecutionHandler); + + EXECUTOR_SERVICE = executorService; + } + + @SuppressWarnings("FutureReturnValueIgnored") + protected final synchronized void maybeInvokeCallbacks() { + if (cancelled) { + return; + } + + if (result != null && successCallback != null) { + EXECUTOR_SERVICE.submit(new Runnable() { + @Override + public void run() { + successCallback.onSuccess(result); + } + }); + } else if (exception != null && exceptionCallback != null) { + EXECUTOR_SERVICE.submit(new Runnable() { + @Override + public void run() { + exceptionCallback.processException(exception); + } + }); + } + } + + public static class InternalMiniDnsFuture extends MiniDnsFuture { + public final synchronized void setResult(V result) { + if (isDone()) { + return; + } + + this.result = result; + this.notifyAll(); + + maybeInvokeCallbacks(); + } + + public final synchronized void setException(E exception) { + if (isDone()) { + return; + } + + this.exception = exception; + this.notifyAll(); + + maybeInvokeCallbacks(); + } + } + + public static MiniDnsFuture from(V result) { + InternalMiniDnsFuture future = new InternalMiniDnsFuture<>(); + future.setResult(result); + return future; + } + + public static MiniDnsFuture anySuccessfulOf(Collection> futures) { + return anySuccessfulOf(futures, exceptions -> MultipleIoException.toIOException(exceptions)); + } + + public interface ExceptionsWrapper { + EO wrap(List exceptions); + } + + public static MiniDnsFuture anySuccessfulOf( + Collection> futures, + ExceptionsWrapper exceptionsWrapper) { + InternalMiniDnsFuture returnedFuture = new InternalMiniDnsFuture<>(); + + final List exceptions = Collections.synchronizedList(new ArrayList<>(futures.size())); + + for (MiniDnsFuture future : futures) { + future.onSuccess(new SuccessCallback() { + @Override + public void onSuccess(V result) { + // Cancel all futures. Yes, this includes the future which just returned the + // result and futures which already failed with an exception, but then cancel + // will be a no-op. + for (MiniDnsFuture futureToCancel : futures) { + futureToCancel.cancel(true); + } + returnedFuture.setResult(result); + } + }); + future.onError(new ExceptionCallback() { + @Override + public void processException(EI exception) { + exceptions.add(exception); + // Signal the main future about the exceptions, but only if all sub-futures returned an exception. + if (exceptions.size() == futures.size()) { + EO returnedException = exceptionsWrapper.wrap(exceptions); + returnedFuture.setException(returnedException); + } + } + }); + } + + return returnedFuture; + } +} diff --git a/src/main/java/org/minidns/MiniDnsInitialization.java b/src/main/java/org/minidns/MiniDnsInitialization.java new file mode 100644 index 000000000..44ad4b519 --- /dev/null +++ b/src/main/java/org/minidns/MiniDnsInitialization.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MiniDnsInitialization { + + private static final Logger LOGGER = Logger.getLogger(MiniDnsInitialization.class.getName()); + + static final String VERSION; + + static { + String miniDnsVersion; + BufferedReader reader = null; + try { + InputStream is = MiniDnsInitialization.class.getClassLoader().getResourceAsStream("org.minidns/version"); + reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + miniDnsVersion = reader.readLine(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Could not determine MiniDNS version", e); + miniDnsVersion = "unkown"; + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "IOException closing stream", e); + } + } + } + VERSION = miniDnsVersion; + } +} diff --git a/src/main/java/org/minidns/RrSet.java b/src/main/java/org/minidns/RrSet.java new file mode 100644 index 000000000..cd0fce2a9 --- /dev/null +++ b/src/main/java/org/minidns/RrSet.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Data; +import org.minidns.record.Record; +import org.minidns.record.Record.CLASS; +import org.minidns.record.Record.TYPE; + +public final class RrSet { + + public final DnsName name; + public final TYPE type; + public final CLASS clazz; + public final Set> records; + + private RrSet(DnsName name, TYPE type, CLASS clazz, Set> records) { + this.name = name; + this.type = type; + this.clazz = clazz; + this.records = Collections.unmodifiableSet(records); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(name).append('\t').append(clazz).append('\t').append(type).append('\n'); + for (Record record : records) { + sb.append(record).append('\n'); + } + return sb.toString(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private DnsName name; + private TYPE type; + private CLASS clazz; + Set> records = new LinkedHashSet<>(8); + + private Builder() { + } + + public Builder addRecord(Record record) { + if (name == null) { + name = record.name; + type = record.type; + clazz = record.clazz; + } else if (!couldContain(record)) { + throw new IllegalArgumentException( + "Can not add " + record + " to RRSet " + name + ' ' + type + ' ' + clazz); + } + + boolean didNotExist = records.add(record); + assert didNotExist; + + return this; + } + + public boolean couldContain(Record record) { + if (name == null) { + return true; + } + return name.equals(record.name) && type == record.type && clazz == record.clazz; + } + + public boolean addIfPossible(Record record) { + if (!couldContain(record)) { + return false; + } + addRecord(record); + return true; + } + + public RrSet build() { + if (name == null) { + // There is no RR added to this builder. + throw new IllegalStateException(); + } + return new RrSet(name, type, clazz, records); + } + } +} diff --git a/src/main/java/org/minidns/cache/ExtendedLruCache.java b/src/main/java/org/minidns/cache/ExtendedLruCache.java new file mode 100644 index 000000000..b977e92f0 --- /dev/null +++ b/src/main/java/org/minidns/cache/ExtendedLruCache.java @@ -0,0 +1,129 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.cache; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.CachedDnsQueryResult; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsqueryresult.SynthesizedCachedDnsQueryResult; +import org.minidns.record.Data; +import org.minidns.record.Record; + +/** + * A variant of {@link LruCache} also using the data found in the sections for caching. + */ +public class ExtendedLruCache extends LruCache { + + public ExtendedLruCache() { + this(DEFAULT_CACHE_SIZE); + } + + public ExtendedLruCache(int capacity) { + super(capacity); + } + + public ExtendedLruCache(int capacity, long maxTTL) { + super(capacity, maxTTL); + } + + @SuppressWarnings("UnsynchronizedOverridesSynchronized") + @Override + protected void putNormalized(DnsMessage q, DnsQueryResult result) { + super.putNormalized(q, result); + DnsMessage message = result.response; + Map>> extraCaches = new HashMap<>(message.additionalSection.size()); + + gather(extraCaches, q, message.answerSection, null); + gather(extraCaches, q, message.authoritySection, null); + gather(extraCaches, q, message.additionalSection, null); + + putExtraCaches(result, extraCaches); + } + + @Override + public void offer(DnsMessage query, DnsQueryResult result, DnsName authoritativeZone) { + DnsMessage reply = result.response; + // The reply shouldn't be an authoritative answers when offer() is used. That would be a case for put(). + assert !reply.authoritativeAnswer; + + Map>> extraCaches = new HashMap<>(reply.additionalSection.size()); + + // N.B. not gathering from reply.answerSection here. Since it is a non authoritativeAnswer it shouldn't contain anything. + gather(extraCaches, query, reply.authoritySection, authoritativeZone); + gather(extraCaches, query, reply.additionalSection, authoritativeZone); + + putExtraCaches(result, extraCaches); + } + + private void gather(Map>> extraCaches, DnsMessage q, List> records, DnsName authoritativeZone) { + for (Record extraRecord : records) { + if (!shouldGather(extraRecord, q.getQuestion(), authoritativeZone)) + continue; + + DnsMessage.Builder additionalRecordQuestionBuilder = extraRecord.getQuestionMessage(); + if (additionalRecordQuestionBuilder == null) + continue; + + additionalRecordQuestionBuilder.copyFlagsFrom(q); + + additionalRecordQuestionBuilder.setAdditionalResourceRecords(q.additionalSection); + + DnsMessage additionalRecordQuestion = additionalRecordQuestionBuilder.build(); + if (additionalRecordQuestion.equals(q)) { + // No need to cache the additional question if it is the same as the original question. + continue; + } + + List> additionalRecords = extraCaches.get(additionalRecordQuestion); + if (additionalRecords == null) { + additionalRecords = new ArrayList<>(); + extraCaches.put(additionalRecordQuestion, additionalRecords); + } + additionalRecords.add(extraRecord); + } + } + + private void putExtraCaches(DnsQueryResult synthesynthesizationSource, Map>> extraCaches) { + DnsMessage reply = synthesynthesizationSource.response; + for (Entry>> entry : extraCaches.entrySet()) { + DnsMessage question = entry.getKey(); + DnsMessage answer = reply.asBuilder() + .setQuestion(question.getQuestion()) + .setAuthoritativeAnswer(true) + .addAnswers(entry.getValue()) + .build(); + CachedDnsQueryResult cachedDnsQueryResult = new SynthesizedCachedDnsQueryResult(question, answer, synthesynthesizationSource); + synchronized (this) { + backend.put(question, cachedDnsQueryResult); + } + } + } + + protected boolean shouldGather(Record extraRecord, Question question, DnsName authoritativeZone) { + boolean extraRecordIsChildOfQuestion = extraRecord.name.isChildOf(question.name); + + boolean extraRecordIsChildOfAuthoritativeZone = false; + if (authoritativeZone != null) { + extraRecordIsChildOfAuthoritativeZone = extraRecord.name.isChildOf(authoritativeZone); + } + + return extraRecordIsChildOfQuestion || extraRecordIsChildOfAuthoritativeZone; + } + +} diff --git a/src/main/java/org/minidns/cache/FullLruCache.java b/src/main/java/org/minidns/cache/FullLruCache.java new file mode 100644 index 000000000..768bee988 --- /dev/null +++ b/src/main/java/org/minidns/cache/FullLruCache.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.cache; + +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.record.Data; +import org.minidns.record.Record; + +/** + * An insecure variant of {@link LruCache} also using all the data found in the sections of an answer. + */ +public class FullLruCache extends ExtendedLruCache { + + public FullLruCache() { + this(DEFAULT_CACHE_SIZE); + } + + public FullLruCache(int capacity) { + super(capacity); + } + + public FullLruCache(int capacity, long maxTTL) { + super(capacity, maxTTL); + } + + @Override + protected boolean shouldGather(Record extraRecord, Question question, DnsName authoritativeZone) { + return true; + } +} diff --git a/src/main/java/org/minidns/cache/LruCache.java b/src/main/java/org/minidns/cache/LruCache.java new file mode 100644 index 000000000..8718f3ba8 --- /dev/null +++ b/src/main/java/org/minidns/cache/LruCache.java @@ -0,0 +1,169 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.cache; + +import java.util.LinkedHashMap; +import java.util.Map.Entry; + +import org.minidns.DnsCache; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.CachedDnsQueryResult; +import org.minidns.dnsqueryresult.DirectCachedDnsQueryResult; +import org.minidns.dnsqueryresult.DnsQueryResult; + +/** + * LRU based DNSCache backed by a LinkedHashMap. + */ +public class LruCache extends DnsCache { + + /** + * Internal miss count. + */ + protected long missCount = 0L; + + /** + * Internal expire count (subset of misses that was caused by expire). + */ + protected long expireCount = 0L; + + /** + * Internal hit count. + */ + protected long hitCount = 0L; + + /** + * The internal capacity of the backend cache. + */ + protected int capacity; + + /** + * The upper bound of the ttl. All longer TTLs will be capped by this ttl. + */ + protected long maxTTL; + + /** + * The backend cache. + */ + protected LinkedHashMap backend; + + /** + * Create a new LRUCache with given capacity and upper bound ttl. + * @param capacity The internal capacity. + * @param maxTTL The upper bound for any ttl. + */ + @SuppressWarnings("serial") + public LruCache(final int capacity, final long maxTTL) { + this.capacity = capacity; + this.maxTTL = maxTTL; + backend = new LinkedHashMap( + Math.min(capacity + (capacity + 3) / 4 + 2, 11), 0.75f, true) { + @Override + protected boolean removeEldestEntry( + Entry eldest) { + return size() > capacity; + } + }; + } + + /** + * Create a new LRUCache with given capacity. + * @param capacity The capacity of this cache. + */ + public LruCache(final int capacity) { + this(capacity, Long.MAX_VALUE); + } + + public LruCache() { + this(DEFAULT_CACHE_SIZE); + } + + @Override + protected synchronized void putNormalized(DnsMessage q, DnsQueryResult result) { + if (result.response.receiveTimestamp <= 0L) { + return; + } + backend.put(q, new DirectCachedDnsQueryResult(q, result)); + } + + @Override + protected synchronized CachedDnsQueryResult getNormalized(DnsMessage q) { + CachedDnsQueryResult result = backend.get(q); + if (result == null) { + missCount++; + return null; + } + + DnsMessage message = result.response; + + // RFC 2181 § 5.2 says that all TTLs in a RRSet should be equal, if this isn't the case, then we assume the + // shortest TTL to be the effective one. + final long answersMinTtl = message.getAnswersMinTtl(); + final long ttl = Math.min(answersMinTtl, maxTTL); + + final long expiryDate = message.receiveTimestamp + (ttl * 1000); + final long now = System.currentTimeMillis(); + if (expiryDate < now) { + missCount++; + expireCount++; + backend.remove(q); + return null; + } else { + hitCount++; + return result; + } + } + + /** + * Clear all entries in this cache. + */ + public synchronized void clear() { + backend.clear(); + missCount = 0L; + hitCount = 0L; + expireCount = 0L; + } + + /** + * Get the miss count of this cache which is the number of fruitless + * get calls since this cache was last resetted. + * @return The number of cache misses. + */ + public long getMissCount() { + return missCount; + } + + /** + * The number of expires (cache hits that have had a ttl to low to be + * retrieved). + * @return The expire count. + */ + public long getExpireCount() { + return expireCount; + } + + /** + * The cache hit count (all successful calls to get). + * @return The hit count. + */ + public long getHitCount() { + return hitCount; + } + + @Override + public String toString() { + return "LRUCache{usage=" + backend.size() + "/" + capacity + ", hits=" + hitCount + ", misses=" + missCount + ", expires=" + expireCount + "}"; + } + + @Override + public void offer(DnsMessage query, DnsQueryResult result, DnsName knownAuthoritativeZone) { + } +} diff --git a/src/main/java/org/minidns/cache/MiniDnsCacheFactory.java b/src/main/java/org/minidns/cache/MiniDnsCacheFactory.java new file mode 100644 index 000000000..54c25c92d --- /dev/null +++ b/src/main/java/org/minidns/cache/MiniDnsCacheFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.cache; + +import org.minidns.DnsCache; + +public interface MiniDnsCacheFactory { + + DnsCache newCache(); + +} diff --git a/src/main/java/org/minidns/constants/DnsRootServer.java b/src/main/java/org/minidns/constants/DnsRootServer.java new file mode 100644 index 000000000..9bc0c0402 --- /dev/null +++ b/src/main/java/org/minidns/constants/DnsRootServer.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.constants; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class DnsRootServer { + + private static final Map IPV4_ROOT_SERVER_MAP = new HashMap<>(); + + private static final Map IPV6_ROOT_SERVER_MAP = new HashMap<>(); + + protected static final Inet4Address[] IPV4_ROOT_SERVERS = new Inet4Address[] { + rootServerInet4Address('a', 198, 41, 0, 4), + rootServerInet4Address('b', 192, 228, 79, 201), + rootServerInet4Address('c', 192, 33, 4, 12), + rootServerInet4Address('d', 199, 7, 91 , 13), + rootServerInet4Address('e', 192, 203, 230, 10), + rootServerInet4Address('f', 192, 5, 5, 241), + rootServerInet4Address('g', 192, 112, 36, 4), + rootServerInet4Address('h', 198, 97, 190, 53), + rootServerInet4Address('i', 192, 36, 148, 17), + rootServerInet4Address('j', 192, 58, 128, 30), + rootServerInet4Address('k', 193, 0, 14, 129), + rootServerInet4Address('l', 199, 7, 83, 42), + rootServerInet4Address('m', 202, 12, 27, 33), + }; + + protected static final Inet6Address[] IPV6_ROOT_SERVERS = new Inet6Address[] { + rootServerInet6Address('a', 0x2001, 0x0503, 0xba3e, 0x0000, 0x0000, 0x000, 0x0002, 0x0030), + rootServerInet6Address('b', 0x2001, 0x0500, 0x0084, 0x0000, 0x0000, 0x000, 0x0000, 0x000b), + rootServerInet6Address('c', 0x2001, 0x0500, 0x0002, 0x0000, 0x0000, 0x000, 0x0000, 0x000c), + rootServerInet6Address('d', 0x2001, 0x0500, 0x002d, 0x0000, 0x0000, 0x000, 0x0000, 0x000d), + rootServerInet6Address('f', 0x2001, 0x0500, 0x002f, 0x0000, 0x0000, 0x000, 0x0000, 0x000f), + rootServerInet6Address('h', 0x2001, 0x0500, 0x0001, 0x0000, 0x0000, 0x000, 0x0000, 0x0053), + rootServerInet6Address('i', 0x2001, 0x07fe, 0x0000, 0x0000, 0x0000, 0x000, 0x0000, 0x0053), + rootServerInet6Address('j', 0x2001, 0x0503, 0x0c27, 0x0000, 0x0000, 0x000, 0x0002, 0x0030), + rootServerInet6Address('l', 0x2001, 0x0500, 0x0003, 0x0000, 0x0000, 0x000, 0x0000, 0x0042), + rootServerInet6Address('m', 0x2001, 0x0dc3, 0x0000, 0x0000, 0x0000, 0x000, 0x0000, 0x0035), + }; + + private static Inet4Address rootServerInet4Address(char rootServerId, int addr0, int addr1, int addr2, int addr3) { + Inet4Address inetAddress; + String name = rootServerId + ".root-servers.net"; + try { + inetAddress = (Inet4Address) InetAddress.getByAddress(name, new byte[] { (byte) addr0, (byte) addr1, (byte) addr2, + (byte) addr3 }); + IPV4_ROOT_SERVER_MAP.put(rootServerId, inetAddress); + } catch (UnknownHostException e) { + // This should never happen, if it does it's our fault! + throw new RuntimeException(e); + } + + return inetAddress; + } + + private static Inet6Address rootServerInet6Address(char rootServerId, int addr0, int addr1, int addr2, int addr3, int addr4, int addr5, int addr6, int addr7) { + Inet6Address inetAddress; + String name = rootServerId + ".root-servers.net"; + try { + inetAddress = (Inet6Address) InetAddress.getByAddress(name, new byte[] { + // @formatter:off + (byte) (addr0 >> 8), (byte) addr0, (byte) (addr1 >> 8), (byte) addr1, + (byte) (addr2 >> 8), (byte) addr2, (byte) (addr3 >> 8), (byte) addr3, + (byte) (addr4 >> 8), (byte) addr4, (byte) (addr5 >> 8), (byte) addr5, + (byte) (addr6 >> 8), (byte) addr6, (byte) (addr7 >> 8), (byte) addr7 + // @formatter:on + }); + IPV6_ROOT_SERVER_MAP.put(rootServerId, inetAddress); + } catch (UnknownHostException e) { + // This should never happen, if it does it's our fault! + throw new RuntimeException(e); + } + return inetAddress; + } + + public static Inet4Address getRandomIpv4RootServer(Random random) { + return IPV4_ROOT_SERVERS[random.nextInt(IPV4_ROOT_SERVERS.length)]; + } + + public static Inet6Address getRandomIpv6RootServer(Random random) { + return IPV6_ROOT_SERVERS[random.nextInt(IPV6_ROOT_SERVERS.length)]; + } + + public static Inet4Address getIpv4RootServerById(char id) { + return IPV4_ROOT_SERVER_MAP.get(id); + } + + public static Inet6Address getIpv6RootServerById(char id) { + return IPV6_ROOT_SERVER_MAP.get(id); + } + +} diff --git a/src/main/java/org/minidns/constants/DnssecConstants.java b/src/main/java/org/minidns/constants/DnssecConstants.java new file mode 100644 index 000000000..27a2eeb1f --- /dev/null +++ b/src/main/java/org/minidns/constants/DnssecConstants.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.constants; + +import java.util.HashMap; +import java.util.Map; + +public final class DnssecConstants { + /** + * Do not allow to instantiate DNSSECConstants + */ + private DnssecConstants() { + } + + private static final Map SIGNATURE_ALGORITHM_LUT = new HashMap<>(); + + /** + * DNSSEC Signature Algorithms. + * + * @see
+ * IANA DNSSEC Algorithm Numbers + */ + public enum SignatureAlgorithm { + @Deprecated + RSAMD5(1, "RSA/MD5"), + DH(2, "Diffie-Hellman"), + DSA(3, "DSA/SHA1"), + RSASHA1(5, "RSA/SHA-1"), + DSA_NSEC3_SHA1(6, "DSA_NSEC3-SHA1"), + RSASHA1_NSEC3_SHA1(7, "RSASHA1-NSEC3-SHA1"), + RSASHA256(8, "RSA/SHA-256"), + RSASHA512(10, "RSA/SHA-512"), + ECC_GOST(12, "GOST R 34.10-2001"), + ECDSAP256SHA256(13, "ECDSA Curve P-256 with SHA-256"), + ECDSAP384SHA384(14, "ECDSA Curve P-384 with SHA-384"), + INDIRECT(252, "Reserved for Indirect Keys"), + PRIVATEDNS(253, "private algorithm"), + PRIVATEOID(254, "private algorithm oid"), + ; + + SignatureAlgorithm(int number, String description) { + if (number < 0 || number > 255) { + throw new IllegalArgumentException(); + } + this.number = (byte) number; + this.description = description; + SIGNATURE_ALGORITHM_LUT.put(this.number, this); + } + + public final byte number; + public final String description; + + public static SignatureAlgorithm forByte(byte b) { + return SIGNATURE_ALGORITHM_LUT.get(b); + } + } + + private static final Map DELEGATION_DIGEST_LUT = new HashMap<>(); + + /** + * DNSSEC Digest Algorithms. + * + * @see + * IANA Delegation Signer (DS) Resource Record (RR) + */ + public enum DigestAlgorithm { + SHA1(1, "SHA-1"), + SHA256(2, "SHA-256"), + GOST(3, "GOST R 34.11-94"), + SHA384(4, "SHA-384"), + ; + + DigestAlgorithm(int value, String description) { + if (value < 0 || value > 255) { + throw new IllegalArgumentException(); + } + this.value = (byte) value; + this.description = description; + DELEGATION_DIGEST_LUT.put(this.value, this); + } + + public final byte value; + public final String description; + + public static DigestAlgorithm forByte(byte b) { + return DELEGATION_DIGEST_LUT.get(b); + } + } +} diff --git a/src/main/java/org/minidns/dane/DaneCertificateException.java b/src/main/java/org/minidns/dane/DaneCertificateException.java new file mode 100644 index 000000000..a7d71f9c2 --- /dev/null +++ b/src/main/java/org/minidns/dane/DaneCertificateException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.List; + +import org.minidns.record.TLSA; + +public abstract class DaneCertificateException extends CertificateException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + protected DaneCertificateException() { + } + + protected DaneCertificateException(String message) { + super(message); + } + + public static class CertificateMismatch extends DaneCertificateException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final TLSA tlsa; + public final byte[] computed; + + public CertificateMismatch(TLSA tlsa, byte[] computed) { + super("The TLSA RR does not match the certificate"); + this.tlsa = tlsa; + this.computed = computed; + } + } + + public static class MultipleCertificateMismatchExceptions extends DaneCertificateException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final List certificateMismatchExceptions; + + public MultipleCertificateMismatchExceptions(List certificateMismatchExceptions) { + super("There where multiple CertificateMismatch exceptions because none of the TLSA RR does match the certificate"); + assert !certificateMismatchExceptions.isEmpty(); + this.certificateMismatchExceptions = Collections.unmodifiableList(certificateMismatchExceptions); + } + } +} diff --git a/src/main/java/org/minidns/dane/DaneVerifier.java b/src/main/java/org/minidns/dane/DaneVerifier.java new file mode 100644 index 000000000..ed519db56 --- /dev/null +++ b/src/main/java/org/minidns/dane/DaneVerifier.java @@ -0,0 +1,272 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.record.Data; +import org.minidns.record.Record; +import org.minidns.record.TLSA; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * A helper class to validate the usage of TLSA records. + */ +public class DaneVerifier { + private static final Logger LOGGER = Logger.getLogger(DaneVerifier.class.getName()); + + private final DnssecClient client; + + public DaneVerifier() { + this(new DnssecClient()); + } + + public DaneVerifier(DnssecClient client) { + this.client = client; + } + + /** + * Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected. + * + * @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE. + * @return Whether the DANE verification is the only requirement according to the TLSA record. + * If this method returns {@code false}, additional PKIX validation is required. + * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. + */ + public boolean verify(SSLSocket socket) throws CertificateException { + if (!socket.isConnected()) { + throw new IllegalStateException("Socket not yet connected."); + } + return verify(socket.getSession()); + } + + /** + * Verifies the certificate chain in an active {@link SSLSession}. + * + * @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE. + * @return Whether the DANE verification is the only requirement according to the TLSA record. + * If this method returns {@code false}, additional PKIX validation is required. + * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. + */ + public boolean verify(SSLSession session) throws CertificateException { + try { + return verifyCertificateChain(convert(session.getPeerCertificates()), session.getPeerHost(), session.getPeerPort()); + } catch (SSLPeerUnverifiedException e) { + throw new CertificateException("Peer not verified", e); + } + } + + /** + * Verifies a certificate chain to be valid when used with the given connection details using DANE. + * + * @param chain A certificate chain that should be verified using DANE. + * @param hostName The DNS name of the host this certificate chain belongs to. + * @param port The port number that was used to reach the server providing the certificate chain in question. + * @return Whether the DANE verification is the only requirement according to the TLSA record. + * If this method returns {@code false}, additional PKIX validation is required. + * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. + */ + public boolean verifyCertificateChain(X509Certificate[] chain, String hostName, int port) throws CertificateException { + DnsName req = DnsName.from("_" + port + "._tcp." + hostName); + DnssecQueryResult result; + try { + result = client.queryDnssec(req, Record.TYPE.TLSA); + } catch (IOException e) { + throw new RuntimeException(e); + } + DnsMessage res = result.dnsQueryResult.response; + // TODO: We previously used the AD bit here. This allowed non-DNSSEC aware clients to be plugged into + // DaneVerifier, which, in turn, allows to use a trusted forward as DNSSEC validator. Is this a good idea? + if (!result.isAuthenticData()) { + String msg = "Got TLSA response from DNS server, but was not signed properly."; + msg += " Reasons:"; + for (DnssecUnverifiedReason reason : result.getUnverifiedReasons()) { + msg += " " + reason; + } + LOGGER.info(msg); + return false; + } + + List certificateMismatchExceptions = new ArrayList<>(); + boolean verified = false; + for (Record record : res.answerSection) { + if (record.type == Record.TYPE.TLSA && record.name.equals(req)) { + TLSA tlsa = (TLSA) record.payloadData; + try { + verified |= checkCertificateMatches(chain[0], tlsa, hostName); + } catch (DaneCertificateException.CertificateMismatch certificateMismatchException) { + // Record the mismatch and only throw an exception if no + // TLSA RR is able to verify the cert. This allows for TLSA + // certificate rollover. + certificateMismatchExceptions.add(certificateMismatchException); + } + if (verified) break; + } + } + + if (!verified && !certificateMismatchExceptions.isEmpty()) { + throw new DaneCertificateException.MultipleCertificateMismatchExceptions(certificateMismatchExceptions); + } + + return verified; + } + + private static boolean checkCertificateMatches(X509Certificate cert, TLSA tlsa, String hostName) throws CertificateException { + if (tlsa.certUsage == null) { + LOGGER.warning("TLSA certificate usage byte " + tlsa.certUsageByte + " is not supported while verifying " + hostName); + return false; + } + + switch (tlsa.certUsage) { + case serviceCertificateConstraint: // PKIX-EE + case domainIssuedCertificate: // DANE-EE + break; + case caConstraint: // PKIX-TA + case trustAnchorAssertion: // DANE-TA + default: + LOGGER.warning("TLSA certificate usage " + tlsa.certUsage + " (" + tlsa.certUsageByte + ") not supported while verifying " + hostName); + return false; + } + + if (tlsa.selector == null) { + LOGGER.warning("TLSA selector byte " + tlsa.selectorByte + " is not supported while verifying " + hostName); + return false; + } + + byte[] comp = null; + switch (tlsa.selector) { + case fullCertificate: + comp = cert.getEncoded(); + break; + case subjectPublicKeyInfo: + comp = cert.getPublicKey().getEncoded(); + break; + default: + LOGGER.warning("TLSA selector " + tlsa.selector + " (" + tlsa.selectorByte + ") not supported while verifying " + hostName); + return false; + } + + if (tlsa.matchingType == null) { + LOGGER.warning("TLSA matching type byte " + tlsa.matchingTypeByte + " is not supported while verifying " + hostName); + return false; + } + + switch (tlsa.matchingType) { + case noHash: + break; + case sha256: + try { + comp = MessageDigest.getInstance("SHA-256").digest(comp); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException("Verification using TLSA failed: could not SHA-256 for matching", e); + } + break; + case sha512: + try { + comp = MessageDigest.getInstance("SHA-512").digest(comp); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException("Verification using TLSA failed: could not SHA-512 for matching", e); + } + break; + default: + LOGGER.warning("TLSA matching type " + tlsa.matchingType + " not supported while verifying " + hostName); + return false; + } + + boolean matches = tlsa.certificateAssociationEquals(comp); + if (!matches) { + throw new DaneCertificateException.CertificateMismatch(tlsa, comp); + } + + // domain issued certificate does not require further verification, + // service certificate constraint does. + return tlsa.certUsage == TLSA.CertUsage.domainIssuedCertificate; + } + + /** + * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. + * This method must be called before {@link HttpsURLConnection#connect()} is invoked. + * + * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. You can use + * {@link #verifiedConnect(HttpsURLConnection, X509TrustManager)} to inject a custom {@link TrustManager}. + * + * @param conn connection to be connected. + * @return The {@link HttpsURLConnection} after being connected. + * @throws IOException when the connection could not be established. + * @throws CertificateException if there was an exception while verifying the certificate. + */ + public HttpsURLConnection verifiedConnect(HttpsURLConnection conn) throws IOException, CertificateException { + return verifiedConnect(conn, null); + } + + /** + * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. + * This method must be called before {@link HttpsURLConnection#connect()} is invoked. + * + * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. + * + * @param conn connection to be connected. + * @param trustManager A non-default {@link TrustManager} to be used. + * @return The {@link HttpsURLConnection} after being connected. + * @throws IOException when the connection could not be established. + * @throws CertificateException if there was an exception while verifying the certificate. + */ + public HttpsURLConnection verifiedConnect(HttpsURLConnection conn, X509TrustManager trustManager) throws IOException, CertificateException { + try { + SSLContext context = SSLContext.getInstance("TLS"); + ExpectingTrustManager expectingTrustManager = new ExpectingTrustManager(trustManager); + context.init(null, new TrustManager[] {expectingTrustManager}, null); + conn.setSSLSocketFactory(context.getSocketFactory()); + conn.connect(); + boolean fullyVerified = verifyCertificateChain(convert(conn.getServerCertificates()), conn.getURL().getHost(), + conn.getURL().getPort() < 0 ? conn.getURL().getDefaultPort() : conn.getURL().getPort()); + // If fullyVerified is true then it's the DANE verification performed by verifiyCertificateChain() is + // sufficient to verify the certificate and we ignore possible pending exceptions of ExpectingTrustManager. + if (!fullyVerified && expectingTrustManager.hasException()) { + throw new IOException("Peer verification failed using PKIX", expectingTrustManager.getException()); + } + return conn; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + private static X509Certificate[] convert(Certificate[] certificates) { + List certs = new ArrayList<>(); + for (Certificate certificate : certificates) { + if (certificate instanceof X509Certificate) { + certs.add((X509Certificate) certificate); + } + } + return certs.toArray(new X509Certificate[certs.size()]); + } +} diff --git a/src/main/java/org/minidns/dane/ExpectingTrustManager.java b/src/main/java/org/minidns/dane/ExpectingTrustManager.java new file mode 100644 index 000000000..63c097f39 --- /dev/null +++ b/src/main/java/org/minidns/dane/ExpectingTrustManager.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class ExpectingTrustManager implements X509TrustManager { + private CertificateException exception; + private final X509TrustManager trustManager; + + /** + * Creates a new instance of ExpectingTrustManager. + * + * @param trustManager The {@link X509TrustManager} to be used for verification. + * {@code null} to use the system default. + */ + public ExpectingTrustManager(X509TrustManager trustManager) { + this.trustManager = trustManager == null ? X509TrustManagerUtil.getDefault() : trustManager; + } + + public boolean hasException() { + return exception != null; + } + + public CertificateException getException() { + CertificateException e = exception; + exception = null; + return e; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + trustManager.checkClientTrusted(chain, authType); + } catch (CertificateException e) { + exception = e; + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + trustManager.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + exception = e; + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManager.getAcceptedIssuers(); + } +} diff --git a/src/main/java/org/minidns/dane/X509TrustManagerUtil.java b/src/main/java/org/minidns/dane/X509TrustManagerUtil.java new file mode 100644 index 000000000..0e550ca34 --- /dev/null +++ b/src/main/java/org/minidns/dane/X509TrustManagerUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +public class X509TrustManagerUtil { + + public static X509TrustManager getDefault() { + return getDefault(null); + } + + public static X509TrustManager getDefault(KeyStore keyStore) { + String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory trustManagerFactory; + try { + trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm); + trustManagerFactory.init(keyStore); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + throw new AssertionError(e); + } + + for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + throw new AssertionError("No trust manager for the default algorithm " + defaultAlgorithm + " found"); + } +} diff --git a/src/main/java/org/minidns/dane/java7/DaneExtendedTrustManager.java b/src/main/java/org/minidns/dane/java7/DaneExtendedTrustManager.java new file mode 100644 index 000000000..8cab3ad0d --- /dev/null +++ b/src/main/java/org/minidns/dane/java7/DaneExtendedTrustManager.java @@ -0,0 +1,172 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane.java7; + +import org.minidns.dane.DaneVerifier; +import org.minidns.dane.X509TrustManagerUtil; +import org.minidns.dnssec.DnssecClient; +import org.minidns.util.InetAddressUtil; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; + +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.logging.Logger; + +public class DaneExtendedTrustManager extends X509ExtendedTrustManager { + private static final Logger LOGGER = Logger.getLogger(DaneExtendedTrustManager.class.getName()); + + private final X509TrustManager base; + private final DaneVerifier verifier; + + public static void inject() { + inject(new DaneExtendedTrustManager()); + } + + public static void inject(DaneExtendedTrustManager trustManager) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] {trustManager}, null); + SSLContext.setDefault(sslContext); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + public DaneExtendedTrustManager() { + this(X509TrustManagerUtil.getDefault()); + } + + public DaneExtendedTrustManager(DnssecClient client) { + this(client, X509TrustManagerUtil.getDefault()); + } + + public DaneExtendedTrustManager(X509TrustManager base) { + this(new DaneVerifier(), base); + } + + public DaneExtendedTrustManager(DnssecClient client, X509TrustManager base) { + this(new DaneVerifier(client), base); + } + + public DaneExtendedTrustManager(DaneVerifier verifier, X509TrustManager base) { + this.verifier = verifier; + this.base = base; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + if (base == null) { + LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified"); + return; + } + + LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check forwarding request to fallback X509TrustManage"); + if (base instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) base).checkClientTrusted(chain, authType, socket); + } else { + base.checkClientTrusted(chain, authType); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + boolean verificationSuccessful = false; + + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + final String hostname = sslSocket.getHandshakeSession().getPeerHost(); + + if (hostname == null) { + LOGGER.warning("Hostname returned by sslSocket.getHandshakeSession().getPeerHost() is null"); + } else if (InetAddressUtil.isIpAddress(hostname)) { + LOGGER.warning( + "Hostname returned by sslSocket.getHandshakeSession().getPeerHost() '" + hostname + + "' is an IP address"); + } else { + final int port = socket.getPort(); + verificationSuccessful = verifier.verifyCertificateChain(chain, hostname, port); + } + } else { + throw new IllegalStateException("The provided socket '" + socket + "' is not of type SSLSocket"); + } + + if (verificationSuccessful) { + // Verification successful, no need to delegate to base trust manager. + return; + } + + if (base instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) base).checkServerTrusted(chain, authType, socket); + } else { + base.checkServerTrusted(chain, authType); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + if (base == null) { + LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified"); + return; + } + + LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check, forwarding request to fallback X509TrustManage"); + if (base instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) base).checkClientTrusted(chain, authType, engine); + } else { + base.checkClientTrusted(chain, authType); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + if (verifier.verifyCertificateChain(chain, engine.getPeerHost(), engine.getPeerPort())) { + // Verification successful, no need to delegate to base trust manager. + return; + } + + if (base instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) base).checkServerTrusted(chain, authType, engine); + } else { + base.checkServerTrusted(chain, authType); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (base == null) { + LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified"); + return; + } + + LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check, forwarding request to fallback X509TrustManage"); + base.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + LOGGER.info("DaneExtendedTrustManager cannot be used without hostname information, forwarding request to fallback X509TrustManage"); + base.checkServerTrusted(chain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return base.getAcceptedIssuers(); + } +} diff --git a/src/main/java/org/minidns/dnslabel/ALabel.java b/src/main/java/org/minidns/dnslabel/ALabel.java new file mode 100644 index 000000000..9b541fef7 --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/ALabel.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +import org.minidns.idna.MiniDnsIdna; + +public final class ALabel extends XnLabel { + + ALabel(String label) { + super(label); + } + + @Override + protected String getInternationalizedRepresentationInternal() { + return MiniDnsIdna.toUnicode(label); + } +} diff --git a/src/main/java/org/minidns/dnslabel/DnsLabel.java b/src/main/java/org/minidns/dnslabel/DnsLabel.java new file mode 100644 index 000000000..cba1f3eb6 --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/DnsLabel.java @@ -0,0 +1,280 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import org.minidns.util.SafeCharSequence; + +/** + * A DNS label is an individual component of a DNS name. Labels are usually shown separated by dots. + *

+ * This class implements {@link Comparable} which compares DNS labels according to the Canonical DNS Name Order as + * specified in RFC 4034 § 6.1. + *

+ *

+ * Note that as per RFC 2181 § 11 DNS labels may contain + * any byte. + *

+ * + * @see RFC 5890 § 2.2. DNS-Related Terminology + * @author Florian Schmaus + * + */ +public abstract class DnsLabel extends SafeCharSequence implements Comparable { + + /** + * The maximum length of a DNS label in octets. + * + * @see RFC 1035 § 2.3.4. + */ + public static final int MAX_LABEL_LENGTH_IN_OCTETS = 63; + + public static final DnsLabel WILDCARD_LABEL = DnsLabel.from("*"); + + /** + * Whether or not the DNS label is validated on construction. + */ + public static boolean VALIDATE = true; + + public final String label; + + protected DnsLabel(String label) { + this.label = label; + + if (!VALIDATE) { + return; + } + + setBytesIfRequired(); + if (byteCache.length > MAX_LABEL_LENGTH_IN_OCTETS) { + throw new LabelToLongException(label); + } + } + + private transient String internationalizedRepresentation; + + public final String getInternationalizedRepresentation() { + if (internationalizedRepresentation == null) { + internationalizedRepresentation = getInternationalizedRepresentationInternal(); + } + return internationalizedRepresentation; + } + + protected String getInternationalizedRepresentationInternal() { + return label; + } + + public final String getLabelType() { + return getClass().getSimpleName(); + } + + private transient String safeToStringRepresentation; + + @Override + public final String toString() { + if (safeToStringRepresentation == null) { + safeToStringRepresentation = toSafeRepesentation(label); + } + + return safeToStringRepresentation; + } + + /** + * Get the raw label. Note that this may return a String containing null bytes. + * Those Strings are notoriously difficult to handle from a security + * perspective. Therefore it is recommended to use {@link #toString()} instead, + * which will return a sanitized String. + * + * @return the raw label. + * @since 1.1.0 + */ + public final String getRawLabel() { + return label; + } + + @Override + public final boolean equals(Object other) { + if (!(other instanceof DnsLabel)) { + return false; + } + DnsLabel otherDnsLabel = (DnsLabel) other; + return label.equals(otherDnsLabel.label); + } + + @Override + public final int hashCode() { + return label.hashCode(); + } + + private transient DnsLabel lowercasedVariant; + + public final DnsLabel asLowercaseVariant() { + if (lowercasedVariant == null) { + String lowercaseLabel = label.toLowerCase(Locale.US); + lowercasedVariant = DnsLabel.from(lowercaseLabel); + } + return lowercasedVariant; + } + + private transient byte[] byteCache; + + private void setBytesIfRequired() { + if (byteCache == null) { + byteCache = label.getBytes(StandardCharsets.US_ASCII); + } + } + + public final void writeToBoas(ByteArrayOutputStream byteArrayOutputStream) { + setBytesIfRequired(); + + byteArrayOutputStream.write(byteCache.length); + byteArrayOutputStream.write(byteCache, 0, byteCache.length); + } + + @Override + public final int compareTo(DnsLabel other) { + String myCanonical = asLowercaseVariant().label; + String otherCanonical = other.asLowercaseVariant().label; + + return myCanonical.compareTo(otherCanonical); + } + + public static DnsLabel from(String label) { + if (label == null || label.isEmpty()) { + throw new IllegalArgumentException("Label is null or empty"); + } + + if (LdhLabel.isLdhLabel(label)) { + return LdhLabel.fromInternal(label); + } + + return NonLdhLabel.fromInternal(label); + } + + public static DnsLabel[] from(String[] labels) { + DnsLabel[] res = new DnsLabel[labels.length]; + + for (int i = 0; i < labels.length; i++) { + res[i] = DnsLabel.from(labels[i]); + } + + return res; + } + + public static boolean isIdnAcePrefixed(String string) { + return string.toLowerCase(Locale.US).startsWith("xn--"); + } + + public static String toSafeRepesentation(String dnsLabel) { + if (consistsOnlyOfLettersDigitsHypenAndUnderscore(dnsLabel)) { + // This label is safe, nothing to do. + return dnsLabel; + } + + StringBuilder sb = new StringBuilder(2 * dnsLabel.length()); + for (int i = 0; i < dnsLabel.length(); i++) { + char c = dnsLabel.charAt(i); + if (isLdhOrMaybeUnderscore(c, true)) { + sb.append(c); + continue; + } + + + // Let's see if we found and unsafe char we want to replace. + switch (c) { + case '.': + sb.append('●'); // U+25CF BLACK CIRCLE; + break; + case '\\': + sb.append('⧷'); // U+29F7 REVERSE SOLIDUS WITH HORIZONTAL STROKE + break; + case '\u007f': + // Convert DEL to U+2421 SYMBOL FOR DELETE + sb.append('␡'); + break; + case ' ': + sb.append('␣'); // U+2423 OPEN BOX + break; + default: + if (c < 32) { + // First convert the ASCI control codes to the Unicode Control Pictures + int substituteAsInt = c + '\u2400'; + assert substituteAsInt <= Character.MAX_CODE_POINT; + char substitute = (char) substituteAsInt; + sb.append(substitute); + } else if (c < 127) { + // Everything smaller than 127 is now safe to directly append. + sb.append(c); + } else if (c > 255) { + throw new IllegalArgumentException("The string '" + dnsLabel + + "' contains characters outside the 8-bit range: " + c + " at position " + i); + } else { + // Everything that did not match the previous conditions is explicitly escaped. + sb.append("〚"); // U+301A + // Transform the char to hex notation. Note that we have ensure that c is <= 255 + // here, hence only two hexadecimal places are ok. + String hex = String.format("%02X", (int) c); + sb.append(hex); + sb.append("〛"); // U+301B + } + } + } + + return sb.toString(); + } + + private static boolean isLdhOrMaybeUnderscore(char c, boolean underscore) { + // CHECKSTYLE:OFF + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '-' + || (underscore && c == '_') + ; + // CHECKSTYLE:ON + } + + private static boolean consistsOnlyOfLdhAndMaybeUnderscore(String string, boolean underscore) { + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (isLdhOrMaybeUnderscore(c, underscore)) { + continue; + } + return false; + } + return true; + } + + public static boolean consistsOnlyOfLettersDigitsAndHypen(String string) { + return consistsOnlyOfLdhAndMaybeUnderscore(string, false); + } + + public static boolean consistsOnlyOfLettersDigitsHypenAndUnderscore(String string) { + return consistsOnlyOfLdhAndMaybeUnderscore(string, true); + } + + public static class LabelToLongException extends IllegalArgumentException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final String label; + + LabelToLongException(String label) { + this.label = label; + } + } +} diff --git a/src/main/java/org/minidns/dnslabel/FakeALabel.java b/src/main/java/org/minidns/dnslabel/FakeALabel.java new file mode 100644 index 000000000..bd4c791df --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/FakeALabel.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +public final class FakeALabel extends XnLabel { + + FakeALabel(String label) { + super(label); + } + +} diff --git a/src/main/java/org/minidns/dnslabel/LdhLabel.java b/src/main/java/org/minidns/dnslabel/LdhLabel.java new file mode 100644 index 000000000..32b024b4a --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/LdhLabel.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A LDH (Letters, Digits, Hyphen) label, which is the + * classical label form. + *

+ * Note that it is a common misconception that LDH labels can not start with a + * digit. The origin of this misconception is likely that + * RFC 1034 + * § 3.5 specified + *

+ *
+ * They [i.e, DNS labels] must start with a letter, end with a letter or digit, + * and have as interior characters only letters, digits, and hyphen. + *
. + * However, this was relaxed in + * RFC 1123 § + * 2.1 + *
+ * One aspect of host name syntax is hereby changed: the restriction on the first + * character is relaxed to allow either a letter or a digit. + *
+ * and later summarized in + * RFC 3696 § + * 2: + *
+ * If the hyphen is used, it is not permitted to appear at either the beginning + * or end of a label. + *
+ * Furthermore + * RFC + * 5890 § 2.3.1 only mentions the requirement that hyphen must not be the + * first or last character of a LDH label. + * + * @see RFC 5890 § + * 2.3.1. LDH Label + * + */ +public abstract class LdhLabel extends DnsLabel { + + protected LdhLabel(String label) { + super(label); + } + + public static boolean isLdhLabel(String label) { + if (label.isEmpty()) { + return false; + } + + if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) { + return false; + } + + return consistsOnlyOfLettersDigitsAndHypen(label); + } + + protected static LdhLabel fromInternal(String label) { + assert isLdhLabel(label); + + if (ReservedLdhLabel.isReservedLdhLabel(label)) { + // Label starts with '??--'. Now let us see if it is a XN-Label, starting with 'xn--', but be aware that the + // 'xn' part is case insensitive. The XnLabel.isXnLabelInternal(String) method takes care of this. + if (XnLabel.isXnLabelInternal(label)) { + return XnLabel.fromInternal(label); + } else { + return new ReservedLdhLabel(label); + } + } + return new NonReservedLdhLabel(label); + } +} diff --git a/src/main/java/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java b/src/main/java/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java new file mode 100644 index 000000000..c48df9f5b --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A DNS label with a leading or trailing hyphen ('-'). + */ +public final class LeadingOrTrailingHyphenLabel extends NonLdhLabel { + + LeadingOrTrailingHyphenLabel(String label) { + super(label); + } + + static boolean isLeadingOrTrailingHypenLabelInternal(String label) { + if (label.isEmpty()) { + return false; + } + + if (label.charAt(0) == '-') { + return true; + } + + if (label.charAt(label.length() - 1) == '-') { + return true; + } + + return false; + } +} diff --git a/src/main/java/org/minidns/dnslabel/NonLdhLabel.java b/src/main/java/org/minidns/dnslabel/NonLdhLabel.java new file mode 100644 index 000000000..6db6dc23f --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/NonLdhLabel.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A DNS label which contains more than just letters, digits and a hyphen. + * + */ +public abstract class NonLdhLabel extends DnsLabel { + + protected NonLdhLabel(String label) { + super(label); + } + + protected static DnsLabel fromInternal(String label) { + if (UnderscoreLabel.isUnderscoreLabelInternal(label)) { + return new UnderscoreLabel(label); + } + + if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) { + return new LeadingOrTrailingHyphenLabel(label); + } + + return new OtherNonLdhLabel(label); + } + +} diff --git a/src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java b/src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java new file mode 100644 index 000000000..329ce6e8f --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A Non-Reserved LDH label (NR-LDH label), which do not have "--" in the third and fourth position. + * + */ +public final class NonReservedLdhLabel extends LdhLabel { + + NonReservedLdhLabel(String label) { + super(label); + assert isNonReservedLdhLabelInternal(label); + } + + public static boolean isNonReservedLdhLabel(String label) { + if (!isLdhLabel(label)) { + return false; + } + return isNonReservedLdhLabelInternal(label); + } + + static boolean isNonReservedLdhLabelInternal(String label) { + return !ReservedLdhLabel.isReservedLdhLabelInternal(label); + } +} diff --git a/src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java b/src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java new file mode 100644 index 000000000..13bbd2c47 --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java @@ -0,0 +1,23 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A Non-LDH label which does not begin with an underscore ('_'), hyphen ('-') or ends with an hyphen. + * + */ +public final class OtherNonLdhLabel extends NonLdhLabel { + + OtherNonLdhLabel(String label) { + super(label); + } + +} diff --git a/src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java b/src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java new file mode 100644 index 000000000..38e92b565 --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A reserved LDH label (R-LDH label), which have the property that they contain "--" in the third and fourth characters. + * + */ +public class ReservedLdhLabel extends LdhLabel { + + protected ReservedLdhLabel(String label) { + super(label); + assert isReservedLdhLabelInternal(label); + } + + public static boolean isReservedLdhLabel(String label) { + if (!isLdhLabel(label)) { + return false; + } + return isReservedLdhLabelInternal(label); + } + + static boolean isReservedLdhLabelInternal(String label) { + return label.length() >= 4 + && label.charAt(2) == '-' + && label.charAt(3) == '-'; + } +} diff --git a/src/main/java/org/minidns/dnslabel/UnderscoreLabel.java b/src/main/java/org/minidns/dnslabel/UnderscoreLabel.java new file mode 100644 index 000000000..e75ff8f9e --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/UnderscoreLabel.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +/** + * A DNS label which begins with an underscore ('_'). + * + */ +public final class UnderscoreLabel extends NonLdhLabel { + + UnderscoreLabel(String label) { + super(label); + } + + static boolean isUnderscoreLabelInternal(String label) { + return label.charAt(0) == '_'; + } +} diff --git a/src/main/java/org/minidns/dnslabel/XnLabel.java b/src/main/java/org/minidns/dnslabel/XnLabel.java new file mode 100644 index 000000000..bb323d5ca --- /dev/null +++ b/src/main/java/org/minidns/dnslabel/XnLabel.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnslabel; + +import java.util.Locale; + +import org.minidns.idna.MiniDnsIdna; + +/** + * A label that begins with "xn--" and follows the LDH rule. + */ +public abstract class XnLabel extends ReservedLdhLabel { + + protected XnLabel(String label) { + super(label); + } + + protected static LdhLabel fromInternal(String label) { + assert isIdnAcePrefixed(label); + + String uLabel = MiniDnsIdna.toUnicode(label); + if (label.equals(uLabel)) { + // No Punycode conversation to Unicode was performed, this is a fake A-label! + return new FakeALabel(label); + } else { + return new ALabel(label); + } + } + + public static boolean isXnLabel(String label) { + if (!isReservedLdhLabel(label)) { + return false; + } + return isXnLabelInternal(label); + } + + static boolean isXnLabelInternal(String label) { + // Note that we already ensure the minimum label length here, since reserved LDH + // labels must start with "xn--". + return label.substring(0, 2).toLowerCase(Locale.US).equals("xn"); + } +} diff --git a/src/main/java/org/minidns/dnsmessage/DnsMessage.java b/src/main/java/org/minidns/dnsmessage/DnsMessage.java new file mode 100644 index 000000000..d584eab01 --- /dev/null +++ b/src/main/java/org/minidns/dnsmessage/DnsMessage.java @@ -0,0 +1,1281 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsmessage; + +import org.minidns.edns.Edns; +import org.minidns.record.Data; +import org.minidns.record.OPT; +import org.minidns.record.Record; +import org.minidns.record.Record.TYPE; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A DNS message as defined by RFC 1035. The message consists of a header and + * 4 sections: question, answer, nameserver and addition resource record + * section. + * A message can either be parsed ({@link #DnsMessage(byte[])}) or serialized + * ({@link DnsMessage#toArray()}). + * + * @see RFC 1035 + */ +public class DnsMessage { + + private static final Logger LOGGER = Logger.getLogger(DnsMessage.class.getName()); + + /** + * Possible DNS response codes. + * + * @see + * IANA Domain Name System (DNS) Paramters - DNS RCODEs + * @see RFC 6895 § 2.3 + */ + public enum RESPONSE_CODE { + NO_ERROR(0), + FORMAT_ERR(1), + SERVER_FAIL(2), + NX_DOMAIN(3), + NO_IMP(4), + REFUSED(5), + YXDOMAIN(6), + YXRRSET(7), + NXRRSET(8), + NOT_AUTH(9), + NOT_ZONE(10), + BADVERS_BADSIG(16), + BADKEY(17), + BADTIME(18), + BADMODE(19), + BADNAME(20), + BADALG(21), + BADTRUNC(22), + BADCOOKIE(23), + ; + + /** + * Reverse lookup table for response codes. + */ + private static final Map INVERSE_LUT = new HashMap<>(RESPONSE_CODE.values().length); + + static { + for (RESPONSE_CODE responseCode : RESPONSE_CODE.values()) { + INVERSE_LUT.put((int) responseCode.value, responseCode); + } + } + + /** + * The response code value. + */ + private final byte value; + + /** + * Create a new response code. + * + * @param value The response code value. + */ + RESPONSE_CODE(int value) { + this.value = (byte) value; + } + + /** + * Retrieve the byte value of the response code. + * + * @return the response code. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the response code for a byte value. + * + * @param value The byte value. + * @return The symbolic response code or null. + * @throws IllegalArgumentException if the value is not in the range of 0..15. + */ + public static RESPONSE_CODE getResponseCode(int value) throws IllegalArgumentException { + if (value < 0 || value > 65535) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT.get(value); + } + + } + + /** + * Symbolic DNS Opcode values. + * + * @see + * IANA Domain Name System (DNS) Paramters - DNS OpCodes + */ + public enum OPCODE { + QUERY, + INVERSE_QUERY, + STATUS, + UNASSIGNED3, + NOTIFY, + UPDATE, + ; + + /** + * Lookup table for for opcode resolution. + */ + private static final OPCODE[] INVERSE_LUT = new OPCODE[OPCODE.values().length]; + + static { + for (OPCODE opcode : OPCODE.values()) { + if (INVERSE_LUT[opcode.getValue()] != null) { + throw new IllegalStateException(); + } + INVERSE_LUT[opcode.getValue()] = opcode; + } + } + + /** + * The value of this opcode. + */ + private final byte value; + + /** + * Create a new opcode for a given byte value. + * + */ + @SuppressWarnings("EnumOrdinal") + OPCODE() { + this.value = (byte) this.ordinal(); + } + + /** + * Retrieve the byte value of this opcode. + * + * @return The byte value of this opcode. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the symbolic name of an opcode byte. + * + * @param value The byte value of the opcode. + * @return The symbolic opcode or null. + * @throws IllegalArgumentException If the byte value is not in the + * range 0..15. + */ + public static OPCODE getOpcode(int value) throws IllegalArgumentException { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + if (value >= INVERSE_LUT.length) { + return null; + } + return INVERSE_LUT[value]; + } + + } + + /** + * The DNS message id. + */ + public final int id; + + /** + * The DNS message opcode. + */ + public final OPCODE opcode; + + /** + * The response code of this dns message. + */ + public final RESPONSE_CODE responseCode; + + /** + * The QR flag of the DNS message header. Note that this will be true if the message is a + * response and false if it is a query. + * + * @see RFC 1035 § 4.1.1 + */ + public final boolean qr; + + /** + * True if this is a authorative response. If set, the responding nameserver is an authority for the domain name in + * the question section. Note that the answer section may have multiple owner names because of aliases. This flag + * corresponds to the name which matches the query name, or the first owner name in the query section. + * + * @see RFC 1035 § 4.1.1. Header section format + */ + public final boolean authoritativeAnswer; + + /** + * True if message is truncated. Then TCP should be used. + */ + public final boolean truncated; + + /** + * True if the server should recurse. + */ + public final boolean recursionDesired; + + /** + * True if recursion is possible. + */ + public final boolean recursionAvailable; + + /** + * True if the server regarded the response as authentic. + */ + public final boolean authenticData; + + /** + * True if the server should not perform DNSSEC validation before returning the result. + */ + public final boolean checkingDisabled; + + /** + * The question section content. Usually there will be only one question. + *

+ * This list is unmodifiable. + *

+ */ + public final List questions; + + /** + * The answers section records. Note that it is not guaranteed that all records found in this section will be direct + * answers to the question in the query. If DNSSEC is used, then this section also contains the RRSIG record. + *

+ * This list is unmodifiable. + *

+ */ + public final List> answerSection; + + /** + * The Authority Section. Note that it is not guaranteed that this section only contains nameserver records. If DNSSEC is used, then this section could also contain a NSEC(3) record. + *

+ * This list is unmodifiable. + *

+ */ + public final List> authoritySection; + + /** + * The additional section. It eventually contains RRs which relate to the query. + *

+ * This list is unmodifiable. + *

+ */ + public final List> additionalSection; + + public final int optRrPosition; + + /** + * The optional but very common EDNS information. Note that this field is lazily populated. + * + */ + private Edns edns; + + /** + * The receive timestamp. Set only if this message was created via parse. + * This should be used to evaluate TTLs. + */ + public final long receiveTimestamp; + + protected DnsMessage(Builder builder) { + this.id = builder.id; + this.opcode = builder.opcode; + this.responseCode = builder.responseCode; + this.receiveTimestamp = builder.receiveTimestamp; + this.qr = builder.query; + this.authoritativeAnswer = builder.authoritativeAnswer; + this.truncated = builder.truncated; + this.recursionDesired = builder.recursionDesired; + this.recursionAvailable = builder.recursionAvailable; + this.authenticData = builder.authenticData; + this.checkingDisabled = builder.checkingDisabled; + + if (builder.questions == null) { + this.questions = Collections.emptyList(); + } else { + List q = new ArrayList<>(builder.questions.size()); + q.addAll(builder.questions); + this.questions = Collections.unmodifiableList(q); + } + + if (builder.answerSection == null) { + this.answerSection = Collections.emptyList(); + } else { + List> a = new ArrayList<>(builder.answerSection.size()); + a.addAll(builder.answerSection); + this.answerSection = Collections.unmodifiableList(a); + } + + if (builder.authoritySection == null) { + this.authoritySection = Collections.emptyList(); + } else { + List> n = new ArrayList<>(builder.authoritySection.size()); + n.addAll(builder.authoritySection); + this.authoritySection = Collections.unmodifiableList(n); + } + + if (builder.additionalSection == null && builder.ednsBuilder == null) { + this.additionalSection = Collections.emptyList(); + } else { + int size = 0; + if (builder.additionalSection != null) { + size += builder.additionalSection.size(); + } + if (builder.ednsBuilder != null) { + size++; + } + List> a = new ArrayList<>(size); + if (builder.additionalSection != null) { + a.addAll(builder.additionalSection); + } + if (builder.ednsBuilder != null) { + Edns edns = builder.ednsBuilder.build(); + this.edns = edns; + a.add(edns.asRecord()); + } + this.additionalSection = Collections.unmodifiableList(a); + } + + optRrPosition = getOptRrPosition(this.additionalSection); + + if (optRrPosition != -1) { + // Verify that there are no further OPT records but the one we already found. + for (int i = optRrPosition + 1; i < this.additionalSection.size(); i++) { + if (this.additionalSection.get(i).type == TYPE.OPT) { + throw new IllegalArgumentException("There must be only one OPT pseudo RR in the additional section"); + } + } + } + + // TODO Add verification of dns message state here + } + + /** + * Build a DNS Message based on a binary DNS message. + * + * @param data The DNS message data. + * @throws IOException On read errors. + */ + public DnsMessage(byte[] data) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + DataInputStream dis = new DataInputStream(bis); + id = dis.readUnsignedShort(); + int header = dis.readUnsignedShort(); + qr = ((header >> 15) & 1) == 1; + opcode = OPCODE.getOpcode((header >> 11) & 0xf); + authoritativeAnswer = ((header >> 10) & 1) == 1; + truncated = ((header >> 9) & 1) == 1; + recursionDesired = ((header >> 8) & 1) == 1; + recursionAvailable = ((header >> 7) & 1) == 1; + authenticData = ((header >> 5) & 1) == 1; + checkingDisabled = ((header >> 4) & 1) == 1; + responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); + receiveTimestamp = System.currentTimeMillis(); + int questionCount = dis.readUnsignedShort(); + int answerCount = dis.readUnsignedShort(); + int nameserverCount = dis.readUnsignedShort(); + int additionalResourceRecordCount = dis.readUnsignedShort(); + questions = new ArrayList<>(questionCount); + for (int i = 0; i < questionCount; i++) { + questions.add(new Question(dis, data)); + } + answerSection = new ArrayList<>(answerCount); + for (int i = 0; i < answerCount; i++) { + answerSection.add(Record.parse(dis, data)); + } + authoritySection = new ArrayList<>(nameserverCount); + for (int i = 0; i < nameserverCount; i++) { + authoritySection.add(Record.parse(dis, data)); + } + additionalSection = new ArrayList<>(additionalResourceRecordCount); + for (int i = 0; i < additionalResourceRecordCount; i++) { + additionalSection.add(Record.parse(dis, data)); + } + optRrPosition = getOptRrPosition(additionalSection); + } + + /** + * Constructs an normalized version of the given DnsMessage by setting the id to '0'. + * + * @param message the message of which normalized version should be constructed. + */ + private DnsMessage(DnsMessage message) { + id = 0; + qr = message.qr; + opcode = message.opcode; + authoritativeAnswer = message.authoritativeAnswer; + truncated = message.truncated; + recursionDesired = message.recursionDesired; + recursionAvailable = message.recursionAvailable; + authenticData = message.authenticData; + checkingDisabled = message.checkingDisabled; + responseCode = message.responseCode; + receiveTimestamp = message.receiveTimestamp; + questions = message.questions; + answerSection = message.answerSection; + authoritySection = message.authoritySection; + additionalSection = message.additionalSection; + optRrPosition = message.optRrPosition; + } + + private static int getOptRrPosition(List> additionalSection) { + int optRrPosition = -1; + for (int i = 0; i < additionalSection.size(); i++) { + Record record = additionalSection.get(i); + if (record.type == Record.TYPE.OPT) { + optRrPosition = i; + break; + } + } + return optRrPosition; + } + + /** + * Generate a binary dns packet out of this message. + * + * @return byte[] the binary representation. + */ + public byte[] toArray() { + return serialize().clone(); + } + + public DatagramPacket asDatagram(InetAddress address, int port) { + byte[] bytes = serialize(); + return new DatagramPacket(bytes, bytes.length, address, port); + } + + public void writeTo(OutputStream outputStream) throws IOException { + writeTo(outputStream, true); + } + + public void writeTo(OutputStream outputStream, boolean writeLength) throws IOException { + byte[] bytes = serialize(); + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + if (writeLength) { + dataOutputStream.writeShort(bytes.length); + } + dataOutputStream.write(bytes); + } + + public ByteBuffer getInByteBuffer() { + byte[] bytes = serialize().clone(); + return ByteBuffer.wrap(bytes); + } + + private byte[] byteCache; + + private byte[] serialize() { + if (byteCache != null) { + return byteCache; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + int header = calculateHeaderBitmap(); + try { + dos.writeShort((short) id); + dos.writeShort((short) header); + if (questions == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) questions.size()); + } + if (answerSection == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) answerSection.size()); + } + if (authoritySection == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) authoritySection.size()); + } + if (additionalSection == null) { + dos.writeShort(0); + } else { + dos.writeShort((short) additionalSection.size()); + } + if (questions != null) { + for (Question question : questions) { + dos.write(question.toByteArray()); + } + } + if (answerSection != null) { + for (Record answer : answerSection) { + dos.write(answer.toByteArray()); + } + } + if (authoritySection != null) { + for (Record nameserverRecord : authoritySection) { + dos.write(nameserverRecord.toByteArray()); + } + } + if (additionalSection != null) { + for (Record additionalResourceRecord : additionalSection) { + dos.write(additionalResourceRecord.toByteArray()); + } + } + dos.flush(); + } catch (IOException e) { + // Should never happen. + throw new AssertionError(e); + } + byteCache = baos.toByteArray(); + return byteCache; + } + + int calculateHeaderBitmap() { + int header = 0; + if (qr) { + header += 1 << 15; + } + if (opcode != null) { + header += opcode.getValue() << 11; + } + if (authoritativeAnswer) { + header += 1 << 10; + } + if (truncated) { + header += 1 << 9; + } + if (recursionDesired) { + header += 1 << 8; + } + if (recursionAvailable) { + header += 1 << 7; + } + if (authenticData) { + header += 1 << 5; + } + if (checkingDisabled) { + header += 1 << 4; + } + if (responseCode != null) { + header += responseCode.getValue(); + } + return header; + } + + public Question getQuestion() { + return questions.get(0); + } + + /** + * Copy the questions found in the question section. + * + * @return a copy of the question section questions. + * @see #questions + */ + public List copyQuestions() { + List copy = new ArrayList<>(questions.size()); + copy.addAll(questions); + return copy; + } + + /** + * Copy the records found in the answer section into a new list. + * + * @return a copy of the answer section records. + * @see #answerSection + */ + public List> copyAnswers() { + List> res = new ArrayList<>(answerSection.size()); + res.addAll(answerSection); + return res; + } + + /** + * Copy the records found in the authority section into a new list. + * + * @return a copy of the authority section records. + * @see #authoritySection + */ + public List> copyAuthority() { + List> res = new ArrayList<>(authoritySection.size()); + res.addAll(authoritySection); + return res; + } + + public Edns getEdns() { + if (edns != null) return edns; + + Record optRecord = getOptPseudoRecord(); + if (optRecord == null) return null; + edns = new Edns(optRecord); + return edns; + } + + @SuppressWarnings("unchecked") + public Record getOptPseudoRecord() { + if (optRrPosition == -1) return null; + return (Record) additionalSection.get(optRrPosition); + } + + /** + * Check if the EDNS DO (DNSSEC OK) flag is set. + * + * @return true if the DO flag is set. + */ + public boolean isDnssecOk() { + Edns edns = getEdns(); + if (edns == null) + return false; + + return edns.dnssecOk; + } + + private String toStringCache; + + @Override + public String toString() { + if (toStringCache != null) return toStringCache; + + StringBuilder sb = new StringBuilder("DnsMessage"); + asBuilder().writeToStringBuilder(sb); + + toStringCache = sb.toString(); + return toStringCache; + } + + private String terminalOutputCache; + + /** + * Format the DnsMessage object in a way suitable for terminal output. + * The format is loosely based on the output provided by {@code dig}. + * + * @return This message as a String suitable for terminal output. + */ + @SuppressWarnings("JavaUtilDate") + public String asTerminalOutput() { + if (terminalOutputCache != null) return terminalOutputCache; + + StringBuilder sb = new StringBuilder(";; ->>HEADER<<-") + .append(" opcode: ").append(opcode) + .append(", status: ").append(responseCode) + .append(", id: ").append(id).append("\n") + .append(";; flags:"); + if (!qr) sb.append(" qr"); + if (authoritativeAnswer) sb.append(" aa"); + if (truncated) sb.append(" tr"); + if (recursionDesired) sb.append(" rd"); + if (recursionAvailable) sb.append(" ra"); + if (authenticData) sb.append(" ad"); + if (checkingDisabled) sb.append(" cd"); + sb.append("; QUERY: ").append(questions.size()) + .append(", ANSWER: ").append(answerSection.size()) + .append(", AUTHORITY: ").append(authoritySection.size()) + .append(", ADDITIONAL: ").append(additionalSection.size()) + .append("\n\n"); + for (Record record : additionalSection) { + Edns edns = Edns.fromRecord(record); + if (edns != null) { + sb.append(";; OPT PSEUDOSECTION:\n; ").append(edns.asTerminalOutput()); + break; + } + } + if (questions.size() != 0) { + sb.append(";; QUESTION SECTION:\n"); + for (Question question : questions) { + sb.append(';').append(question.toString()).append('\n'); + } + } + if (authoritySection.size() != 0) { + sb.append("\n;; AUTHORITY SECTION:\n"); + for (Record record : authoritySection) { + sb.append(record.toString()).append('\n'); + } + } + if (answerSection.size() != 0) { + sb.append("\n;; ANSWER SECTION:\n"); + for (Record record : answerSection) { + sb.append(record.toString()).append('\n'); + } + } + if (additionalSection.size() != 0) { + boolean hasNonOptArr = false; + for (Record record : additionalSection) { + if (record.type != Record.TYPE.OPT) { + if (!hasNonOptArr) { + hasNonOptArr = true; + sb.append("\n;; ADDITIONAL SECTION:\n"); + } + sb.append(record.toString()).append('\n'); + } + } + } + if (receiveTimestamp > 0) { + sb.append("\n;; WHEN: ").append(new Date(receiveTimestamp).toString()); + } + terminalOutputCache = sb.toString(); + return terminalOutputCache; + } + + public Set getAnswersFor(Question q) { + if (responseCode != RESPONSE_CODE.NO_ERROR) return null; + + // It would be great if we could verify that D matches q.type at this + // point. But on the other hand, if it does not, then the cast to D + // below will fail. + Set res = new HashSet<>(answerSection.size()); + for (Record record : answerSection) { + if (!record.isAnswer(q)) continue; + + Data data = record.getPayload(); + @SuppressWarnings("unchecked") + D d = (D) data; + boolean isNew = res.add(d); + if (!isNew) { + LOGGER.log(Level.WARNING, "DnsMessage contains duplicate answers. Record: " + record + "; DnsMessage: " + this); + } + } + return res; + } + + private long answersMinTtlCache = -1; + + /** + * Get the minimum TTL from all answers in seconds. + * + * @return the minimum TTL from all answers in seconds. + */ + public long getAnswersMinTtl() { + if (answersMinTtlCache >= 0) { + return answersMinTtlCache; + } + + answersMinTtlCache = Long.MAX_VALUE; + for (Record r : answerSection) { + answersMinTtlCache = Math.min(answersMinTtlCache, r.ttl); + } + return answersMinTtlCache; + } + + public Builder asBuilder() { + return new Builder(this); + } + + private DnsMessage normalizedVersionCache; + + public DnsMessage asNormalizedVersion() { + if (normalizedVersionCache == null) { + normalizedVersionCache = new DnsMessage(this); + } + return normalizedVersionCache; + } + + public Builder getResponseBuilder(RESPONSE_CODE responseCode) { + if (qr) { + throw new IllegalStateException(); + } + Builder responseBuilder = DnsMessage.builder() + .setQrFlag(true) + .setResponseCode(responseCode) + .setId(id) + .setQuestion(getQuestion()); + + return responseBuilder; + } + + private transient Integer hashCodeCache; + + @Override + public int hashCode() { + if (hashCodeCache == null) { + byte[] bytes = serialize(); + hashCodeCache = Arrays.hashCode(bytes); + } + return hashCodeCache; + } + + private enum SectionName { + answer, + authority, + additional, + } + + private List> filterSectionByType(boolean stopOnFirst, SectionName sectionName, Class type) { + List> sectionToFilter; + switch (sectionName) { + case answer: + sectionToFilter = answerSection; + break; + case authority: + sectionToFilter = authoritySection; + break; + case additional: + sectionToFilter = additionalSection; + break; + default: + throw new AssertionError("Unknown section name " + sectionName); + } + + List> res = new ArrayList<>(stopOnFirst ? 1 : sectionToFilter.size()); + + for (Record record : sectionToFilter) { + Record target = record.ifPossibleAs(type); + if (target != null) { + res.add(target); + if (stopOnFirst) { + return res; + } + } + } + + return res; + } + + private List> filterSectionByType(SectionName sectionName, Class type) { + return filterSectionByType(false, sectionName, type); + } + + private Record getFirstOfType(SectionName sectionName, Class type) { + List> result = filterSectionByType(true, sectionName, type); + if (result.isEmpty()) { + return null; + } + + return result.get(0); + } + + public List> filterAnswerSectionBy(Class type) { + return filterSectionByType(SectionName.answer, type); + } + + public List> filterAuthoritySectionBy(Class type) { + return filterSectionByType(SectionName.authority, type); + } + + public List> filterAdditionalSectionBy(Class type) { + return filterSectionByType(SectionName.additional, type); + } + + public Record getFirstOfTypeFromAnswerSection(Class type) { + return getFirstOfType(SectionName.answer, type); + } + + public Record getFirstOfTypeFromAuthoritySection(Class type) { + return getFirstOfType(SectionName.authority, type); + } + + public Record getFirstOfTypeFromAdditionalSection(Class type) { + return getFirstOfType(SectionName.additional, type); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof DnsMessage)) { + return false; + } + if (other == this) { + return true; + } + DnsMessage otherDnsMessage = (DnsMessage) other; + byte[] otherBytes = otherDnsMessage.serialize(); + byte[] myBytes = serialize(); + return Arrays.equals(myBytes, otherBytes); + } + + public static Builder builder() { + return new DnsMessage.Builder(); + } + + public static final class Builder { + + private Builder() { + } + + private Builder(DnsMessage message) { + id = message.id; + opcode = message.opcode; + responseCode = message.responseCode; + query = message.qr; + authoritativeAnswer = message.authoritativeAnswer; + truncated = message.truncated; + recursionDesired = message.recursionDesired; + recursionAvailable = message.recursionAvailable; + authenticData = message.authenticData; + checkingDisabled = message.checkingDisabled; + receiveTimestamp = message.receiveTimestamp; + + // Copy the unmodifiable lists over into this new builder. + questions = new ArrayList<>(message.questions.size()); + questions.addAll(message.questions); + answerSection = new ArrayList<>(message.answerSection.size()); + answerSection.addAll(message.answerSection); + authoritySection = new ArrayList<>(message.authoritySection.size()); + authoritySection.addAll(message.authoritySection); + additionalSection = new ArrayList<>(message.additionalSection.size()); + additionalSection.addAll(message.additionalSection); + } + + private int id; + private OPCODE opcode = OPCODE.QUERY; + private RESPONSE_CODE responseCode = RESPONSE_CODE.NO_ERROR; + private boolean query; + private boolean authoritativeAnswer; + private boolean truncated; + private boolean recursionDesired; + private boolean recursionAvailable; + private boolean authenticData; + private boolean checkingDisabled; + + private long receiveTimestamp = -1; + + private List questions; + private List> answerSection; + private List> authoritySection; + private List> additionalSection; + private Edns.Builder ednsBuilder; + + /** + * Set the current DNS message id. + * + * @param id The new DNS message id. + * @return a reference to this builder. + */ + public Builder setId(int id) { + this.id = id & 0xffff; + return this; + } + + public Builder setOpcode(OPCODE opcode) { + this.opcode = opcode; + return this; + } + + public Builder setResponseCode(RESPONSE_CODE responseCode) { + this.responseCode = responseCode; + return this; + } + + /** + * Set the QR flag. Note that this will be true if the message is a + * response and false if it is a query. + * + * @param query The new QR flag status. + * @return a reference to this builder. + */ + public Builder setQrFlag(boolean query) { + this.query = query; + return this; + } + + /** + * Set the authoritative answer flag. + * + * @param authoritativeAnswer Tge new authoritative answer value. + * @return a reference to this builder. + */ + public Builder setAuthoritativeAnswer(boolean authoritativeAnswer) { + this.authoritativeAnswer = authoritativeAnswer; + return this; + } + + /** + * Set the truncation bit on this DNS message. + * + * @param truncated The new truncated bit status. + * @return a reference to this builder. + */ + public Builder setTruncated(boolean truncated) { + this.truncated = truncated; + return this; + } + + /** + * Set the recursion desired flag on this message. + * + * @param recursionDesired The new recusrion setting. + * @return a reference to this builder. + */ + public Builder setRecursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + /** + * Set the recursion available flog from this DNS message. + * + * @param recursionAvailable The new recursion available status. + * @return a reference to this builder. + */ + public Builder setRecursionAvailable(boolean recursionAvailable) { + this.recursionAvailable = recursionAvailable; + return this; + } + + /** + * Set the authentic data flag on this DNS message. + * + * @param authenticData The new authentic data flag value. + * @return a reference to this builder. + */ + public Builder setAuthenticData(boolean authenticData) { + this.authenticData = authenticData; + return this; + } + + /** + * Change the check status of this packet. + * + * @param checkingDisabled The new check disabled value. + * @return a reference to this builder. + */ + @Deprecated + public Builder setCheckDisabled(boolean checkingDisabled) { + this.checkingDisabled = checkingDisabled; + return this; + } + + /** + * Change the check status of this packet. + * + * @param checkingDisabled The new check disabled value. + * @return a reference to this builder. + */ + public Builder setCheckingDisabled(boolean checkingDisabled) { + this.checkingDisabled = checkingDisabled; + return this; + } + + public void copyFlagsFrom(DnsMessage dnsMessage) { + this.query = dnsMessage.qr; + this.authoritativeAnswer = dnsMessage.authenticData; + this.truncated = dnsMessage.truncated; + this.recursionDesired = dnsMessage.recursionDesired; + this.recursionAvailable = dnsMessage.recursionAvailable; + this.authenticData = dnsMessage.authenticData; + this.checkingDisabled = dnsMessage.checkingDisabled; + } + + public Builder setReceiveTimestamp(long receiveTimestamp) { + this.receiveTimestamp = receiveTimestamp; + return this; + } + + public Builder addQuestion(Question question) { + if (questions == null) { + questions = new ArrayList<>(1); + } + questions.add(question); + return this; + } + + /** + * Set the question part of this message. + * + * @param questions The questions. + * @return a reference to this builder. + */ + public Builder setQuestions(List questions) { + this.questions = questions; + return this; + } + + /** + * Set the question part of this message. + * + * @param question The question. + * @return a reference to this builder. + */ + public Builder setQuestion(Question question) { + this.questions = new ArrayList<>(1); + this.questions.add(question); + return this; + } + + public Builder addAnswer(Record answer) { + if (answerSection == null) { + answerSection = new ArrayList<>(1); + } + answerSection.add(answer); + return this; + } + + public Builder addAnswers(Collection> records) { + if (answerSection == null) { + answerSection = new ArrayList<>(records.size()); + } + answerSection.addAll(records); + return this; + } + + public Builder setAnswers(Collection> records) { + answerSection = new ArrayList<>(records.size()); + answerSection.addAll(records); + return this; + } + + public List> getAnswers() { + if (answerSection == null) { + return Collections.emptyList(); + } + return answerSection; + } + + public Builder addNameserverRecords(Record record) { + if (authoritySection == null) { + authoritySection = new ArrayList<>(8); + } + authoritySection.add(record); + return this; + } + + public Builder setNameserverRecords(Collection> records) { + authoritySection = new ArrayList<>(records.size()); + authoritySection.addAll(records); + return this; + } + + public Builder setAdditionalResourceRecords(Collection> records) { + additionalSection = new ArrayList<>(records.size()); + additionalSection.addAll(records); + return this; + } + + public Builder addAdditionalResourceRecord(Record record) { + if (additionalSection == null) { + additionalSection = new ArrayList<>(); + } + additionalSection.add(record); + return this; + } + + public Builder addAdditionalResourceRecords(List> records) { + if (additionalSection == null) { + additionalSection = new ArrayList<>(records.size()); + } + additionalSection.addAll(records); + return this; + } + + public List> getAdditionalResourceRecords() { + if (additionalSection == null) { + return Collections.emptyList(); + } + return additionalSection; + } + + /** + * Get the {@link Edns} builder. If no builder has been set so far, then a new one will be created. + *

+ * The EDNS record can be used to announce the supported size of UDP payload as well as additional flags. + *

+ *

+ * Note that some networks and firewalls are known to block big UDP payloads. 1280 should be a reasonable value, + * everything below 512 is treated as 512 and should work on all networks. + *

+ * + * @return a EDNS builder. + */ + public Edns.Builder getEdnsBuilder() { + if (ednsBuilder == null) { + ednsBuilder = Edns.builder(); + } + return ednsBuilder; + } + + public DnsMessage build() { + return new DnsMessage(this); + } + + private void writeToStringBuilder(StringBuilder sb) { + sb.append('(') + .append(id) + .append(' ') + .append(opcode) + .append(' ') + .append(responseCode) + .append(' '); + if (query) { + sb.append("resp[qr=1]"); + } else { + sb.append("query[qr=0]"); + } + if (authoritativeAnswer) + sb.append(" aa"); + if (truncated) + sb.append(" tr"); + if (recursionDesired) + sb.append(" rd"); + if (recursionAvailable) + sb.append(" ra"); + if (authenticData) + sb.append(" ad"); + if (checkingDisabled) + sb.append(" cd"); + sb.append(")\n"); + if (questions != null) { + for (Question question : questions) { + sb.append("[Q: ").append(question).append("]\n"); + } + } + if (answerSection != null) { + for (Record record : answerSection) { + sb.append("[A: ").append(record).append("]\n"); + } + } + if (authoritySection != null) { + for (Record record : authoritySection) { + sb.append("[N: ").append(record).append("]\n"); + } + } + if (additionalSection != null) { + for (Record record : additionalSection) { + sb.append("[X: "); + Edns edns = Edns.fromRecord(record); + if (edns != null) { + sb.append(edns.toString()); + } else { + sb.append(record); + } + sb.append("]\n"); + } + } + + // Strip trailing newline. + if (sb.charAt(sb.length() - 1) == '\n') { + sb.setLength(sb.length() - 1); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Builder of DnsMessage"); + writeToStringBuilder(sb); + return sb.toString(); + } + } + +} diff --git a/src/main/java/org/minidns/dnsmessage/Question.java b/src/main/java/org/minidns/dnsmessage/Question.java new file mode 100644 index 000000000..29db7466b --- /dev/null +++ b/src/main/java/org/minidns/dnsmessage/Question.java @@ -0,0 +1,180 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsmessage; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.CLASS; +import org.minidns.record.Record.TYPE; + +/** + * A DNS question (request). + */ +public class Question { + + /** + * The question string (e.g. "measite.de"). + */ + public final DnsName name; + + /** + * The question type (e.g. A). + */ + public final TYPE type; + + /** + * The question class (usually IN for Internet). + */ + public final CLASS clazz; + + /** + * UnicastQueries have the highest bit of the CLASS field set to 1. + */ + private final boolean unicastQuery; + + /** + * Cache for the serialized object. + */ + private byte[] byteArray; + + /** + * Create a dns question for the given name/type/class. + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + * @param clazz The class, usually IN (internet). + * @param unicastQuery True if this is a unicast query. + */ + public Question(CharSequence name, TYPE type, CLASS clazz, boolean unicastQuery) { + this(DnsName.from(name), type, clazz, unicastQuery); + } + + public Question(DnsName name, TYPE type, CLASS clazz, boolean unicastQuery) { + assert name != null; + assert type != null; + assert clazz != null; + this.name = name; + this.type = type; + this.clazz = clazz; + this.unicastQuery = unicastQuery; + } + + /** + * Create a dns question for the given name/type/class. + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + * @param clazz The class, usually IN (internet). + */ + public Question(DnsName name, TYPE type, CLASS clazz) { + this(name, type, clazz, false); + } + + /** + * Create a dns question for the given name/type/IN (internet class). + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + */ + public Question(DnsName name, TYPE type) { + this(name, type, CLASS.IN); + } + + /** + * Create a dns question for the given name/type/class. + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + * @param clazz The class, usually IN (internet). + */ + public Question(CharSequence name, TYPE type, CLASS clazz) { + this(DnsName.from(name), type, clazz); + } + + /** + * Create a dns question for the given name/type/IN (internet class). + * @param name The name e.g. "measite.de". + * @param type The type, e.g. A. + */ + public Question(CharSequence name, TYPE type) { + this(DnsName.from(name), type); + } + + /** + * Parse a byte array and rebuild the dns question from it. + * @param dis The input stream. + * @param data The plain data (for dns name references). + * @throws IOException On errors (read outside of packet). + */ + public Question(DataInputStream dis, byte[] data) throws IOException { + name = DnsName.parse(dis, data); + type = TYPE.getType(dis.readUnsignedShort()); + clazz = CLASS.getClass(dis.readUnsignedShort()); + unicastQuery = false; + } + + /** + * Generate a binary paket for this dns question. + * @return The dns question. + */ + public byte[] toByteArray() { + if (byteArray == null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + + try { + name.writeToStream(dos); + dos.writeShort(type.getValue()); + dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0)); + dos.flush(); + } catch (IOException e) { + // Should never happen + throw new RuntimeException(e); + } + byteArray = baos.toByteArray(); + } + return byteArray; + } + + @Override + public int hashCode() { + return Arrays.hashCode(toByteArray()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Question)) { + return false; + } + byte[] t = toByteArray(); + byte[] o = ((Question) other).toByteArray(); + return Arrays.equals(t, o); + } + + @Override + public String toString() { + return name.getRawAce() + ".\t" + clazz + '\t' + type; + } + + public DnsMessage.Builder asMessageBuilder() { + DnsMessage.Builder builder = DnsMessage.builder(); + builder.setQuestion(this); + return builder; + } + + public DnsMessage asQueryMessage() { + return asMessageBuilder().build(); + } +} diff --git a/src/main/java/org/minidns/dnsname/DnsName.java b/src/main/java/org/minidns/dnsname/DnsName.java new file mode 100644 index 000000000..860363822 --- /dev/null +++ b/src/main/java/org/minidns/dnsname/DnsName.java @@ -0,0 +1,646 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsname; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; + +import org.minidns.dnslabel.DnsLabel; +import org.minidns.idna.MiniDnsIdna; +import org.minidns.util.SafeCharSequence; + +/** + * A DNS name, also called "domain name". A DNS name consists of multiple 'labels' (see {@link DnsLabel}) and is subject to certain restrictions (see + * for example RFC 3696 § 2.). + *

+ * Instances of this class can be created by using {@link #from(String)}. + *

+ *

+ * This class holds three representations of a DNS name: ACE, raw ACE and IDN. ACE (ASCII Compatible Encoding), which + * can be accessed via {@link #ace}, represents mostly the data that got send over the wire. But since DNS names are + * case insensitive, the ACE value is normalized to lower case. You can use {@link #getRawAce()} to get the raw ACE data + * that was received, which possibly includes upper case characters. The IDN (Internationalized Domain Name), that is + * the DNS name as it should be shown to the user, can be retrieved using {@link #asIdn()}. + *

+ * More information about Internationalized Domain Names can be found at: + * + * + * @see RFC 3696 + * @see DnsLabel + * @author Florian Schmaus + * + */ +public final class DnsName extends SafeCharSequence implements Serializable, Comparable { + + /** + * + */ + private static final long serialVersionUID = 1L; + + /** + * @see RFC 3490 § 3.1 1. + */ + private static final String LABEL_SEP_REGEX = "[.\u3002\uFF0E\uFF61]"; + + /** + * See RFC 1035 § 2.3.4. + */ + static final int MAX_DNSNAME_LENGTH_IN_OCTETS = 255; + + public static final int MAX_LABELS = 128; + + public static final DnsName ROOT = new DnsName("."); + + public static final DnsName IN_ADDR_ARPA = new DnsName("in-addr.arpa"); + + public static final DnsName IP6_ARPA = new DnsName("ip6.arpa"); + + /** + * Whether or not the DNS name is validated on construction. + */ + public static boolean VALIDATE = true; + + /** + * The DNS name in ASCII Compatible Encoding (ACE). + */ + public final String ace; + + /** + * The DNS name in raw format, i.e. as it was received from the remote server. This means that compared to + * {@link #ace}, this String may not be lower-cased. + */ + private final String rawAce; + + private transient byte[] bytes; + + private transient byte[] rawBytes; + + private transient String idn; + + private transient String domainpart; + + private transient String hostpart; + + /** + * The labels in reverse order. + */ + private transient DnsLabel[] labels; + + private transient DnsLabel[] rawLabels; + + private transient int hashCode; + + private int size = -1; + + private DnsName(String name) { + this(name, true); + } + + private DnsName(String name, boolean inAce) { + if (name.isEmpty()) { + rawAce = ROOT.rawAce; + } else { + final int nameLength = name.length(); + final int nameLastPos = nameLength - 1; + + // Strip potential trailing dot. N.B. that we require nameLength > 2, because we don't want to strip the one + // character string containing only a single dot to the empty string. + if (nameLength >= 2 && name.charAt(nameLastPos) == '.') { + name = name.subSequence(0, nameLastPos).toString(); + } + + if (inAce) { + // Name is already in ACE format. + rawAce = name; + } else { + rawAce = MiniDnsIdna.toASCII(name); + } + } + + ace = rawAce.toLowerCase(Locale.US); + + if (!VALIDATE) { + return; + } + + // Validate the DNS name. + validateMaxDnsnameLengthInOctets(); + } + + private DnsName(DnsLabel[] rawLabels, boolean validateMaxDnsnameLength) { + this.rawLabels = rawLabels; + this.labels = new DnsLabel[rawLabels.length]; + + int size = 0; + for (int i = 0; i < rawLabels.length; i++) { + size += rawLabels[i].length() + 1; + labels[i] = rawLabels[i].asLowercaseVariant(); + } + + rawAce = labelsToString(rawLabels, size); + ace = labelsToString(labels, size); + + // The following condition is deliberately designed that VALIDATE=false causes the validation to be skipped even + // if validateMaxDnsnameLength is set to true. There is no need to validate even if this constructor is called + // with validateMaxDnsnameLength set to true if VALIDATE is globally set to false. + if (!validateMaxDnsnameLength || !VALIDATE) { + return; + } + + validateMaxDnsnameLengthInOctets(); + } + + private static String labelsToString(DnsLabel[] labels, int stringLength) { + StringBuilder sb = new StringBuilder(stringLength); + for (int i = labels.length - 1; i >= 0; i--) { + sb.append(labels[i]).append('.'); + } + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + private void validateMaxDnsnameLengthInOctets() { + setBytesIfRequired(); + if (bytes.length > MAX_DNSNAME_LENGTH_IN_OCTETS) { + throw new InvalidDnsNameException.DNSNameTooLongException(ace, bytes); + } + } + + public void writeToStream(OutputStream os) throws IOException { + setBytesIfRequired(); + os.write(bytes); + } + + /** + * Serialize a domain name under IDN rules. + * + * @return The binary domain name representation. + */ + public byte[] getBytes() { + setBytesIfRequired(); + return bytes.clone(); + } + + public byte[] getRawBytes() { + if (rawBytes == null) { + setLabelsIfRequired(); + rawBytes = toBytes(rawLabels); + } + + return rawBytes.clone(); + } + + private void setBytesIfRequired() { + if (bytes != null) + return; + + setLabelsIfRequired(); + bytes = toBytes(labels); + } + + private static byte[] toBytes(DnsLabel[] labels) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + for (int i = labels.length - 1; i >= 0; i--) { + labels[i].writeToBoas(baos); + } + + baos.write(0); + + assert baos.size() <= MAX_DNSNAME_LENGTH_IN_OCTETS; + + return baos.toByteArray(); + } + + private void setLabelsIfRequired() { + if (labels != null && rawLabels != null) return; + + if (isRootLabel()) { + rawLabels = labels = new DnsLabel[0]; + return; + } + + labels = getLabels(ace); + rawLabels = getLabels(rawAce); + } + + private static DnsLabel[] getLabels(String ace) { + String[] labels = ace.split(LABEL_SEP_REGEX, MAX_LABELS); + + // Reverse the labels, so that 'foo, example, org' becomes 'org, example, foo'. + for (int i = 0; i < labels.length / 2; i++) { + String t = labels[i]; + int j = labels.length - i - 1; + labels[i] = labels[j]; + labels[j] = t; + } + + try { + return DnsLabel.from(labels); + } catch (DnsLabel.LabelToLongException e) { + throw new InvalidDnsNameException.LabelTooLongException(ace, e.label); + } + } + + /** + * Return the ACE (ASCII Compatible Encoding) version of this DNS name. Note + * that this method may return a String containing null bytes. Those Strings are + * notoriously difficult to handle from a security perspective. Therefore it is + * recommended to use {@link #toString()} instead, which will return a sanitized + * String. + * + * @return the ACE version of this DNS name. + * @since 1.1.0 + */ + public String getAce() { + return ace; + } + + /** + * Returns the raw ACE version of this DNS name. That is, the version as it was + * received over the wire. Most notably, this version may include uppercase + * letters. + * + * Please refer to {@link #getAce()} for a discussion of the security + * implications when working with the ACE representation of a DNS name. + * + * @return the raw ACE version of this DNS name. + * @see #getAce() + */ + public String getRawAce() { + return rawAce; + } + + public String asIdn() { + if (idn != null) + return idn; + + idn = MiniDnsIdna.toUnicode(ace); + return idn; + } + + /** + * Domainpart in ACE representation. + * + * @return the domainpart in ACE representation. + */ + public String getDomainpart() { + setHostnameAndDomainpartIfRequired(); + return domainpart; + } + + /** + * Hostpart in ACE representation. + * + * @return the hostpart in ACE representation. + */ + public String getHostpart() { + setHostnameAndDomainpartIfRequired(); + return hostpart; + } + + public DnsLabel getHostpartLabel() { + setLabelsIfRequired(); + return labels[labels.length - 1]; + } + + private void setHostnameAndDomainpartIfRequired() { + if (hostpart != null) return; + + String[] parts = ace.split(LABEL_SEP_REGEX, 2); + hostpart = parts[0]; + if (parts.length > 1) { + domainpart = parts[1]; + } else { + domainpart = ""; + } + } + + public int size() { + if (size < 0) { + if (isRootLabel()) { + size = 1; + } else { + size = ace.length() + 2; + } + } + return size; + } + + private transient String safeToStringRepresentation; + + @Override + public String toString() { + if (safeToStringRepresentation == null) { + setLabelsIfRequired(); + if (labels.length == 0) { + return "."; + } + + StringBuilder sb = new StringBuilder(); + for (int i = labels.length - 1; i >= 0; i--) { + // Note that it is important that we append the result of DnsLabel.toString() to + // the StringBuilder. As only the result of toString() is the safe label + // representation. + String safeLabelRepresentation = labels[i].toString(); + sb.append(safeLabelRepresentation); + if (i != 0) { + sb.append('.'); + } + } + safeToStringRepresentation = sb.toString(); + } + + return safeToStringRepresentation; + } + + public static DnsName from(CharSequence name) { + return from(name.toString()); + } + + public static DnsName from(String name) { + return new DnsName(name, false); + } + + /** + * Create a DNS name by "concatenating" the child under the parent name. The child can also be seen as the "left" + * part of the resulting DNS name and the parent is the "right" part. + *

+ * For example using "i.am.the.child" as child and "of.this.parent.example" as parent, will result in a DNS name: + * "i.am.the.child.of.this.parent.example". + *

+ * + * @param child the child DNS name. + * @param parent the parent DNS name. + * @return the resulting of DNS name. + */ + public static DnsName from(DnsName child, DnsName parent) { + child.setLabelsIfRequired(); + parent.setLabelsIfRequired(); + + DnsLabel[] rawLabels = new DnsLabel[child.rawLabels.length + parent.rawLabels.length]; + System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length); + System.arraycopy(child.rawLabels, 0, rawLabels, parent.rawLabels.length, child.rawLabels.length); + return new DnsName(rawLabels, true); + } + + public static DnsName from(CharSequence child, DnsName parent) { + DnsLabel childLabel = DnsLabel.from(child.toString()); + return DnsName.from(childLabel, parent); + } + + public static DnsName from(DnsLabel child, DnsName parent) { + parent.setLabelsIfRequired(); + + DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 1]; + System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length); + rawLabels[parent.rawLabels.length] = child; + return new DnsName(rawLabels, true); + } + + public static DnsName from(DnsLabel grandchild, DnsLabel child, DnsName parent) { + parent.setBytesIfRequired(); + + DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 2]; + System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length); + rawLabels[parent.rawLabels.length] = child; + rawLabels[parent.rawLabels.length + 1] = grandchild; + return new DnsName(rawLabels, true); + } + + public static DnsName from(DnsName... nameComponents) { + int labelCount = 0; + for (DnsName component : nameComponents) { + component.setLabelsIfRequired(); + labelCount += component.rawLabels.length; + } + + DnsLabel[] rawLabels = new DnsLabel[labelCount]; + int destLabelPos = 0; + for (int i = nameComponents.length - 1; i >= 0; i--) { + DnsName component = nameComponents[i]; + System.arraycopy(component.rawLabels, 0, rawLabels, destLabelPos, component.rawLabels.length); + destLabelPos += component.rawLabels.length; + } + + return new DnsName(rawLabels, true); + } + + public static DnsName from(String[] parts) { + DnsLabel[] rawLabels = DnsLabel.from(parts); + + return new DnsName(rawLabels, true); + } + + /** + * Parse a domain name starting at the current offset and moving the input + * stream pointer past this domain name (even if cross references occure). + * + * @param dis The input stream. + * @param data The raw data (for cross references). + * @return The domain name string. + * @throws IOException Should never happen. + */ + public static DnsName parse(DataInputStream dis, byte[] data) + throws IOException { + int c = dis.readUnsignedByte(); + if ((c & 0xc0) == 0xc0) { + c = ((c & 0x3f) << 8) + dis.readUnsignedByte(); + HashSet jumps = new HashSet(); + jumps.add(c); + return parse(data, c, jumps); + } + if (c == 0) { + return DnsName.ROOT; + } + byte[] b = new byte[c]; + dis.readFully(b); + + String childLabelString = new String(b, StandardCharsets.US_ASCII); + DnsName child = new DnsName(childLabelString); + + DnsName parent = parse(dis, data); + return DnsName.from(child, parent); + } + + /** + * Parse a domain name starting at the given offset. + * + * @param data The raw data. + * @param offset The offset. + * @param jumps The list of jumps (by now). + * @return The parsed domain name. + * @throws IllegalStateException on cycles. + */ + @SuppressWarnings("NonApiType") + private static DnsName parse(byte[] data, int offset, HashSet jumps) + throws IllegalStateException { + int c = data[offset] & 0xff; + if ((c & 0xc0) == 0xc0) { + c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff); + if (jumps.contains(c)) { + throw new IllegalStateException("Cyclic offsets detected."); + } + jumps.add(c); + return parse(data, c, jumps); + } + if (c == 0) { + return DnsName.ROOT; + } + + String childLabelString = new String(data, offset + 1, c, StandardCharsets.US_ASCII); + DnsName child = new DnsName(childLabelString); + + DnsName parent = parse(data, offset + 1 + c, jumps); + return DnsName.from(child, parent); + } + + @Override + public int compareTo(DnsName other) { + return ace.compareTo(other.ace); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + + if (other instanceof DnsName) { + DnsName otherDnsName = (DnsName) other; + setBytesIfRequired(); + otherDnsName.setBytesIfRequired(); + return Arrays.equals(bytes, otherDnsName.bytes); + } + + return false; + } + + @Override + public int hashCode() { + if (hashCode == 0 && !isRootLabel()) { + setBytesIfRequired(); + hashCode = Arrays.hashCode(bytes); + } + return hashCode; + } + + public boolean isDirectChildOf(DnsName parent) { + setLabelsIfRequired(); + parent.setLabelsIfRequired(); + int parentLabelsCount = parent.labels.length; + + if (labels.length - 1 != parentLabelsCount) + return false; + + for (int i = 0; i < parent.labels.length; i++) { + if (!labels[i].equals(parent.labels[i])) + return false; + } + + return true; + } + + public boolean isChildOf(DnsName parent) { + setLabelsIfRequired(); + parent.setLabelsIfRequired(); + + if (labels.length < parent.labels.length) + return false; + + for (int i = 0; i < parent.labels.length; i++) { + if (!labels[i].equals(parent.labels[i])) + return false; + } + + return true; + } + + public int getLabelCount() { + setLabelsIfRequired(); + return labels.length; + } + + /** + * Get a copy of the labels of this DNS name. The resulting array will contain the labels in reverse order, that is, + * the top-level domain will be at res[0]. + * + * @return an array of the labels in reverse order. + */ + public DnsLabel[] getLabels() { + setLabelsIfRequired(); + return labels.clone(); + } + + + public DnsLabel getLabel(int labelNum) { + setLabelsIfRequired(); + return labels[labelNum]; + } + + /** + * Get a copy of the raw labels of this DNS name. The resulting array will contain the labels in reverse order, that is, + * the top-level domain will be at res[0]. + * + * @return an array of the raw labels in reverse order. + */ + public DnsLabel[] getRawLabels() { + setLabelsIfRequired(); + return rawLabels.clone(); + } + + public DnsName stripToLabels(int labelCount) { + setLabelsIfRequired(); + + if (labelCount > labels.length) { + throw new IllegalArgumentException(); + } + + if (labelCount == labels.length) { + return this; + } + + if (labelCount == 0) { + return ROOT; + } + + DnsLabel[] stripedLabels = Arrays.copyOfRange(rawLabels, 0, labelCount); + + return new DnsName(stripedLabels, false); + } + + /** + * Return the parent of this DNS label. Will return the root label if this label itself is the root label (because there is no parent of root). + *

+ * For example: + *

+ *
    + *
  • "foo.bar.org".getParent() == "bar.org"
  • + *
  • ".".getParent() == "."
  • + *
+ * @return the parent of this DNS label. + */ + public DnsName getParent() { + if (isRootLabel()) return ROOT; + return stripToLabels(getLabelCount() - 1); + } + + public boolean isRootLabel() { + return ace.isEmpty() || ace.equals("."); + } +} diff --git a/src/main/java/org/minidns/dnsname/InvalidDnsNameException.java b/src/main/java/org/minidns/dnsname/InvalidDnsNameException.java new file mode 100644 index 000000000..e5617d0cd --- /dev/null +++ b/src/main/java/org/minidns/dnsname/InvalidDnsNameException.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsname; + +import org.minidns.dnslabel.DnsLabel; + +public abstract class InvalidDnsNameException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + protected final String ace; + + protected InvalidDnsNameException(String ace) { + this.ace = ace; + } + + public static class LabelTooLongException extends InvalidDnsNameException { + /** + * + */ + private static final long serialVersionUID = 1L; + + private final String label; + + public LabelTooLongException(String ace, String label) { + super(ace); + this.label = label; + } + + @Override + public String getMessage() { + return "The DNS name '" + ace + "' contains the label '" + label + + "' which exceeds the maximum label length of " + DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS + " octets by " + + (label.length() - DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS) + " octets."; + } + } + + public static class DNSNameTooLongException extends InvalidDnsNameException { + /** + * + */ + private static final long serialVersionUID = 1L; + + private final byte[] bytes; + + public DNSNameTooLongException(String ace, byte[] bytes) { + super(ace); + this.bytes = bytes; + } + + @Override + public String getMessage() { + return "The DNS name '" + ace + "' exceeds the maximum name length of " + + DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS + " octets by " + + (bytes.length - DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS) + " octets."; + } + } +} diff --git a/src/main/java/org/minidns/dnsqueryresult/CachedDnsQueryResult.java b/src/main/java/org/minidns/dnsqueryresult/CachedDnsQueryResult.java new file mode 100644 index 000000000..fbb17ae7b --- /dev/null +++ b/src/main/java/org/minidns/dnsqueryresult/CachedDnsQueryResult.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsqueryresult; + +import org.minidns.dnsmessage.DnsMessage; + +public abstract class CachedDnsQueryResult extends DnsQueryResult { + + protected final DnsQueryResult cachedDnsQueryResult; + + protected CachedDnsQueryResult(DnsMessage query, DnsQueryResult cachedDnsQueryResult) { + super(QueryMethod.cachedDirect, query, cachedDnsQueryResult.response); + this.cachedDnsQueryResult = cachedDnsQueryResult; + } + + protected CachedDnsQueryResult(DnsMessage query, DnsMessage response, DnsQueryResult synthesynthesizationSource) { + super(QueryMethod.cachedSynthesized, query, response); + this.cachedDnsQueryResult = synthesynthesizationSource; + } +} diff --git a/src/main/java/org/minidns/dnsqueryresult/DirectCachedDnsQueryResult.java b/src/main/java/org/minidns/dnsqueryresult/DirectCachedDnsQueryResult.java new file mode 100644 index 000000000..a9e977bb6 --- /dev/null +++ b/src/main/java/org/minidns/dnsqueryresult/DirectCachedDnsQueryResult.java @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsqueryresult; + +import org.minidns.dnsmessage.DnsMessage; + +public class DirectCachedDnsQueryResult extends CachedDnsQueryResult { + + public DirectCachedDnsQueryResult(DnsMessage query, DnsQueryResult cachedDnsQueryResult) { + super(query, cachedDnsQueryResult); + } + +} diff --git a/src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java b/src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java new file mode 100644 index 000000000..fb6d2ce8f --- /dev/null +++ b/src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsqueryresult; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; + +public abstract class DnsQueryResult { + + public enum QueryMethod { + udp, + tcp, + asyncUdp, + asyncTcp, + cachedDirect, + cachedSynthesized, + testWorld, + } + + public final QueryMethod queryMethod; + + public final DnsMessage query; + + public final DnsMessage response; + + protected DnsQueryResult(QueryMethod queryMethod, DnsMessage query, DnsMessage response) { + assert queryMethod != null; + assert query != null; + assert response != null; + + this.queryMethod = queryMethod; + this.query = query; + this.response = response; + } + + @Override + public String toString() { + return response.toString(); + } + + public boolean wasSuccessful() { + return response.responseCode == RESPONSE_CODE.NO_ERROR; + } +} diff --git a/src/main/java/org/minidns/dnsqueryresult/StandardDnsQueryResult.java b/src/main/java/org/minidns/dnsqueryresult/StandardDnsQueryResult.java new file mode 100644 index 000000000..947b80852 --- /dev/null +++ b/src/main/java/org/minidns/dnsqueryresult/StandardDnsQueryResult.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsqueryresult; + +import java.net.InetAddress; + +import org.minidns.dnsmessage.DnsMessage; + +public class StandardDnsQueryResult extends DnsQueryResult { + + public final InetAddress serverAddress; + + public final int port; + + public StandardDnsQueryResult(InetAddress serverAddress, int port, QueryMethod queryMethod, DnsMessage query, DnsMessage responseDnsMessage) { + super(queryMethod, query, responseDnsMessage); + this.serverAddress = serverAddress; + this.port = port; + } + +} diff --git a/src/main/java/org/minidns/dnsqueryresult/SynthesizedCachedDnsQueryResult.java b/src/main/java/org/minidns/dnsqueryresult/SynthesizedCachedDnsQueryResult.java new file mode 100644 index 000000000..42c6081de --- /dev/null +++ b/src/main/java/org/minidns/dnsqueryresult/SynthesizedCachedDnsQueryResult.java @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsqueryresult; + +import org.minidns.dnsmessage.DnsMessage; + +public class SynthesizedCachedDnsQueryResult extends CachedDnsQueryResult { + + public SynthesizedCachedDnsQueryResult(DnsMessage query, DnsMessage response, DnsQueryResult synthesynthesizationSource) { + super(query, response, synthesynthesizationSource); + } + +} diff --git a/src/main/java/org/minidns/dnssec/DigestCalculator.java b/src/main/java/org/minidns/dnssec/DigestCalculator.java new file mode 100644 index 000000000..b876031c1 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DigestCalculator.java @@ -0,0 +1,15 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +public interface DigestCalculator { + byte[] digest(byte[] bytes); +} diff --git a/src/main/java/org/minidns/dnssec/DnssecClient.java b/src/main/java/org/minidns/dnssec/DnssecClient.java new file mode 100644 index 000000000..b7e85869c --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DnssecClient.java @@ -0,0 +1,573 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.DnsCache; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason.NoActiveSignaturesReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NoSecureEntryPointReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NoSignaturesReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NoTrustAnchorReason; +import org.minidns.dnssec.DnssecValidationFailedException.AuthorityDoesNotContainSoa; +import org.minidns.iterative.ReliableDnsClient; +import org.minidns.record.DLV; +import org.minidns.record.DNSKEY; +import org.minidns.record.DS; +import org.minidns.record.Data; +import org.minidns.record.DelegatingDnssecRR; +import org.minidns.record.NSEC; +import org.minidns.record.NSEC3; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; +import org.minidns.record.Record.CLASS; +import org.minidns.record.Record.TYPE; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DnssecClient extends ReliableDnsClient { + + /** + * The root zone's KSK. + * The ID of the current key is "Klajeyz", and the key tag value is "20326". + */ + private static final BigInteger rootEntryKey = new BigInteger("1628686155461064465348252249725010996177649738666492500572664444461532807739744536029771810659241049343994038053541290419968870563183856865780916376571550372513476957870843322273120879361960335192976656756972171258658400305760429696147778001233984421619267530978084631948434496468785021389956803104620471232008587410372348519229650742022804219634190734272506220018657920136902014393834092648785514548876370028925405557661759399901378816916683122474038734912535425670533237815676134840739565610963796427401855723026687073600445461090736240030247906095053875491225879656640052743394090544036297390104110989318819106653199917493"); + + private static final DnsName DEFAULT_DLV = DnsName.from("dlv.isc.org"); + + /** + * Create a new DNSSEC aware DNS client using the global default cache. + */ + public DnssecClient() { + this(DEFAULT_CACHE); + } + + /** + * Create a new DNSSEC aware DNS client with the given DNS cache. + * + * @param cache The backend DNS cache. + */ + public DnssecClient(DnsCache cache) { + super(cache); + addSecureEntryPoint(DnsName.ROOT, rootEntryKey.toByteArray()); + } + + /** + * Known secure entry points (SEPs). + */ + private final Map knownSeps = new ConcurrentHashMap<>(); + + private boolean stripSignatureRecords = true; + + /** + * The active DNSSEC Look-aside Validation Registry. May be null. + */ + private DnsName dlv; + + @Override + public DnsQueryResult query(Question q) throws IOException { + DnssecQueryResult dnssecQueryResult = queryDnssec(q); + if (!dnssecQueryResult.isAuthenticData()) { + // TODO: Refine exception. + throw new IOException(); + } + return dnssecQueryResult.dnsQueryResult; + } + + public DnssecQueryResult queryDnssec(CharSequence name, TYPE type) throws IOException { + Question q = new Question(name, type, CLASS.IN); + return queryDnssec(q); + } + + public DnssecQueryResult queryDnssec(Question q) throws IOException { + DnsQueryResult dnsQueryResult = super.query(q); + DnssecQueryResult dnssecQueryResult = performVerification(dnsQueryResult); + return dnssecQueryResult; + } + + private DnssecQueryResult performVerification(DnsQueryResult dnsQueryResult) throws IOException { + if (dnsQueryResult == null) return null; + + DnsMessage dnsMessage = dnsQueryResult.response; + DnsMessage.Builder messageBuilder = dnsMessage.asBuilder(); + + Set unverifiedReasons = verify(dnsMessage); + + messageBuilder.setAuthenticData(unverifiedReasons.isEmpty()); + + List> answers = dnsMessage.answerSection; + List> nameserverRecords = dnsMessage.authoritySection; + List> additionalResourceRecords = dnsMessage.additionalSection; + Set> signatures = new HashSet<>(); + Record.filter(signatures, RRSIG.class, answers); + Record.filter(signatures, RRSIG.class, nameserverRecords); + Record.filter(signatures, RRSIG.class, additionalResourceRecords); + + if (stripSignatureRecords) { + messageBuilder.setAnswers(stripSignatureRecords(answers)); + messageBuilder.setNameserverRecords(stripSignatureRecords(nameserverRecords)); + messageBuilder.setAdditionalResourceRecords(stripSignatureRecords(additionalResourceRecords)); + } + + return new DnssecQueryResult(messageBuilder.build(), dnsQueryResult, signatures, unverifiedReasons); + } + + private static List> stripSignatureRecords(List> records) { + if (records.isEmpty()) return records; + List> recordList = new ArrayList<>(records.size()); + for (Record record : records) { + if (record.type != TYPE.RRSIG) { + recordList.add(record); + } + } + return recordList; + } + + private Set verify(DnsMessage dnsMessage) throws IOException { + if (!dnsMessage.answerSection.isEmpty()) { + return verifyAnswer(dnsMessage); + } else { + return verifyNsec(dnsMessage); + } + } + + private Set verifyAnswer(DnsMessage dnsMessage) throws IOException { + Question q = dnsMessage.questions.get(0); + List> answers = dnsMessage.answerSection; + List> toBeVerified = dnsMessage.copyAnswers(); + VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified); + Set result = verifiedSignatures.reasons; + if (!result.isEmpty()) { + return result; + } + + // Keep SEPs separated, we only need one valid SEP. + boolean sepSignatureValid = false; + Set sepReasons = new HashSet<>(); + for (Iterator> iterator = toBeVerified.iterator(); iterator.hasNext(); ) { + Record record = iterator.next().ifPossibleAs(DNSKEY.class); + if (record == null) { + continue; + } + + // Verify all DNSKEYs as if it was a SEP. If we find a single SEP we are safe. + Set reasons = verifySecureEntryPoint(record); + if (reasons.isEmpty()) { + sepSignatureValid = true; + } else { + sepReasons.addAll(reasons); + } + if (!verifiedSignatures.sepSignaturePresent) { + LOGGER.finer("SEP key is not self-signed."); + } + iterator.remove(); + } + + if (verifiedSignatures.sepSignaturePresent && !sepSignatureValid) { + result.addAll(sepReasons); + } + if (verifiedSignatures.sepSignatureRequired && !verifiedSignatures.sepSignaturePresent) { + result.add(new NoSecureEntryPointReason(q.name)); + } + if (!toBeVerified.isEmpty()) { + if (toBeVerified.size() != answers.size()) { + throw new DnssecValidationFailedException(q, "Only some records are signed!"); + } else { + result.add(new NoSignaturesReason(q)); + } + } + return result; + } + + private Set verifyNsec(DnsMessage dnsMessage) throws IOException { + Set result = new HashSet<>(); + Question q = dnsMessage.questions.get(0); + boolean validNsec = false; + boolean nsecPresent = false; + + // Get the SOA RR that has to be in the authority section. Note that we will verify its signature later, after + // we have verified the NSEC3 RR. And although the data form the SOA RR is only required for NSEC3 we check for + // its existence here, since it would be invalid if there is none. + // TODO: Add a reference to the relevant RFC parts which specify that there has to be a SOA RR in X. + DnsName zone = null; + List> authoritySection = dnsMessage.authoritySection; + for (Record authorityRecord : authoritySection) { + if (authorityRecord.type == TYPE.SOA) { + zone = authorityRecord.name; + break; + } + } + if (zone == null) + throw new AuthorityDoesNotContainSoa(dnsMessage); + + // TODO Examine if it is better to verify the RRs in the authority section *before* we verify NSEC(3). We + // currently do it the other way around. + + // TODO: This whole logic needs to be changed. It currently checks one NSEC(3) record after another, when it + // should first determine if we are dealing with NSEC or NSEC3 and the verify the whole response. + for (Record record : authoritySection) { + DnssecUnverifiedReason reason; + + switch (record.type) { + case NSEC: + nsecPresent = true; + Record nsecRecord = record.as(NSEC.class); + reason = Verifier.verifyNsec(nsecRecord, q); + break; + case NSEC3: + nsecPresent = true; + Record nsec3Record = record.as(NSEC3.class); + reason = Verifier.verifyNsec3(zone, nsec3Record, q); + break; + default: + continue; + } + + if (reason != null) { + result.add(reason); + } else { + validNsec = true; + } + } + + // TODO: Shouldn't we also throw if !nsecPresent? + if (nsecPresent && !validNsec) { + throw new DnssecValidationFailedException(q, "Invalid NSEC!"); + } + + List> toBeVerified = dnsMessage.copyAuthority(); + VerifySignaturesResult verifiedSignatures = verifySignatures(q, authoritySection, toBeVerified); + if (validNsec && verifiedSignatures.reasons.isEmpty()) { + result.clear(); + } else { + result.addAll(verifiedSignatures.reasons); + } + + if (!toBeVerified.isEmpty() && toBeVerified.size() != authoritySection.size()) { + // TODO Refine this exception and include the missing toBeVerified RRs and the whole DnsMessage into it. + throw new DnssecValidationFailedException(q, "Only some resource records from the authority section are signed!"); + } + + return result; + } + + private static final class VerifySignaturesResult { + boolean sepSignatureRequired = false; + boolean sepSignaturePresent = false; + Set reasons = new HashSet<>(); + } + + @SuppressWarnings("JavaUtilDate") + private VerifySignaturesResult verifySignatures(Question q, Collection> reference, List> toBeVerified) throws IOException { + final Date now = new Date(); + final List outdatedRrSigs = new ArrayList<>(); + VerifySignaturesResult result = new VerifySignaturesResult(); + final List> rrsigs = new ArrayList<>(toBeVerified.size()); + + for (Record recordToBeVerified : toBeVerified) { + Record record = recordToBeVerified.ifPossibleAs(RRSIG.class); + if (record == null) continue; + + RRSIG rrsig = record.payloadData; + if (rrsig.signatureExpiration.compareTo(now) < 0 || rrsig.signatureInception.compareTo(now) > 0) { + // This RRSIG is out of date, but there might be one that is not. + outdatedRrSigs.add(rrsig); + continue; + } + rrsigs.add(record); + } + + if (rrsigs.isEmpty()) { + if (!outdatedRrSigs.isEmpty()) { + result.reasons.add(new NoActiveSignaturesReason(q, outdatedRrSigs)); + } else { + // TODO: Check if QNAME results should have signatures and add a different reason if there are RRSIGs + // expected compared to when not. + result.reasons.add(new NoSignaturesReason(q)); + } + return result; + } + + for (Record sigRecord : rrsigs) { + RRSIG rrsig = sigRecord.payloadData; + + List> records = new ArrayList<>(reference.size()); + for (Record record : reference) { + if (record.type == rrsig.typeCovered && record.name.equals(sigRecord.name)) { + records.add(record); + } + } + + Set reasons = verifySignedRecords(q, rrsig, records); + result.reasons.addAll(reasons); + + if (q.name.equals(rrsig.signerName) && rrsig.typeCovered == TYPE.DNSKEY) { + for (Iterator> iterator = records.iterator(); iterator.hasNext(); ) { + Record dnsKeyRecord = iterator.next().ifPossibleAs(DNSKEY.class); + // dnsKeyRecord should never be null here. + DNSKEY dnskey = dnsKeyRecord.payloadData; + // DNSKEYs are verified separately, so don't mark them verified now. + iterator.remove(); + if (dnskey.getKeyTag() == rrsig.keyTag) { + result.sepSignaturePresent = true; + } + } + // DNSKEY's should be signed by a SEP + result.sepSignatureRequired = true; + } + + if (!isParentOrSelf(sigRecord.name.ace, rrsig.signerName.ace)) { + LOGGER.finer("Records at " + sigRecord.name + " are cross-signed with a key from " + rrsig.signerName); + } else { + toBeVerified.removeAll(records); + } + toBeVerified.remove(sigRecord); + } + return result; + } + + private static boolean isParentOrSelf(String child, String parent) { + if (child.equals(parent)) return true; + if (parent.isEmpty()) return true; + String[] childSplit = child.split("\\."); + String[] parentSplit = parent.split("\\."); + if (parentSplit.length > childSplit.length) return false; + for (int i = 1; i <= parentSplit.length; i++) { + if (!parentSplit[parentSplit.length - i].equals(childSplit[childSplit.length - i])) { + return false; + } + } + return true; + } + + private Set verifySignedRecords(Question q, RRSIG rrsig, List> records) throws IOException { + Set result = new HashSet<>(); + DNSKEY dnskey = null; + + if (rrsig.typeCovered == TYPE.DNSKEY) { + // Key must be present + List> dnskeyRrs = Record.filter(DNSKEY.class, records); + for (Record dnsKeyRecord : dnskeyRrs) { + if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) { + dnskey = dnsKeyRecord.payloadData; + break; + } + } + } else if (q.type == TYPE.DS && rrsig.signerName.equals(q.name)) { + // We should not probe for the self signed DS negative response, as it will be an endless loop. + result.add(new NoTrustAnchorReason(q.name)); + return result; + } else { + DnssecQueryResult dnskeyRes = queryDnssec(rrsig.signerName, TYPE.DNSKEY); + result.addAll(dnskeyRes.getUnverifiedReasons()); + List> dnskeyRrs = dnskeyRes.dnsQueryResult.response.filterAnswerSectionBy(DNSKEY.class); + for (Record dnsKeyRecord : dnskeyRrs) { + if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) { + dnskey = dnsKeyRecord.payloadData; + break; + } + } + } + + if (dnskey == null) { + throw new DnssecValidationFailedException(q, records.size() + " " + rrsig.typeCovered + " record(s) are signed using an unknown key."); + } + + DnssecUnverifiedReason unverifiedReason = Verifier.verify(records, rrsig, dnskey); + if (unverifiedReason != null) { + result.add(unverifiedReason); + } + + return result; + } + + private Set verifySecureEntryPoint(final Record sepRecord) throws IOException { + final DNSKEY dnskey = sepRecord.payloadData; + + Set unverifiedReasons = new HashSet<>(); + Set activeReasons = new HashSet<>(); + if (knownSeps.containsKey(sepRecord.name)) { + if (dnskey.keyEquals(knownSeps.get(sepRecord.name))) { + return unverifiedReasons; + } else { + unverifiedReasons.add(new DnssecUnverifiedReason.ConflictsWithSep(sepRecord)); + return unverifiedReasons; + } + } + + // If we are looking for the SEP of the root zone at this point, then the client was not + // configured with one. Hence we can abort and state the reason why we aborted. + if (sepRecord.name.isRootLabel()) { + unverifiedReasons.add(new DnssecUnverifiedReason.NoRootSecureEntryPointReason()); + return unverifiedReasons; + } + + DelegatingDnssecRR delegation = null; + DnssecQueryResult dsResp = queryDnssec(sepRecord.name, TYPE.DS); + unverifiedReasons.addAll(dsResp.getUnverifiedReasons()); + + List> dsRrs = dsResp.dnsQueryResult.response.filterAnswerSectionBy(DS.class); + for (Record dsRecord : dsRrs) { + DS ds = dsRecord.payloadData; + if (dnskey.getKeyTag() == ds.keyTag) { + delegation = ds; + activeReasons = dsResp.getUnverifiedReasons(); + break; + } + } + + if (delegation == null) { + LOGGER.fine("There is no DS record for \'" + sepRecord.name + "\', server gives empty result"); + } + + if (delegation == null && dlv != null && !dlv.isChildOf(sepRecord.name)) { + DnssecQueryResult dlvResp = queryDnssec(DnsName.from(sepRecord.name, dlv), TYPE.DLV); + unverifiedReasons.addAll(dlvResp.getUnverifiedReasons()); + + List> dlvRrs = dlvResp.dnsQueryResult.response.filterAnswerSectionBy(DLV.class); + for (Record dlvRecord : dlvRrs) { + if (sepRecord.payloadData.getKeyTag() == dlvRecord.payloadData.keyTag) { + LOGGER.fine("Found DLV for " + sepRecord.name + ", awesome."); + delegation = dlvRecord.payloadData; + activeReasons = dlvResp.getUnverifiedReasons(); + break; + } + } + } + + if (delegation != null) { + DnssecUnverifiedReason unverifiedReason = Verifier.verify(sepRecord, delegation); + if (unverifiedReason != null) { + unverifiedReasons.add(unverifiedReason); + } else { + unverifiedReasons = activeReasons; + } + } else if (unverifiedReasons.isEmpty()) { + unverifiedReasons.add(new NoTrustAnchorReason(sepRecord.name)); + } + return unverifiedReasons; + } + + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { + message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(); + message.setCheckingDisabled(true); + return super.newQuestion(message); + } + + @Override + protected String isResponseAcceptable(DnsMessage response) { + boolean dnssecOk = response.isDnssecOk(); + if (!dnssecOk) { + // This is a deliberate violation of RFC 6840 § 5.6. I doubt that + // "resolvers MUST ignore the DO bit in responses" does any good. Also we basically ignore the DO bit after + // the fall back to iterative mode. + return "DNSSEC OK (DO) flag not set in response"; + } + boolean checkingDisabled = response.checkingDisabled; + if (!checkingDisabled) { + return "CHECKING DISABLED (CD) flag not set in response"; + } + return super.isResponseAcceptable(response); + } + + /** + * Add a new secure entry point to the list of known secure entry points. + * + * A secure entry point acts as a trust anchor. By default, the only secure entry point is the key signing key + * provided by the root zone. + * + * @param name The domain name originating the key. Once the secure entry point for this domain is requested, + * the resolver will use this key without further verification instead of using the DNS system to + * verify the key. + * @param key The secure entry point corresponding to the domain name. This key can be retrieved by requesting + * the DNSKEY record for the domain and using the key with first flags bit set + * (also called key signing key) + */ + public final void addSecureEntryPoint(DnsName name, byte[] key) { + knownSeps.put(name, key); + } + + /** + * Remove the secure entry point stored for a domain name. + * + * @param name The domain name of which the corresponding secure entry point shall be removed. For the root zone, + * use the empty string here. + */ + public void removeSecureEntryPoint(DnsName name) { + knownSeps.remove(name); + } + + /** + * Clears the list of known secure entry points. + * + * This will also remove the secure entry point of the root zone and + * thus render this instance useless until a new secure entry point is added. + */ + public void clearSecureEntryPoints() { + knownSeps.clear(); + } + + /** + * Whether signature records (RRSIG) are stripped from the resulting {@link DnsMessage}. + * + * Default is {@code true}. + * + * @return Whether signature records are stripped. + */ + public boolean isStripSignatureRecords() { + return stripSignatureRecords; + } + + /** + * Enable or disable stripping of signature records (RRSIG) from the result {@link DnsMessage}. + * @param stripSignatureRecords Whether signature records shall be stripped. + */ + public void setStripSignatureRecords(boolean stripSignatureRecords) { + this.stripSignatureRecords = stripSignatureRecords; + } + + /** + * Enables DNSSEC Lookaside Validation (DLV) using the default DLV service at dlv.isc.org. + */ + public void enableLookasideValidation() { + configureLookasideValidation(DEFAULT_DLV); + } + + /** + * Disables DNSSEC Lookaside Validation (DLV). + * DLV is disabled by default, this is only required if {@link #enableLookasideValidation()} was used before. + */ + public void disableLookasideValidation() { + configureLookasideValidation(null); + } + + /** + * Enables DNSSEC Lookaside Validation (DLV) using the given DLV service. + * + * @param dlv The domain name of the DLV service to be used or {@code null} to disable DLV. + */ + public void configureLookasideValidation(DnsName dlv) { + this.dlv = dlv; + } +} diff --git a/src/main/java/org/minidns/dnssec/DnssecQueryResult.java b/src/main/java/org/minidns/dnssec/DnssecQueryResult.java new file mode 100644 index 000000000..de1d4f676 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DnssecQueryResult.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; + +public class DnssecQueryResult { + + public final DnsMessage synthesizedResponse; + public final DnsQueryResult dnsQueryResult; + + private final Set> signatures; + private final Set dnssecUnverifiedReasons; + + DnssecQueryResult(DnsMessage synthesizedResponse, DnsQueryResult dnsQueryResult, Set> signatures, + Set dnssecUnverifiedReasons) { + this.synthesizedResponse = synthesizedResponse; + this.dnsQueryResult = dnsQueryResult; + this.signatures = Collections.unmodifiableSet(signatures); + if (dnssecUnverifiedReasons == null) { + this.dnssecUnverifiedReasons = Collections.emptySet(); + } else { + this.dnssecUnverifiedReasons = Collections.unmodifiableSet(dnssecUnverifiedReasons); + } + } + + public boolean isAuthenticData() { + return dnssecUnverifiedReasons.isEmpty(); + } + + public Set> getSignatures() { + return signatures; + } + + public Set getUnverifiedReasons() { + return dnssecUnverifiedReasons; + } + +} diff --git a/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java b/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java new file mode 100644 index 000000000..f0987dc8e --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.MiniDnsException; + +public final class DnssecResultNotAuthenticException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final Set unverifiedReasons; + + private DnssecResultNotAuthenticException(String message, Set unverifiedReasons) { + super(message); + if (unverifiedReasons.isEmpty()) { + throw new IllegalArgumentException(); + } + this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons); + } + + public static DnssecResultNotAuthenticException from(Set unverifiedReasons) { + StringBuilder sb = new StringBuilder(); + sb.append("DNSSEC result not authentic. Reasons: "); + for (DnssecUnverifiedReason reason : unverifiedReasons) { + sb.append(reason).append('.'); + } + + return new DnssecResultNotAuthenticException(sb.toString(), unverifiedReasons); + } + + public Set getUnverifiedReasons() { + return unverifiedReasons; + } +} diff --git a/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java b/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java new file mode 100644 index 000000000..3b436882b --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java @@ -0,0 +1,175 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import java.util.Collections; +import java.util.List; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.record.DNSKEY; +import org.minidns.record.Data; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; +import org.minidns.record.Record.TYPE; + +public abstract class DnssecUnverifiedReason { + public abstract String getReasonString(); + + @Override + public String toString() { + return getReasonString(); + } + + @Override + public int hashCode() { + return getReasonString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof DnssecUnverifiedReason && ((DnssecUnverifiedReason) obj).getReasonString().equals(getReasonString()); + } + + public static class AlgorithmNotSupportedReason extends DnssecUnverifiedReason { + private final String algorithm; + private final TYPE type; + private final Record record; + + public AlgorithmNotSupportedReason(byte algorithm, TYPE type, Record record) { + this.algorithm = Integer.toString(algorithm & 0xff); + this.type = type; + this.record = record; + } + + @Override + public String getReasonString() { + return type.name() + " algorithm " + algorithm + " required to verify " + record.name + " is unknown or not supported by platform"; + } + } + + public static class AlgorithmExceptionThrownReason extends DnssecUnverifiedReason { + private final int algorithmNumber; + private final String kind; + private final Exception reason; + private final Record record; + + public AlgorithmExceptionThrownReason(DigestAlgorithm algorithm, String kind, Record record, Exception reason) { + this.algorithmNumber = algorithm.value; + this.kind = kind; + this.record = record; + this.reason = reason; + } + + @Override + public String getReasonString() { + return kind + " algorithm " + algorithmNumber + " threw exception while verifying " + record.name + ": " + reason; + } + } + + public static class ConflictsWithSep extends DnssecUnverifiedReason { + private final Record record; + + public ConflictsWithSep(Record record) { + this.record = record; + } + + @Override + public String getReasonString() { + return "Zone " + record.name.ace + " is in list of known SEPs, but DNSKEY from response mismatches!"; + } + } + + public static class NoTrustAnchorReason extends DnssecUnverifiedReason { + private final DnsName zone; + + public NoTrustAnchorReason(DnsName zone) { + this.zone = zone; + } + + @Override + public String getReasonString() { + return "No trust anchor was found for zone " + zone + ". Try enabling DLV"; + } + } + + public static class NoSecureEntryPointReason extends DnssecUnverifiedReason { + private final DnsName zone; + + public NoSecureEntryPointReason(DnsName zone) { + this.zone = zone; + } + + @Override + public String getReasonString() { + return "No secure entry point was found for zone " + zone; + } + } + + public static class NoRootSecureEntryPointReason extends DnssecUnverifiedReason { + public NoRootSecureEntryPointReason() { + } + + @Override + public String getReasonString() { + return "No secure entry point was found for the root zone (\"Did you forget to configure a root SEP?\")"; + } + } + + public static class NoSignaturesReason extends DnssecUnverifiedReason { + private final Question question; + + public NoSignaturesReason(Question question) { + this.question = question; + } + + @Override + public String getReasonString() { + return "No signatures were attached to answer on question for " + question.type + " at " + question.name; + } + } + + public static class NoActiveSignaturesReason extends DnssecUnverifiedReason { + private final Question question; + private final List outdatedRrSigs; + + public NoActiveSignaturesReason(Question question, List outdatedRrSigs) { + this.question = question; + assert !outdatedRrSigs.isEmpty(); + this.outdatedRrSigs = Collections.unmodifiableList(outdatedRrSigs); + } + + @Override + public String getReasonString() { + return "No currently active signatures were attached to answer on question for " + question.type + " at " + question.name; + } + + public List getOutdatedRrSigs() { + return outdatedRrSigs; + } + } + + public static class NSECDoesNotMatchReason extends DnssecUnverifiedReason { + private final Question question; + private final Record record; + + public NSECDoesNotMatchReason(Question question, Record record) { + this.question = question; + this.record = record; + } + + @Override + public String getReasonString() { + return "NSEC " + record.name + " does nat match question for " + question.type + " at " + question.name; + } + } +} diff --git a/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java b/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java new file mode 100644 index 000000000..5cc230d12 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java @@ -0,0 +1,153 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.record.Data; +import org.minidns.record.DelegatingDnssecRR; +import org.minidns.record.Record; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import java.util.Locale; + +public class DnssecValidationFailedException extends IOException { + private static final long serialVersionUID = 5413184667629832742L; + + public DnssecValidationFailedException(Question question, String reason) { + super("Validation of request to " + question + " failed: " + reason); + } + + public DnssecValidationFailedException(String message) { + super(message); + } + + public DnssecValidationFailedException(String message, Throwable cause) { + super(message, cause); + } + + public DnssecValidationFailedException(Record record, String reason) { + super("Validation of record " + record + " failed: " + reason); + } + + public DnssecValidationFailedException(List> records, String reason) { + super("Validation of " + records.size() + " " + records.get(0).type + " record" + (records.size() > 1 ? "s" : "") + " failed: " + reason); + } + + public static class DataMalformedException extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final byte[] data; + + public DataMalformedException(IOException exception, byte[] data) { + super("Malformed data", exception); + this.data = data; + } + + public DataMalformedException(String message, IOException exception, byte[] data) { + super(message, exception); + this.data = data; + } + + public byte[] getData() { + return data; + } + } + + public static class DnssecInvalidKeySpecException extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public DnssecInvalidKeySpecException(InvalidKeySpecException exception) { + super("Invalid key spec", exception); + } + + public DnssecInvalidKeySpecException(String message, InvalidKeySpecException exception, byte[] data) { + super(message, exception); + } + + } + + public static class AuthorityDoesNotContainSoa extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage response; + + public AuthorityDoesNotContainSoa(DnsMessage response) { + super("Autority does not contain SOA"); + this.response = response; + } + + public DnsMessage getResponse() { + return response; + } + } + + public static final class DigestComparisonFailedException extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final Record record; + private final DelegatingDnssecRR ds; + private final byte[] digest; + private final String digestHex; + + private DigestComparisonFailedException(String message, Record record, DelegatingDnssecRR ds, byte[] digest, String digestHex) { + super(message); + this.record = record; + this.ds = ds; + this.digest = digest; + this.digestHex = digestHex; + } + + public Record getRecord() { + return record; + } + + public DelegatingDnssecRR getDelegaticDnssecRr() { + return ds; + } + + public byte[] getDigest() { + return digest.clone(); + } + + public String getDigestHex() { + return digestHex; + } + + public static DigestComparisonFailedException from(Record record, DelegatingDnssecRR ds, byte[] digest) { + BigInteger digestBigInteger = new BigInteger(1, digest); + String digestHex = digestBigInteger.toString(16).toUpperCase(Locale.ROOT); + + String message = "Digest for " + record + " does not match. Digest of delegating DNSSEC RR " + ds + " is '" + + ds.getDigestHex() + "' while we calculated '" + digestHex + "'"; + return new DigestComparisonFailedException(message, record, ds, digest, digestHex); + } + } +} diff --git a/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java b/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java new file mode 100644 index 000000000..9119edd8f --- /dev/null +++ b/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +public class DnssecValidatorInitializationException extends RuntimeException { + private static final long serialVersionUID = -1464257268053507791L; + + public DnssecValidatorInitializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/minidns/dnssec/SignatureVerifier.java b/src/main/java/org/minidns/dnssec/SignatureVerifier.java new file mode 100644 index 000000000..eadbf36cc --- /dev/null +++ b/src/main/java/org/minidns/dnssec/SignatureVerifier.java @@ -0,0 +1,18 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; + +public interface SignatureVerifier { + boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException; +} diff --git a/src/main/java/org/minidns/dnssec/Verifier.java b/src/main/java/org/minidns/dnssec/Verifier.java new file mode 100644 index 000000000..9dc2b2834 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/Verifier.java @@ -0,0 +1,219 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.dnslabel.DnsLabel; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmExceptionThrownReason; +import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmNotSupportedReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NSECDoesNotMatchReason; +import org.minidns.dnssec.DnssecValidationFailedException.DigestComparisonFailedException; +import org.minidns.dnssec.algorithms.AlgorithmMap; +import org.minidns.record.DNSKEY; +import org.minidns.record.Data; +import org.minidns.record.DelegatingDnssecRR; +import org.minidns.record.NSEC; +import org.minidns.record.NSEC3; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; +import org.minidns.util.Base32; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class Verifier { + private static final AlgorithmMap algorithmMap = AlgorithmMap.INSTANCE; + + public static DnssecUnverifiedReason verify(Record dnskeyRecord, DelegatingDnssecRR ds) throws DnssecValidationFailedException { + DNSKEY dnskey = dnskeyRecord.payloadData; + DigestCalculator digestCalculator = algorithmMap.getDsDigestCalculator(ds.digestType); + if (digestCalculator == null) { + return new AlgorithmNotSupportedReason(ds.digestTypeByte, ds.getType(), dnskeyRecord); + } + + byte[] dnskeyData = dnskey.toByteArray(); + byte[] dnskeyOwner = dnskeyRecord.name.getBytes(); + byte[] combined = new byte[dnskeyOwner.length + dnskeyData.length]; + System.arraycopy(dnskeyOwner, 0, combined, 0, dnskeyOwner.length); + System.arraycopy(dnskeyData, 0, combined, dnskeyOwner.length, dnskeyData.length); + byte[] digest; + try { + digest = digestCalculator.digest(combined); + } catch (Exception e) { + return new AlgorithmExceptionThrownReason(ds.digestType, "DS", dnskeyRecord, e); + } + + if (!ds.digestEquals(digest)) { + throw DigestComparisonFailedException.from(dnskeyRecord, ds, digest); + } + return null; + } + + public static DnssecUnverifiedReason verify(List> records, RRSIG rrsig, DNSKEY key) throws IOException { + SignatureVerifier signatureVerifier = algorithmMap.getSignatureVerifier(rrsig.algorithm); + if (signatureVerifier == null) { + return new AlgorithmNotSupportedReason(rrsig.algorithmByte, rrsig.getType(), records.get(0)); + } + + byte[] combine = combine(rrsig, records); + if (signatureVerifier.verify(combine, rrsig, key)) { + return null; + } else { + throw new DnssecValidationFailedException(records, "Signature is invalid."); + } + } + + public static DnssecUnverifiedReason verifyNsec(Record nsecRecord, Question q) { + NSEC nsec = nsecRecord.payloadData; + if (nsecRecord.name.equals(q.name) && !nsec.types.contains(q.type)) { + // records with same name but different types exist + return null; + } else if (nsecMatches(q.name, nsecRecord.name, nsec.next)) { + return null; + } + return new NSECDoesNotMatchReason(q, nsecRecord); + } + + public static DnssecUnverifiedReason verifyNsec3(DnsName zone, Record nsec3record, Question q) { + NSEC3 nsec3 = nsec3record.payloadData; + DigestCalculator digestCalculator = algorithmMap.getNsecDigestCalculator(nsec3.hashAlgorithm); + if (digestCalculator == null) { + return new AlgorithmNotSupportedReason(nsec3.hashAlgorithmByte, nsec3.getType(), nsec3record); + } + + byte[] bytes = nsec3hash(digestCalculator, nsec3, q.name, nsec3.iterations); + String s = Base32.encodeToString(bytes); + DnsName computedNsec3Record = DnsName.from(s + "." + zone); + if (nsec3record.name.equals(computedNsec3Record)) { + if (nsec3.types.contains(q.type)) { + // TODO: Refine exception thrown in this case. + return new NSECDoesNotMatchReason(q, nsec3record); + } + return null; + } + if (nsecMatches(s, nsec3record.name.getHostpart(), Base32.encodeToString(nsec3.getNextHashed()))) { + return null; + } + return new NSECDoesNotMatchReason(q, nsec3record); + } + + static byte[] combine(RRSIG rrsig, List> records) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + // Write RRSIG without signature + try { + rrsig.writePartialSignature(dos); + + DnsName sigName = records.get(0).name; + if (!sigName.isRootLabel()) { + if (sigName.getLabelCount() < rrsig.labels) { + // TODO: This is currently not covered by the unit tests. + throw new DnssecValidationFailedException("Invalid RRsig record"); + } + + if (sigName.getLabelCount() > rrsig.labels) { + // TODO: This is currently not covered by the unit tests. + // Expand wildcards + sigName = DnsName.from(DnsLabel.WILDCARD_LABEL, sigName.stripToLabels(rrsig.labels)); + } + } + + List recordBytes = new ArrayList<>(records.size()); + for (Record record : records) { + Record ref = new Record(sigName, record.type, record.clazzValue, rrsig.originalTtl, record.payloadData); + recordBytes.add(ref.toByteArray()); + } + + // Sort correctly (cause they might be ordered randomly) as per RFC 4034 § 6.3. + final int offset = sigName.size() + 10; // Where the RDATA begins + Collections.sort(recordBytes, new Comparator() { + @Override + public int compare(byte[] b1, byte[] b2) { + for (int i = offset; i < b1.length && i < b2.length; i++) { + if (b1[i] != b2[i]) { + return (b1[i] & 0xFF) - (b2[i] & 0xFF); + } + } + return b1.length - b2.length; + } + }); + + for (byte[] recordByte : recordBytes) { + dos.write(recordByte); + } + dos.flush(); + } catch (IOException e) { + // Never happens + throw new RuntimeException(e); + } + return bos.toByteArray(); + } + + static boolean nsecMatches(String test, String lowerBound, String upperBound) { + return nsecMatches(DnsName.from(test), DnsName.from(lowerBound), DnsName.from(upperBound)); + } + + /** + * Tests if a nsec domain name is part of an NSEC record. + * + * @param test test domain name + * @param lowerBound inclusive lower bound + * @param upperBound exclusive upper bound + * @return test domain name is covered by NSEC record + */ + static boolean nsecMatches(DnsName test, DnsName lowerBound, DnsName upperBound) { + int lowerParts = lowerBound.getLabelCount(); + int upperParts = upperBound.getLabelCount(); + int testParts = test.getLabelCount(); + + if (testParts > lowerParts && !test.isChildOf(lowerBound) && test.stripToLabels(lowerParts).compareTo(lowerBound) < 0) + return false; + if (testParts <= lowerParts && test.compareTo(lowerBound.stripToLabels(testParts)) < 0) + return false; + + if (testParts > upperParts && !test.isChildOf(upperBound) && test.stripToLabels(upperParts).compareTo(upperBound) > 0) + return false; + if (testParts <= upperParts && test.compareTo(upperBound.stripToLabels(testParts)) >= 0) + return false; + + return true; + } + + static byte[] nsec3hash(DigestCalculator digestCalculator, NSEC3 nsec3, DnsName ownerName, int iterations) { + return nsec3hash(digestCalculator, nsec3.getSalt(), ownerName.getBytes(), iterations); + } + + /** + * Derived from RFC 5155 Section 5. + * + * @param digestCalculator the digest calculator. + * @param salt the salt. + * @param data the data. + * @param iterations the number of iterations. + * @return the NSEC3 hash. + */ + static byte[] nsec3hash(DigestCalculator digestCalculator, byte[] salt, byte[] data, int iterations) { + while (iterations-- >= 0) { + byte[] combined = new byte[data.length + salt.length]; + System.arraycopy(data, 0, combined, 0, data.length); + System.arraycopy(salt, 0, combined, data.length, salt.length); + data = digestCalculator.digest(combined); + } + return data; + } +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java b/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java new file mode 100644 index 000000000..4aa10ded5 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.dnssec.DnssecValidatorInitializationException; +import org.minidns.dnssec.DigestCalculator; +import org.minidns.dnssec.SignatureVerifier; +import org.minidns.record.NSEC3.HashAlgorithm; + +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class AlgorithmMap { + private Logger LOGGER = Logger.getLogger(AlgorithmMap.class.getName()); + + public static final AlgorithmMap INSTANCE = new AlgorithmMap(); + + private final Map dsDigestMap = new HashMap<>(); + private final Map signatureMap = new HashMap<>(); + private final Map nsecDigestMap = new HashMap<>(); + + @SuppressWarnings("deprecation") + private AlgorithmMap() { + try { + dsDigestMap.put(DigestAlgorithm.SHA1, new JavaSecDigestCalculator("SHA-1")); + nsecDigestMap.put(HashAlgorithm.SHA1, new JavaSecDigestCalculator("SHA-1")); + } catch (NoSuchAlgorithmException e) { + // SHA-1 is MANDATORY + throw new DnssecValidatorInitializationException("SHA-1 is mandatory", e); + } + try { + dsDigestMap.put(DigestAlgorithm.SHA256, new JavaSecDigestCalculator("SHA-256")); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is MANDATORY + throw new DnssecValidatorInitializationException("SHA-256 is mandatory", e); + } + + try { + dsDigestMap.put(DigestAlgorithm.SHA384, new JavaSecDigestCalculator("SHA-384")); + } catch (NoSuchAlgorithmException e) { + // SHA-384 is OPTIONAL + LOGGER.log(Level.FINE, "Platform does not support SHA-384", e); + } + + try { + signatureMap.put(SignatureAlgorithm.RSAMD5, new RsaSignatureVerifier("MD5withRSA")); + } catch (NoSuchAlgorithmException e) { + // RSA/MD5 is DEPRECATED + LOGGER.log(Level.FINER, "Platform does not support RSA/MD5", e); + } + try { + DsaSignatureVerifier sha1withDSA = new DsaSignatureVerifier("SHA1withDSA"); + signatureMap.put(SignatureAlgorithm.DSA, sha1withDSA); + signatureMap.put(SignatureAlgorithm.DSA_NSEC3_SHA1, sha1withDSA); + } catch (NoSuchAlgorithmException e) { + // DSA/SHA-1 is OPTIONAL + LOGGER.log(Level.FINE, "Platform does not support DSA/SHA-1", e); + } + try { + RsaSignatureVerifier sha1withRSA = new RsaSignatureVerifier("SHA1withRSA"); + signatureMap.put(SignatureAlgorithm.RSASHA1, sha1withRSA); + signatureMap.put(SignatureAlgorithm.RSASHA1_NSEC3_SHA1, sha1withRSA); + } catch (NoSuchAlgorithmException e) { + throw new DnssecValidatorInitializationException("Platform does not support RSA/SHA-1", e); + } + try { + signatureMap.put(SignatureAlgorithm.RSASHA256, new RsaSignatureVerifier("SHA256withRSA")); + } catch (NoSuchAlgorithmException e) { + // RSA/SHA-256 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support RSA/SHA-256", e); + } + try { + signatureMap.put(SignatureAlgorithm.RSASHA512, new RsaSignatureVerifier("SHA512withRSA")); + } catch (NoSuchAlgorithmException e) { + // RSA/SHA-512 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support RSA/SHA-512", e); + } + try { + signatureMap.put(SignatureAlgorithm.ECC_GOST, new EcgostSignatureVerifier()); + } catch (NoSuchAlgorithmException e) { + // GOST R 34.10-2001 is OPTIONAL + LOGGER.log(Level.FINE, "Platform does not support GOST R 34.10-2001", e); + } + try { + signatureMap.put(SignatureAlgorithm.ECDSAP256SHA256, new EcdsaSignatureVerifier.P256SHA256()); + } catch (NoSuchAlgorithmException e) { + // ECDSA/SHA-256 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support ECDSA/SHA-256", e); + } + try { + signatureMap.put(SignatureAlgorithm.ECDSAP384SHA384, new EcdsaSignatureVerifier.P384SHA284()); + } catch (NoSuchAlgorithmException e) { + // ECDSA/SHA-384 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support ECDSA/SHA-384", e); + } + } + + public DigestCalculator getDsDigestCalculator(DigestAlgorithm algorithm) { + return dsDigestMap.get(algorithm); + } + + public SignatureVerifier getSignatureVerifier(SignatureAlgorithm algorithm) { + return signatureMap.get(algorithm); + } + + public DigestCalculator getNsecDigestCalculator(HashAlgorithm algorithm) { + return nsecDigestMap.get(algorithm); + } +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java b/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java new file mode 100644 index 000000000..6267ebabe --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java @@ -0,0 +1,132 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; + +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +class DsaSignatureVerifier extends JavaSecSignatureVerifier { + private static final int LENGTH = 20; + + DsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException { + super("DSA", algorithm); + } + + @Override + protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException { + DataInput dis = rrsig.getSignatureAsDataInputStream(); + + ByteArrayOutputStream bos; + try { + // Convert RFC 2536 to ASN.1 + @SuppressWarnings("unused") + byte t = dis.readByte(); + + byte[] r = new byte[LENGTH]; + dis.readFully(r); + int roff = 0; + final int rlen; + if (r[0] == 0) { + while (roff < LENGTH && r[roff] == 0) { + roff++; + } + rlen = r.length - roff; + } else if (r[0] < 0) { + rlen = r.length + 1; + } else { + rlen = r.length; + } + + byte[] s = new byte[LENGTH]; + dis.readFully(s); + int soff = 0; + final int slen; + if (s[0] == 0) { + while (soff < LENGTH && s[soff] == 0) { + soff++; + } + slen = s.length - soff; + } else if (s[0] < 0) { + slen = s.length + 1; + } else { + slen = s.length; + } + + bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(0x30); + dos.writeByte(rlen + slen + 4); + + dos.writeByte(0x2); + dos.writeByte(rlen); + if (rlen > LENGTH) + dos.writeByte(0); + dos.write(r, roff, LENGTH - roff); + + dos.writeByte(0x2); + dos.writeByte(slen); + if (slen > LENGTH) + dos.writeByte(0); + dos.write(s, soff, LENGTH - soff); + } catch (IOException e) { + throw new DataMalformedException(e, rrsig.getSignature()); + } + + return bos.toByteArray(); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger subPrime, prime, base, pubKey; + + try { + int t = dis.readUnsignedByte(); + + byte[] subPrimeBytes = new byte[LENGTH]; + dis.readFully(subPrimeBytes); + subPrime = new BigInteger(1, subPrimeBytes); + + byte[] primeBytes = new byte[64 + t * 8]; + dis.readFully(primeBytes); + prime = new BigInteger(1, primeBytes); + + byte[] baseBytes = new byte[64 + t * 8]; + dis.readFully(baseBytes); + base = new BigInteger(1, baseBytes); + + byte[] pubKeyBytes = new byte[64 + t * 8]; + dis.readFully(pubKeyBytes); + pubKey = new BigInteger(1, pubKeyBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new DSAPublicKeySpec(pubKey, prime, subPrime, base)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java b/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java new file mode 100644 index 000000000..751791d74 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; + +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; + +abstract class EcdsaSignatureVerifier extends JavaSecSignatureVerifier { + private final ECParameterSpec spec; + private final int length; + + EcdsaSignatureVerifier(BigInteger[] spec, int length, String algorithm) throws NoSuchAlgorithmException { + this(new ECParameterSpec(new EllipticCurve(new ECFieldFp(spec[0]), spec[1], spec[2]), new ECPoint(spec[3], spec[4]), spec[5], 1), length, algorithm); + } + + EcdsaSignatureVerifier(ECParameterSpec spec, int length, String algorithm) throws NoSuchAlgorithmException { + super("EC", algorithm); + this.length = length; + this.spec = spec; + } + + @Override + protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException { + DataInput dis = rrsig.getSignatureAsDataInputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + try { + byte[] r = new byte[length]; + dis.readFully(r); + int rlen = (r[0] < 0) ? length + 1 : length; + + byte[] s = new byte[length]; + dis.readFully(s); + int slen = (s[0] < 0) ? length + 1 : length; + + dos.writeByte(0x30); + dos.writeByte(rlen + slen + 4); + + dos.writeByte(0x2); + dos.writeByte(rlen); + if (rlen > length) dos.writeByte(0); + dos.write(r); + + dos.writeByte(0x2); + dos.writeByte(slen); + if (slen > length) dos.writeByte(0); + dos.write(s); + } catch (IOException e) { + throw new DataMalformedException(e, rrsig.getSignature()); + } + + return bos.toByteArray(); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger x, y; + + try { + byte[] xBytes = new byte[length]; + dis.readFully(xBytes); + x = new BigInteger(1, xBytes); + + byte[] yBytes = new byte[length]; + dis.readFully(yBytes); + y = new BigInteger(1, yBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), spec)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } + + public static class P256SHA256 extends EcdsaSignatureVerifier { + private static BigInteger[] SPEC = { + new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16), + new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16), + new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16), + new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16), + new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16), + new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16) + }; + + P256SHA256() throws NoSuchAlgorithmException { + super(SPEC, 32, "SHA256withECDSA"); + } + } + + public static class P384SHA284 extends EcdsaSignatureVerifier { + private static BigInteger[] SPEC = { + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16), + new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16), + new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16), + new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16) + }; + + P384SHA284() throws NoSuchAlgorithmException { + super(SPEC, 48, "SHA384withECDSA"); + } + } +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java b/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java new file mode 100644 index 000000000..a301b5dda --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; + +import java.io.DataInput; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; + +class EcgostSignatureVerifier extends JavaSecSignatureVerifier { + private static final int LENGTH = 32; + private static final ECParameterSpec SPEC = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD94", 16), + new BigInteger("A6", 16) + ), + new ECPoint(BigInteger.ONE, new BigInteger("8D91E471E0989CDA27DF505A453F2B7635294F2DDF23E3B122ACC99C9E9F1E14", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C611070995AD10045841B09B761B893", 16), + 1 + ); + + EcgostSignatureVerifier() throws NoSuchAlgorithmException { + super("ECGOST3410", "GOST3411withECGOST3410"); + } + + @Override + protected byte[] getSignature(RRSIG rrsig) { + return rrsig.getSignature(); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger x, y; + + try { + byte[] xBytes = new byte[LENGTH]; + dis.readFully(xBytes); + reverse(xBytes); + x = new BigInteger(1, xBytes); + + byte[] yBytes = new byte[LENGTH]; + dis.readFully(yBytes); + reverse(yBytes); + y = new BigInteger(1, yBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), SPEC)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } + + private static void reverse(byte[] array) { + for (int i = 0; i < array.length / 2; i++) { + int j = array.length - i - 1; + byte tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + } +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java b/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java new file mode 100644 index 000000000..d2154650e --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DigestCalculator; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class JavaSecDigestCalculator implements DigestCalculator { + private MessageDigest md; + + public JavaSecDigestCalculator(String algorithm) throws NoSuchAlgorithmException { + md = MessageDigest.getInstance(algorithm); + } + + @Override + public byte[] digest(byte[] bytes) { + return md.digest(bytes); + } +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java b/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java new file mode 100644 index 000000000..3c15afa50 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.dnssec.SignatureVerifier; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +public abstract class JavaSecSignatureVerifier implements SignatureVerifier { + private final KeyFactory keyFactory; + private final String signatureAlgorithm; + + public JavaSecSignatureVerifier(String keyAlgorithm, String signatureAlgorithm) throws NoSuchAlgorithmException { + keyFactory = KeyFactory.getInstance(keyAlgorithm); + this.signatureAlgorithm = signatureAlgorithm; + + // Verify signature algorithm to be valid + Signature.getInstance(signatureAlgorithm); + } + + public KeyFactory getKeyFactory() { + return keyFactory; + } + + @Override + public boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException { + try { + PublicKey publicKey = getPublicKey(key); + Signature signature = Signature.getInstance(signatureAlgorithm); + signature.initVerify(publicKey); + signature.update(content); + return signature.verify(getSignature(rrsig)); + } catch (NoSuchAlgorithmException e) { + // We checked against this before, it should never happen! + throw new AssertionError(e); + } catch (InvalidKeyException | SignatureException | ArithmeticException e) { + throw new DnssecValidationFailedException("Validating signature failed", e); + } + } + + protected abstract byte[] getSignature(RRSIG rrsig) throws DataMalformedException; + + protected abstract PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException; +} diff --git a/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java b/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java new file mode 100644 index 000000000..e079b4dd9 --- /dev/null +++ b/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; + +import java.io.DataInput; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; + +class RsaSignatureVerifier extends JavaSecSignatureVerifier { + RsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException { + super("RSA", algorithm); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger exponent, modulus; + + try { + int exponentLength = dis.readUnsignedByte(); + int bytesRead = 1; + if (exponentLength == 0) { + bytesRead += 2; + exponentLength = dis.readUnsignedShort(); + } + + byte[] exponentBytes = new byte[exponentLength]; + dis.readFully(exponentBytes); + bytesRead += exponentLength; + exponent = new BigInteger(1, exponentBytes); + + byte[] modulusBytes = new byte[key.getKeyLength() - bytesRead]; + dis.readFully(modulusBytes); + modulus = new BigInteger(1, modulusBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new RSAPublicKeySpec(modulus, exponent)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } + + @Override + protected byte[] getSignature(RRSIG rrsig) { + return rrsig.getSignature(); + } +} diff --git a/src/main/java/org/minidns/dnsserverlookup/AbstractDnsServerLookupMechanism.java b/src/main/java/org/minidns/dnsserverlookup/AbstractDnsServerLookupMechanism.java new file mode 100644 index 000000000..6a123cd66 --- /dev/null +++ b/src/main/java/org/minidns/dnsserverlookup/AbstractDnsServerLookupMechanism.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ + package org.minidns.dnsserverlookup; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; + +public abstract class AbstractDnsServerLookupMechanism implements DnsServerLookupMechanism { + + protected static final Logger LOGGER = Logger.getLogger(AbstractDnsServerLookupMechanism.class.getName()); + + private final String name; + private final int priority; + + protected AbstractDnsServerLookupMechanism(String name, int priority) { + this.name = name; + this.priority = priority; + } + + @Override + public final String getName() { + return name; + } + + @Override + public final int getPriority() { + return priority; + } + + @Override + public final int compareTo(DnsServerLookupMechanism other) { + int myPriority = getPriority(); + int otherPriority = other.getPriority(); + + return Integer.compare(myPriority, otherPriority); + } + + @Override + public abstract List getDnsServerAddresses(); + + protected static List toListOfStrings(Collection inetAddresses) { + List result = new ArrayList<>(inetAddresses.size()); + for (InetAddress inetAddress : inetAddresses) { + String address = inetAddress.getHostAddress(); + result.add(address); + } + return result; + } +} diff --git a/src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java b/src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java new file mode 100644 index 000000000..98773ff4b --- /dev/null +++ b/src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsserverlookup; + +import org.minidns.util.PlatformDetection; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +/** + * Try to retrieve the list of DNS server by executing getprop. + */ +public final class AndroidUsingExec extends AbstractDnsServerLookupMechanism { + + public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingExec(); + public static final int PRIORITY = AndroidUsingReflection.PRIORITY - 1; + + private AndroidUsingExec() { + super(AndroidUsingExec.class.getSimpleName(), PRIORITY); + } + + @Override + public List getDnsServerAddresses() { + try { + Process process = Runtime.getRuntime().exec("getprop"); + InputStream inputStream = process.getInputStream(); + LineNumberReader lnr = new LineNumberReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + Set server = parseProps(lnr, true); + if (server.size() > 0) { + List res = new ArrayList<>(server.size()); + res.addAll(server); + return res; + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e); + } + return null; + } + + @Override + public boolean isAvailable() { + return PlatformDetection.isAndroid(); + } + + private static final String PROP_DELIM = "]: ["; + static Set parseProps(BufferedReader lnr, boolean logWarning) throws UnknownHostException, IOException { + String line = null; + Set server = new HashSet(6); + + while ((line = lnr.readLine()) != null) { + int split = line.indexOf(PROP_DELIM); + if (split == -1) { + continue; + } + String property = line.substring(1, split); + + int valueStart = split + PROP_DELIM.length(); + int valueEnd = line.length() - 1; + if (valueEnd < valueStart) { + // This can happen if a newline sneaks in as the first character of the property value. For example + // "[propName]: [\n…]". + if (logWarning) { + LOGGER.warning("Malformed property detected: \"" + line + '"'); + } + continue; + } + + String value = line.substring(valueStart, valueEnd); + + if (value.isEmpty()) { + continue; + } + + if (property.endsWith(".dns") || property.endsWith(".dns1") || + property.endsWith(".dns2") || property.endsWith(".dns3") || + property.endsWith(".dns4")) { + + // normalize the address + + InetAddress ip = InetAddress.getByName(value); + + if (ip == null) continue; + + value = ip.getHostAddress(); + + if (value == null) continue; + if (value.length() == 0) continue; + + server.add(value); + } + } + + return server; + } +} diff --git a/src/main/java/org/minidns/dnsserverlookup/AndroidUsingReflection.java b/src/main/java/org/minidns/dnsserverlookup/AndroidUsingReflection.java new file mode 100644 index 000000000..7175f8243 --- /dev/null +++ b/src/main/java/org/minidns/dnsserverlookup/AndroidUsingReflection.java @@ -0,0 +1,98 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsserverlookup; + +import org.minidns.util.PlatformDetection; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +/** + * Try to retrieve the list of DNS server by calling SystemProperties. + */ +public class AndroidUsingReflection extends AbstractDnsServerLookupMechanism { + + public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingReflection(); + public static final int PRIORITY = 1000; + + private final Method systemPropertiesGet; + + protected AndroidUsingReflection() { + super(AndroidUsingReflection.class.getSimpleName(), PRIORITY); + Method systemPropertiesGet = null; + if (PlatformDetection.isAndroid()) { + try { + Class SystemProperties = Class.forName("android.os.SystemProperties"); + systemPropertiesGet = SystemProperties.getMethod("get", new Class[] { String.class }); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) { + // This is not unexpected, as newer Android versions do not provide access to it any more. + LOGGER.log(Level.FINE, "Can not get method handle for android.os.SystemProperties.get(String).", e); + } + } + this.systemPropertiesGet = systemPropertiesGet; + } + + @Override + public List getDnsServerAddresses() { + ArrayList servers = new ArrayList(5); + + for (String propKey : new String[] { + "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) { + + String value; + try { + value = (String) systemPropertiesGet.invoke(null, propKey); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e); + return null; + } + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + InetAddress ip; + try { + ip = InetAddress.getByName(value); + } catch (UnknownHostException e) { + LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e); + continue; + } + + if (ip == null) continue; + + value = ip.getHostAddress(); + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + servers.add(value); + } + + if (servers.size() > 0) { + return servers; + } + + return null; + } + + @Override + public boolean isAvailable() { + return systemPropertiesGet != null; + } + +} diff --git a/src/main/java/org/minidns/dnsserverlookup/DnsServerLookupMechanism.java b/src/main/java/org/minidns/dnsserverlookup/DnsServerLookupMechanism.java new file mode 100644 index 000000000..7c78d68fe --- /dev/null +++ b/src/main/java/org/minidns/dnsserverlookup/DnsServerLookupMechanism.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsserverlookup; + +import java.util.List; + +public interface DnsServerLookupMechanism extends Comparable { + + String getName(); + + int getPriority(); + + boolean isAvailable(); + + /** + * Returns a List of String representing ideally IP addresses. The list must be modifiable. + *

+ * Note that the lookup mechanisms are not required to assure that only IP addresses are returned. This verification is performed in + * when using {@link org.minidns.DnsClient#findDNS()}. + *

+ * + * @return a List of Strings presenting hopefully IP addresses. + */ + List getDnsServerAddresses(); + +} diff --git a/src/main/java/org/minidns/dnsserverlookup/UnixUsingEtcResolvConf.java b/src/main/java/org/minidns/dnsserverlookup/UnixUsingEtcResolvConf.java new file mode 100644 index 000000000..4332b1289 --- /dev/null +++ b/src/main/java/org/minidns/dnsserverlookup/UnixUsingEtcResolvConf.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsserverlookup; + +import org.minidns.util.PlatformDetection; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class UnixUsingEtcResolvConf extends AbstractDnsServerLookupMechanism { + + public static final DnsServerLookupMechanism INSTANCE = new UnixUsingEtcResolvConf(); + public static final int PRIORITY = 2000; + + private static final Logger LOGGER = Logger.getLogger(UnixUsingEtcResolvConf.class.getName()); + + private static final String RESOLV_CONF_FILE = "/etc/resolv.conf"; + private static final Pattern NAMESERVER_PATTERN = Pattern.compile("^nameserver\\s+(.*)$"); + + private static List cached; + private static long lastModified; + + private UnixUsingEtcResolvConf() { + super(UnixUsingEtcResolvConf.class.getSimpleName(), PRIORITY); + } + + @Override + public List getDnsServerAddresses() { + File file = new File(RESOLV_CONF_FILE); + if (!file.exists()) { + // Not very unixoid systems + return null; + } + + long currentLastModified = file.lastModified(); + if (currentLastModified == lastModified && cached != null) { + return cached; + } + + List servers = new ArrayList<>(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + Matcher matcher = NAMESERVER_PATTERN.matcher(line); + if (matcher.matches()) { + servers.add(matcher.group(1).trim()); + } + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not read from " + RESOLV_CONF_FILE, e); + return null; + } finally { + if (reader != null) try { + reader.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not close reader", e); + } + } + + if (servers.isEmpty()) { + LOGGER.fine("Could not find any nameservers in " + RESOLV_CONF_FILE); + return null; + } + + cached = servers; + lastModified = currentLastModified; + + return cached; + } + + @Override + public boolean isAvailable() { + if (PlatformDetection.isAndroid()) { + // Don't rely on resolv.conf when on Android + return false; + } + + File file = new File(RESOLV_CONF_FILE); + + boolean resolvConfFileExists; + try { + resolvConfFileExists = file.exists(); + } catch (SecurityException securityException) { + LOGGER.log(Level.FINE, "Access to /etc/resolv.conf not possible", securityException); + return false; + } + return resolvConfFileExists; + } + +} diff --git a/src/main/java/org/minidns/dnsserverlookup/android21/AndroidUsingLinkProperties.java b/src/main/java/org/minidns/dnsserverlookup/android21/AndroidUsingLinkProperties.java new file mode 100644 index 000000000..af16cfebf --- /dev/null +++ b/src/main/java/org/minidns/dnsserverlookup/android21/AndroidUsingLinkProperties.java @@ -0,0 +1,135 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnsserverlookup.android21; + +import android.annotation.TargetApi; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.RouteInfo; +import android.os.Build; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +import org.minidns.DnsClient; +import org.minidns.dnsserverlookup.AbstractDnsServerLookupMechanism; +import org.minidns.dnsserverlookup.AndroidUsingExec; + +/** + * A DNS server lookup mechanism using Android's Link Properties method available on Android API 21 or higher. Use + * {@link #setup(Context)} to setup this mechanism. + *

+ * Requires the ACCESS_NETWORK_STATE permission. + *

+ */ +public class AndroidUsingLinkProperties extends AbstractDnsServerLookupMechanism { + + private final ConnectivityManager connectivityManager; + + /** + * Setup this DNS server lookup mechanism. You need to invoke this method only once, ideally before you do your + * first DNS lookup. + * + * @param context a Context instance. + * @return the instance of the newly setup mechanism + */ + public static AndroidUsingLinkProperties setup(Context context) { + AndroidUsingLinkProperties androidUsingLinkProperties = new AndroidUsingLinkProperties(context); + DnsClient.addDnsServerLookupMechanism(androidUsingLinkProperties); + return androidUsingLinkProperties; + } + + /** + * Construct this DNS server lookup mechanism. + * + * @param context an Android context. + */ + public AndroidUsingLinkProperties(Context context) { + super(AndroidUsingLinkProperties.class.getSimpleName(), AndroidUsingExec.PRIORITY - 1); + connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + @Override + public boolean isAvailable() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + @TargetApi(Build.VERSION_CODES.M) + private List getDnsServerAddressesOfActiveNetwork() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return null; + } + + // ConnectivityManager.getActiveNetwork() is API 23. + Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + return null; + } + + LinkProperties linkProperties = connectivityManager.getLinkProperties(activeNetwork); + if (linkProperties == null) { + return null; + } + + List dnsServers = linkProperties.getDnsServers(); + return toListOfStrings(dnsServers); + } + + @Override + @TargetApi(21) + public List getDnsServerAddresses() { + // First, try the API 23 approach using ConnectivityManager.getActiveNetwork(). + List servers = getDnsServerAddressesOfActiveNetwork(); + if (servers != null) { + return servers; + } + + Network[] networks = connectivityManager.getAllNetworks(); + if (networks == null) { + return null; + } + + servers = new ArrayList<>(networks.length * 2); + for (Network network : networks) { + LinkProperties linkProperties = connectivityManager.getLinkProperties(network); + if (linkProperties == null) { + continue; + } + + // Prioritize the DNS servers of links which have a default route + if (hasDefaultRoute(linkProperties)) { + servers.addAll(0, toListOfStrings(linkProperties.getDnsServers())); + } else { + servers.addAll(toListOfStrings(linkProperties.getDnsServers())); + } + } + + if (servers.isEmpty()) { + return null; + } + + return servers; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static boolean hasDefaultRoute(LinkProperties linkProperties) { + for (RouteInfo route : linkProperties.getRoutes()) { + if (route.isDefaultRoute()) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/org/minidns/edns/Edns.java b/src/main/java/org/minidns/edns/Edns.java new file mode 100644 index 000000000..e3306fcc0 --- /dev/null +++ b/src/main/java/org/minidns/edns/Edns.java @@ -0,0 +1,235 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.edns; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Data; +import org.minidns.record.OPT; +import org.minidns.record.Record; +import org.minidns.record.Record.TYPE; + +/** + * EDNS - Extension Mechanism for DNS. + * + * @see RFC 6891 - Extension Mechanisms for DNS (EDNS(0)) + * + */ +public class Edns { + + /** + * Inform the dns server that the client supports DNSSEC. + */ + public static final int FLAG_DNSSEC_OK = 0x8000; + + /** + * The EDNS option code. + * + * @see IANA - DNS EDNS0 Option Codes (OPT) + */ + public enum OptionCode { + UNKNOWN(-1, UnknownEdnsOption.class), + NSID(3, Nsid.class), + ; + + private static Map INVERSE_LUT = new HashMap<>(OptionCode.values().length); + + static { + for (OptionCode optionCode : OptionCode.values()) { + INVERSE_LUT.put(optionCode.asInt, optionCode); + } + } + + public final int asInt; + public final Class clazz; + + OptionCode(int optionCode, Class clazz) { + this.asInt = optionCode; + this.clazz = clazz; + } + + public static OptionCode from(int optionCode) { + OptionCode res = INVERSE_LUT.get(optionCode); + if (res == null) res = OptionCode.UNKNOWN; + return res; + } + } + + public final int udpPayloadSize; + + /** + * 8-bit extended return code. + * + * RFC 6891 § 6.1.3 EXTENDED-RCODE + */ + public final int extendedRcode; + + /** + * 8-bit version field. + * + * RFC 6891 § 6.1.3 VERSION + */ + public final int version; + + /** + * 16-bit flags. + * + * RFC 6891 § 6.1.4 + */ + public final int flags; + + public final List variablePart; + + public final boolean dnssecOk; + + private Record optRecord; + + public Edns(Record optRecord) { + assert optRecord.type == TYPE.OPT; + udpPayloadSize = optRecord.clazzValue; + extendedRcode = (int) ((optRecord.ttl >> 8) & 0xff); + version = (int) ((optRecord.ttl >> 16) & 0xff); + flags = (int) optRecord.ttl & 0xffff; + + dnssecOk = (optRecord.ttl & FLAG_DNSSEC_OK) > 0; + + OPT opt = optRecord.payloadData; + variablePart = opt.variablePart; + this.optRecord = optRecord; + } + + public Edns(Builder builder) { + udpPayloadSize = builder.udpPayloadSize; + extendedRcode = builder.extendedRcode; + version = builder.version; + int flags = 0; + if (builder.dnssecOk) { + flags |= FLAG_DNSSEC_OK; + } + dnssecOk = builder.dnssecOk; + this.flags = flags; + if (builder.variablePart != null) { + variablePart = builder.variablePart; + } else { + variablePart = Collections.emptyList(); + } + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + public O getEdnsOption(OptionCode optionCode) { + for (EdnsOption o : variablePart) { + if (o.getOptionCode().equals(optionCode)) { + return (O) o; + } + } + return null; + } + + public Record asRecord() { + if (optRecord == null) { + long optFlags = flags; + optFlags |= extendedRcode << 8; + optFlags |= version << 16; + optRecord = new Record(DnsName.ROOT, Record.TYPE.OPT, udpPayloadSize, optFlags, new OPT(variablePart)); + } + return optRecord; + } + + private String terminalOutputCache; + + public String asTerminalOutput() { + if (terminalOutputCache == null) { + StringBuilder sb = new StringBuilder(); + sb.append("EDNS: version: ").append(version).append(", flags:"); + if (dnssecOk) + sb.append(" do"); + sb.append("; udp: ").append(udpPayloadSize); + if (!variablePart.isEmpty()) { + sb.append('\n'); + Iterator it = variablePart.iterator(); + while (it.hasNext()) { + EdnsOption edns = it.next(); + sb.append(edns.getOptionCode()).append(": "); + sb.append(edns.asTerminalOutput()); + if (it.hasNext()) { + sb.append('\n'); + } + } + } + terminalOutputCache = sb.toString(); + } + return terminalOutputCache; + } + + @Override + public String toString() { + return asTerminalOutput(); + } + + public static Edns fromRecord(Record record) { + if (record.type != TYPE.OPT) return null; + + @SuppressWarnings("unchecked") + Record optRecord = (Record) record; + return new Edns(optRecord); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private int udpPayloadSize; + private int extendedRcode; + private int version; + private boolean dnssecOk; + private List variablePart; + + private Builder() { + } + + public Builder setUdpPayloadSize(int udpPayloadSize) { + if (udpPayloadSize > 0xffff) { + throw new IllegalArgumentException("UDP payload size must not be greater than 65536, was " + udpPayloadSize); + } + this.udpPayloadSize = udpPayloadSize; + return this; + } + + public Builder setDnssecOk(boolean dnssecOk) { + this.dnssecOk = dnssecOk; + return this; + } + + public Builder setDnssecOk() { + dnssecOk = true; + return this; + } + + public Builder addEdnsOption(EdnsOption ednsOption) { + if (variablePart == null) { + variablePart = new ArrayList<>(4); + } + variablePart.add(ednsOption); + return this; + } + + public Edns build() { + return new Edns(this); + } + } +} diff --git a/src/main/java/org/minidns/edns/EdnsOption.java b/src/main/java/org/minidns/edns/EdnsOption.java new file mode 100644 index 000000000..135855cf3 --- /dev/null +++ b/src/main/java/org/minidns/edns/EdnsOption.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.edns; + +import java.io.DataOutputStream; +import java.io.IOException; + +import org.minidns.edns.Edns.OptionCode; + +public abstract class EdnsOption { + + public final int optionCode; + public final int optionLength; + + protected final byte[] optionData; + + protected EdnsOption(int optionCode, byte[] optionData) { + this.optionCode = optionCode; + this.optionLength = optionData.length; + this.optionData = optionData; + } + + @SuppressWarnings("this-escape") + protected EdnsOption(byte[] optionData) { + this.optionCode = getOptionCode().asInt; + this.optionLength = optionData.length; + this.optionData = optionData; + } + + public final void writeToDos(DataOutputStream dos) throws IOException { + dos.writeShort(optionCode); + dos.writeShort(optionLength); + dos.write(optionData); + } + + public abstract OptionCode getOptionCode(); + + private String toStringCache; + + @Override + public final String toString() { + if (toStringCache == null) { + toStringCache = toStringInternal().toString(); + } + return toStringCache; + } + + protected abstract CharSequence toStringInternal(); + + private String terminalOutputCache; + + public final String asTerminalOutput() { + if (terminalOutputCache == null) { + terminalOutputCache = asTerminalOutputInternal().toString(); + } + return terminalOutputCache; + } + + protected abstract CharSequence asTerminalOutputInternal(); + + public static EdnsOption parse(int intOptionCode, byte[] optionData) { + OptionCode optionCode = OptionCode.from(intOptionCode); + EdnsOption res; + switch (optionCode) { + case NSID: + res = new Nsid(optionData); + break; + default: + res = new UnknownEdnsOption(intOptionCode, optionData); + break; + } + return res; + } + +} diff --git a/src/main/java/org/minidns/edns/Nsid.java b/src/main/java/org/minidns/edns/Nsid.java new file mode 100644 index 000000000..291f02a18 --- /dev/null +++ b/src/main/java/org/minidns/edns/Nsid.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.edns; + +import java.nio.charset.StandardCharsets; + +import org.minidns.edns.Edns.OptionCode; +import org.minidns.util.Hex; + +public class Nsid extends EdnsOption { + + public static final Nsid REQUEST = new Nsid(); + + private Nsid() { + this(new byte[0]); + } + + public Nsid(byte[] payload) { + super(payload); + } + + @Override + public OptionCode getOptionCode() { + return OptionCode.NSID; + } + + @Override + protected CharSequence toStringInternal() { + String res = OptionCode.NSID + ": "; + res += new String(optionData, StandardCharsets.US_ASCII); + return res; + } + + @Override + protected CharSequence asTerminalOutputInternal() { + return Hex.from(optionData); + } + +} diff --git a/src/main/java/org/minidns/edns/UnknownEdnsOption.java b/src/main/java/org/minidns/edns/UnknownEdnsOption.java new file mode 100644 index 000000000..f65f0b4f7 --- /dev/null +++ b/src/main/java/org/minidns/edns/UnknownEdnsOption.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.edns; + +import org.minidns.edns.Edns.OptionCode; +import org.minidns.util.Hex; + +public class UnknownEdnsOption extends EdnsOption { + + protected UnknownEdnsOption(int optionCode, byte[] optionData) { + super(optionCode, optionData); + } + + @Override + public OptionCode getOptionCode() { + return OptionCode.UNKNOWN; + } + + @Override + protected CharSequence asTerminalOutputInternal() { + return Hex.from(optionData); + } + + @Override + protected CharSequence toStringInternal() { + return asTerminalOutputInternal(); + } + +} diff --git a/src/main/java/org/minidns/hla/DnssecResolverApi.java b/src/main/java/org/minidns/hla/DnssecResolverApi.java new file mode 100644 index 000000000..769607977 --- /dev/null +++ b/src/main/java/org/minidns/hla/DnssecResolverApi.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla; + +import java.io.IOException; +import java.util.Set; + +import org.minidns.DnsCache; +import org.minidns.MiniDnsException.NullResultException; +import org.minidns.cache.LruCache; +import org.minidns.cache.MiniDnsCacheFactory; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.iterative.ReliableDnsClient.Mode; +import org.minidns.record.Data; +import org.minidns.record.Record.TYPE; + +public class DnssecResolverApi extends ResolverApi { + + public static final DnssecResolverApi INSTANCE = new DnssecResolverApi(); + + private final DnssecClient dnssecClient; + private final DnssecClient iterativeOnlyDnssecClient; + private final DnssecClient recursiveOnlyDnssecClient; + + public DnssecResolverApi() { + this(new MiniDnsCacheFactory() { + @Override + public DnsCache newCache() { + return new LruCache(); + } + }); + } + + public DnssecResolverApi(MiniDnsCacheFactory cacheFactory) { + this(new DnssecClient(cacheFactory.newCache()), cacheFactory); + } + + private DnssecResolverApi(DnssecClient dnssecClient, MiniDnsCacheFactory cacheFactory) { + super(dnssecClient); + this.dnssecClient = dnssecClient; + + // Set the *_ONLY_DNSSEC ResolverApi. It is important that the two do *not* share the same cache, since we + // probably fall back to iterativeOnly and in that case do not want the cached results of the recursive result. + iterativeOnlyDnssecClient = new DnssecClient(cacheFactory.newCache()); + iterativeOnlyDnssecClient.setMode(Mode.iterativeOnly); + + recursiveOnlyDnssecClient = new DnssecClient(cacheFactory.newCache()); + recursiveOnlyDnssecClient.setMode(Mode.recursiveOnly); + } + + @Override + public ResolverResult resolve(Question question) throws IOException { + DnssecQueryResult dnssecMessage = dnssecClient.queryDnssec(question); + return toResolverResult(question, dnssecMessage); + } + + /** + * Resolve the given name and type which is expected to yield DNSSEC authenticated results. + * + * @param name the DNS name to resolve. + * @param type the class of the RR type to resolve. + * @param the RR type to resolve. + * @return the resolver result. + * @throws IOException in case an exception happens while resolving. + * @see #resolveDnssecReliable(Question) + */ + public ResolverResult resolveDnssecReliable(String name, Class type) throws IOException { + return resolveDnssecReliable(DnsName.from(name), type); + } + + /** + * Resolve the given name and type which is expected to yield DNSSEC authenticated results. + * + * @param name the DNS name to resolve. + * @param type the class of the RR type to resolve. + * @param the RR type to resolve. + * @return the resolver result. + * @throws IOException in case an exception happens while resolving. + * @see #resolveDnssecReliable(Question) + */ + public ResolverResult resolveDnssecReliable(DnsName name, Class type) throws IOException { + TYPE t = TYPE.getType(type); + Question q = new Question(name, t); + return resolveDnssecReliable(q); + } + + /** + * Resolve the given question which is expected to yield DNSSEC authenticated results. + * + * @param question the question to resolve. + * @param the RR type to resolve. + * @return the resolver result. + * @throws IOException in case an exception happens while resolving. + */ + public ResolverResult resolveDnssecReliable(Question question) throws IOException { + DnssecQueryResult dnssecMessage = recursiveOnlyDnssecClient.queryDnssec(question); + if (dnssecMessage == null || !dnssecMessage.isAuthenticData()) { + dnssecMessage = iterativeOnlyDnssecClient.queryDnssec(question); + } + return toResolverResult(question, dnssecMessage); + } + + public DnssecClient getDnssecClient() { + return dnssecClient; + } + + private static ResolverResult toResolverResult(Question question, DnssecQueryResult dnssecMessage) throws NullResultException { + Set unverifiedReasons = dnssecMessage.getUnverifiedReasons(); + + return new ResolverResult(question, dnssecMessage.dnsQueryResult, unverifiedReasons); + } +} diff --git a/src/main/java/org/minidns/hla/ResolutionUnsuccessfulException.java b/src/main/java/org/minidns/hla/ResolutionUnsuccessfulException.java new file mode 100644 index 000000000..41fae72b3 --- /dev/null +++ b/src/main/java/org/minidns/hla/ResolutionUnsuccessfulException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla; + +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; + +public class ResolutionUnsuccessfulException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final Question question; + public final RESPONSE_CODE responseCode; + + public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) { + super("Asking for " + question + " yielded an error response " + responseCode); + this.question = question; + this.responseCode = responseCode; + } +} diff --git a/src/main/java/org/minidns/hla/ResolverApi.java b/src/main/java/org/minidns/hla/ResolverApi.java new file mode 100644 index 000000000..c1fbb3e50 --- /dev/null +++ b/src/main/java/org/minidns/hla/ResolverApi.java @@ -0,0 +1,222 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; + +import org.minidns.AbstractDnsClient; +import org.minidns.DnsClient; +import org.minidns.dnslabel.DnsLabel; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.hla.srv.SrvProto; +import org.minidns.hla.srv.SrvService; +import org.minidns.hla.srv.SrvServiceProto; +import org.minidns.hla.srv.SrvType; +import org.minidns.iterative.ReliableDnsClient; +import org.minidns.record.Data; +import org.minidns.record.PTR; +import org.minidns.record.SRV; +import org.minidns.record.Record.TYPE; + +/** + * The high-level MiniDNS resolving API. It is designed to be easy to use. + *

+ * A simple exammple how to resolve the IPv4 address of a given domain: + *

+ *
+ * {@code
+ * ResolverResult result = DnssecResolverApi.INSTANCE.resolve("verteiltesysteme.net", A.class);
+ * if (!result.wasSuccessful()) {
+ *   RESPONSE_CODE responseCode = result.getResponseCode();
+ *   // Perform error handling.
+ *   …
+ *   return;
+ * }
+ * if (!result.isAuthenticData()) {
+ *   // Response was not secured with DNSSEC.
+ *   …
+ *   return;
+ * }
+ * Set answers = result.getAnswers();
+ * for (A a : answers) {
+ *   InetAddress inetAddress = a.getInetAddress();
+ *   // Do someting with the InetAddress, e.g. connect to.
+ *   …
+ * }
+ * }
+ * 
+ *

+ * MiniDNS also supports SRV resource records as first class citizens: + *

+ *
+ * {@code
+ * SrvResolverResult result = DnssecResolverApi.INSTANCE.resolveSrv(SrvType.xmpp_client, "example.org")
+ * if (!result.wasSuccessful()) {
+ *   RESPONSE_CODE responseCode = result.getResponseCode();
+ *   // Perform error handling.
+ *   …
+ *   return;
+ * }
+ * if (!result.isAuthenticData()) {
+ *   // Response was not secured with DNSSEC.
+ *   …
+ *   return;
+ * }
+ * List srvRecords = result.getSortedSrvResolvedAddresses();
+ * // Loop over the domain names pointed by the SRV RR. MiniDNS will return the list
+ * // correctly sorted by the priority and weight of the related SRV RR.
+ * for (ResolvedSrvRecord srvRecord : srvRecord) {
+ *   // Loop over the Internet Address RRs resolved for the SRV RR. The order of
+ *   // the list depends on the prefered IP version setting of MiniDNS.
+ *   for (InternetAddressRR inetAddressRR : srvRecord.addresses) {
+ *     InetAddress inetAddress = inetAddressRR.getInetAddress();
+ *     int port = srvAddresses.port;
+ *     // Try to connect to inetAddress at port.
+ *     …
+ *   }
+ * }
+ * }
+ * 
+ * + * @author Florian Schmaus + * + */ +public class ResolverApi { + + public static final ResolverApi INSTANCE = new ResolverApi(new ReliableDnsClient()); + + private final AbstractDnsClient dnsClient; + + public ResolverApi(AbstractDnsClient dnsClient) { + this.dnsClient = dnsClient; + } + + public final ResolverResult resolve(String name, Class type) throws IOException { + return resolve(DnsName.from(name), type); + } + + public final ResolverResult resolve(DnsName name, Class type) throws IOException { + TYPE t = TYPE.getType(type); + Question q = new Question(name, t); + return resolve(q); + } + + public ResolverResult resolve(Question question) throws IOException { + DnsQueryResult dnsQueryResult = dnsClient.query(question); + + return new ResolverResult(question, dnsQueryResult, null); + } + + public SrvResolverResult resolveSrv(SrvType type, String serviceName) throws IOException { + return resolveSrv(type.service, type.proto, DnsName.from(serviceName)); + } + + public SrvResolverResult resolveSrv(SrvType type, DnsName serviceName) throws IOException { + return resolveSrv(type.service, type.proto, serviceName); + } + + public SrvResolverResult resolveSrv(SrvService service, SrvProto proto, String name) throws IOException { + return resolveSrv(service.dnsLabel, proto.dnsLabel, DnsName.from(name)); + } + + public SrvResolverResult resolveSrv(SrvService service, SrvProto proto, DnsName name) throws IOException { + return resolveSrv(service.dnsLabel, proto.dnsLabel, name); + } + + public SrvResolverResult resolveSrv(DnsLabel service, DnsLabel proto, DnsName name) throws IOException { + SrvServiceProto srvServiceProto = new SrvServiceProto(service, proto); + return resolveSrv(name, srvServiceProto); + } + + public SrvResolverResult resolveSrv(String name) throws IOException { + return resolveSrv(DnsName.from(name)); + } + + public ResolverResult reverseLookup(CharSequence inetAddressCs) throws IOException { + InetAddress inetAddress = InetAddress.getByName(inetAddressCs.toString()); + return reverseLookup(inetAddress); + } + + public ResolverResult reverseLookup(InetAddress inetAddress) throws IOException { + if (inetAddress instanceof Inet4Address) { + return reverseLookup((Inet4Address) inetAddress); + } else if (inetAddress instanceof Inet6Address) { + return reverseLookup((Inet6Address) inetAddress); + } else { + throw new IllegalArgumentException("The given InetAddress '" + inetAddress + "' is neither of type Inet4Address or Inet6Address"); + } + } + + public ResolverResult reverseLookup(Inet4Address inet4Address) throws IOException { + Question question = DnsClient.getReverseIpLookupQuestionFor(inet4Address); + return resolve(question); + } + + public ResolverResult reverseLookup(Inet6Address inet6Address) throws IOException { + Question question = DnsClient.getReverseIpLookupQuestionFor(inet6Address); + return resolve(question); + } + + /** + * Resolve the {@link SRV} resource record for the given name. After ensuring that the resolution was successful + * with {@link SrvResolverResult#wasSuccessful()} , and, if DNSSEC was used, that the results could be verified with + * {@link SrvResolverResult#isAuthenticData()}, simply use {@link SrvResolverResult#getSortedSrvResolvedAddresses()} to + * retrieve the resolved IP addresses. + *

+ * The name of SRV records is "_[service]._[protocol].[serviceDomain]", for example "_xmpp-client._tcp.example.org". + *

+ * + * @param srvDnsName the name to resolve. + * @return a SrvResolverResult instance which can be used to retrieve the IP addresses. + * @throws IOException if an IO exception occurs. + */ + public SrvResolverResult resolveSrv(DnsName srvDnsName) throws IOException { + final int labelCount = srvDnsName.getLabelCount(); + if (labelCount < 3) { + throw new IllegalArgumentException(); + } + + DnsLabel service = srvDnsName.getLabel(labelCount - 1); + DnsLabel proto = srvDnsName.getLabel(labelCount - 2); + DnsName name = srvDnsName.stripToLabels(labelCount - 2); + + SrvServiceProto srvServiceProto = new SrvServiceProto(service, proto); + + return resolveSrv(name, srvServiceProto); + } + + /** + * Resolve the {@link SRV} resource record for the given service name, service and protcol. After ensuring that the + * resolution was successful with {@link SrvResolverResult#wasSuccessful()} , and, if DNSSEC was used, that the + * results could be verified with {@link SrvResolverResult#isAuthenticData()}, simply use + * {@link SrvResolverResult#getSortedSrvResolvedAddresses()} to retrieve the resolved IP addresses. + * + * @param name the DNS name of the service. + * @param srvServiceProto the service and protocol to lookup. + * @return a SrvResolverResult instance which can be used to retrieve the IP addresses. + * @throws IOException if an I/O error occurs. + */ + public SrvResolverResult resolveSrv(DnsName name, SrvServiceProto srvServiceProto) throws IOException { + DnsName srvDnsName = DnsName.from(srvServiceProto.service, srvServiceProto.proto, name); + ResolverResult result = resolve(srvDnsName, SRV.class); + + return new SrvResolverResult(result, srvServiceProto, this); + } + + public final AbstractDnsClient getClient() { + return dnsClient; + } +} diff --git a/src/main/java/org/minidns/hla/ResolverResult.java b/src/main/java/org/minidns/hla/ResolverResult.java new file mode 100644 index 000000000..ea1b65f90 --- /dev/null +++ b/src/main/java/org/minidns/hla/ResolverResult.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.MiniDnsException; +import org.minidns.MiniDnsException.NullResultException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; +import org.minidns.dnssec.DnssecResultNotAuthenticException; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.record.Data; + +public class ResolverResult { + + protected final Question question; + private final RESPONSE_CODE responseCode; + private final Set data; + private final boolean isAuthenticData; + protected final Set unverifiedReasons; + protected final DnsMessage answer; + protected final DnsQueryResult result; + + ResolverResult(Question question, DnsQueryResult result, Set unverifiedReasons) throws NullResultException { + // TODO: Is this null check still needed? + if (result == null) { + throw new MiniDnsException.NullResultException(question.asMessageBuilder().build()); + } + + this.result = result; + + DnsMessage answer = result.response; + this.question = question; + this.responseCode = answer.responseCode; + this.answer = answer; + + Set r = answer.getAnswersFor(question); + if (r == null) { + this.data = Collections.emptySet(); + } else { + this.data = Collections.unmodifiableSet(r); + } + + if (unverifiedReasons == null) { + this.unverifiedReasons = null; + isAuthenticData = false; + } else { + this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons); + isAuthenticData = this.unverifiedReasons.isEmpty(); + } + } + + public boolean wasSuccessful() { + return responseCode == RESPONSE_CODE.NO_ERROR; + } + + public Set getAnswers() { + throwIseIfErrorResponse(); + return data; + } + + public Set getAnswersOrEmptySet() { + return data; + } + + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + public boolean isAuthenticData() { + throwIseIfErrorResponse(); + return isAuthenticData; + } + + /** + * Get the reasons the result could not be verified if any exists. + * + * @return The reasons the result could not be verified or null. + */ + public Set getUnverifiedReasons() { + throwIseIfErrorResponse(); + return unverifiedReasons; + } + + public Question getQuestion() { + return question; + } + + public void throwIfErrorResponse() throws ResolutionUnsuccessfulException { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException; + } + + private ResolutionUnsuccessfulException resolutionUnsuccessfulException; + + public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() { + if (wasSuccessful()) return null; + + if (resolutionUnsuccessfulException == null) { + resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode); + } + + return resolutionUnsuccessfulException; + } + + private DnssecResultNotAuthenticException dnssecResultNotAuthenticException; + + public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() { + if (!wasSuccessful()) + return null; + if (isAuthenticData) + return null; + + if (dnssecResultNotAuthenticException == null) { + dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons()); + } + + return dnssecResultNotAuthenticException; + } + + /** + * Get the raw answer DNS message we received. This is likely not what you want, try {@link #getAnswers()} instead. + * + * @return the raw answer DNS Message. + * @see #getAnswers() + */ + public DnsMessage getRawAnswer() { + return answer; + } + + public DnsQueryResult getDnsQueryResult() { + return result; + } + + @Override + public final String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(getClass().getName()).append('\n') + .append("Question: ").append(question).append('\n') + .append("Response Code: ").append(responseCode).append('\n'); + + if (responseCode == RESPONSE_CODE.NO_ERROR) { + if (isAuthenticData) { + sb.append("Results verified via DNSSEC\n"); + } + if (hasUnverifiedReasons()) { + sb.append(unverifiedReasons).append('\n'); + } + sb.append(answer.answerSection); + } + + return sb.toString(); + } + + boolean hasUnverifiedReasons() { + return unverifiedReasons != null && !unverifiedReasons.isEmpty(); + } + + protected void throwIseIfErrorResponse() { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) + throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful", + resolutionUnsuccessfulException); + } +} diff --git a/src/main/java/org/minidns/hla/SrvResolverResult.java b/src/main/java/org/minidns/hla/SrvResolverResult.java new file mode 100644 index 000000000..54a87821b --- /dev/null +++ b/src/main/java/org/minidns/hla/SrvResolverResult.java @@ -0,0 +1,204 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +import org.minidns.AbstractDnsClient.IpVersionSetting; +import org.minidns.MiniDnsException.NullResultException; +import org.minidns.dnsname.DnsName; +import org.minidns.hla.srv.SrvServiceProto; +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.InternetAddressRR; +import org.minidns.record.SRV; +import org.minidns.util.SrvUtil; + +public class SrvResolverResult extends ResolverResult { + + private final ResolverApi resolver; + private final IpVersionSetting ipVersion; + private final SrvServiceProto srvServiceProto; + + private List sortedSrvResolvedAddresses; + + SrvResolverResult(ResolverResult srvResult, SrvServiceProto srvServiceProto, ResolverApi resolver) throws NullResultException { + super(srvResult.question, srvResult.result, srvResult.unverifiedReasons); + this.resolver = resolver; + this.ipVersion = resolver.getClient().getPreferedIpVersion(); + this.srvServiceProto = srvServiceProto; + } + + /** + * Get a list ordered by priority and weight of the resolved SRV records. This method will throw if there was an + * error response or if subsequent {@link A} or {@link AAAA} resource record lookups fail. It will return + * {@code null} in case the service is decidedly not available at this domain. + * + * @return a list ordered by priority and weight of the related SRV records. + * @throws IOException in case an I/O error occurs. + */ + public List getSortedSrvResolvedAddresses() throws IOException { + if (sortedSrvResolvedAddresses != null) { + return sortedSrvResolvedAddresses; + } + + throwIseIfErrorResponse(); + + if (isServiceDecidedlyNotAvailableAtThisDomain()) { + return null; + } + + List srvRecords = SrvUtil.sortSrvRecords(getAnswers()); + + List res = new ArrayList<>(srvRecords.size()); + for (SRV srvRecord : srvRecords) { + ResolverResult
aRecordsResult = null; + ResolverResult aaaaRecordsResult = null; + Set aRecords = Collections.emptySet(); + if (ipVersion.v4) { + aRecordsResult = resolver.resolve(srvRecord.target, A.class); + if (aRecordsResult.wasSuccessful() && !aRecordsResult.hasUnverifiedReasons()) { + aRecords = aRecordsResult.getAnswers(); + } + } + + Set aaaaRecords = Collections.emptySet(); + if (ipVersion.v6) { + aaaaRecordsResult = resolver.resolve(srvRecord.target, AAAA.class); + if (aaaaRecordsResult.wasSuccessful() && !aaaaRecordsResult.hasUnverifiedReasons()) { + aaaaRecords = aaaaRecordsResult.getAnswers(); + } + } + + if (aRecords.isEmpty() && aaaaRecords.isEmpty()) { + // TODO Possibly check for (C|D)NAME usage and throw a meaningful exception that it is not allowed for + // the target of an SRV to be an alias as per RFC 2782. + /* + ResolverResult cnameRecordResult = resolve(srvRecord.name, CNAME.class); + if (cnameRecordResult.wasSuccessful()) { + } + */ + continue; + } + + List> srvAddresses = new ArrayList<>(aRecords.size() + aaaaRecords.size()); + switch (ipVersion) { + case v4only: + srvAddresses.addAll(aRecords); + break; + case v6only: + srvAddresses.addAll(aaaaRecords); + break; + case v4v6: + srvAddresses.addAll(aRecords); + srvAddresses.addAll(aaaaRecords); + break; + case v6v4: + srvAddresses.addAll(aaaaRecords); + srvAddresses.addAll(aRecords); + break; + } + + ResolvedSrvRecord resolvedSrvAddresses = new ResolvedSrvRecord(question.name, srvServiceProto, srvRecord, srvAddresses, + aRecordsResult, aaaaRecordsResult); + res.add(resolvedSrvAddresses); + } + + sortedSrvResolvedAddresses = res; + + return res; + } + + public boolean isServiceDecidedlyNotAvailableAtThisDomain() { + Set answers = getAnswers(); + if (answers.size() != 1) { + return false; + } + + SRV singleAnswer = answers.iterator().next(); + return !singleAnswer.isServiceAvailable(); + } + + public static final class ResolvedSrvRecord { + public final DnsName name; + public final SrvServiceProto srvServiceProto; + public final SRV srv; + public final List> addresses; + public final ResolverResult aRecordsResult; + public final ResolverResult aaaaRecordsResult; + + /** + * The port announced by the SRV RR. This is simply a shortcut for srv.port. + */ + public final int port; + + private ResolvedSrvRecord(DnsName name, SrvServiceProto srvServiceProto, SRV srv, + List> addresses, ResolverResult aRecordsResult, + ResolverResult aaaaRecordsResult) { + this.name = name; + this.srvServiceProto = srvServiceProto; + this.srv = srv; + this.addresses = Collections.unmodifiableList(addresses); + this.port = srv.port; + this.aRecordsResult = aRecordsResult; + this.aaaaRecordsResult = aaaaRecordsResult; + } + } + + /** + * Convenience method to sort multiple resolved SRV RRs. This is for example required by XEP-0368, where + * {@link org.minidns.hla.srv.SrvService#xmpp_client} and {@link org.minidns.hla.srv.SrvService#xmpps_client} may be + * sorted together. + * + * @param resolvedSrvRecordCollections a collection of resolved SRV records. + * @return a list ordered by priority and weight of the related SRV records. + */ + @SafeVarargs + public static List sortMultiple(Collection... resolvedSrvRecordCollections) { + int srvRecordsCount = 0; + for (Collection resolvedSrvRecords : resolvedSrvRecordCollections) { + if (resolvedSrvRecords == null) { + continue; + } + srvRecordsCount += resolvedSrvRecords.size(); + } + + List srvToSort = new ArrayList<>(srvRecordsCount); + IdentityHashMap identityMap = new IdentityHashMap<>(srvRecordsCount); + for (Collection resolvedSrvRecords : resolvedSrvRecordCollections) { + if (resolvedSrvRecords == null) { + continue; + } + for (ResolvedSrvRecord resolvedSrvRecord : resolvedSrvRecords) { + srvToSort.add(resolvedSrvRecord.srv); + identityMap.put(resolvedSrvRecord.srv, resolvedSrvRecord); + } + } + + List sortedSrvs = SrvUtil.sortSrvRecords(srvToSort); + assert sortedSrvs.size() == srvRecordsCount; + + List res = new ArrayList<>(srvRecordsCount); + for (SRV sortedSrv : sortedSrvs) { + ResolvedSrvRecord resolvedSrvRecord = identityMap.get(sortedSrv); + res.add(resolvedSrvRecord); + } + + return res; + } +} diff --git a/src/main/java/org/minidns/hla/srv/SrvProto.java b/src/main/java/org/minidns/hla/srv/SrvProto.java new file mode 100644 index 000000000..62b2997e3 --- /dev/null +++ b/src/main/java/org/minidns/hla/srv/SrvProto.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla.srv; + +import org.minidns.dnslabel.DnsLabel; + +public enum SrvProto { + + // @formatter:off + tcp, + udp, + ; + // @formatter:on + + @SuppressWarnings("ImmutableEnumChecker") + public final DnsLabel dnsLabel; + + SrvProto() { + dnsLabel = DnsLabel.from('_' + name()); + } +} diff --git a/src/main/java/org/minidns/hla/srv/SrvService.java b/src/main/java/org/minidns/hla/srv/SrvService.java new file mode 100644 index 000000000..646a1e924 --- /dev/null +++ b/src/main/java/org/minidns/hla/srv/SrvService.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla.srv; + +import org.minidns.dnslabel.DnsLabel; + +public enum SrvService { + + // @formatter:off + xmpp_client, + xmpp_server, + + /** + * XMPP client-to-server (c2s) connections using implicit TLS (also known as "Direct TLS"). + * + * @see XEP-0368: SRV records for XMPP over TLS + */ + xmpps_client, + + /** + * XMPP server-to-server (s2s) connections using implicit TLS (also known as "Direct TLS"). + * + * @see XEP-0368: SRV records for XMPP over TLS + */ + xmpps_server, + ; + // @formatter:on + + @SuppressWarnings("ImmutableEnumChecker") + public final DnsLabel dnsLabel; + + SrvService() { + String enumName = name().replaceAll("_", "-"); + dnsLabel = DnsLabel.from('_' + enumName); + } +} diff --git a/src/main/java/org/minidns/hla/srv/SrvServiceProto.java b/src/main/java/org/minidns/hla/srv/SrvServiceProto.java new file mode 100644 index 000000000..baf4e02d1 --- /dev/null +++ b/src/main/java/org/minidns/hla/srv/SrvServiceProto.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla.srv; + +import org.minidns.dnslabel.DnsLabel; + +/** + * The Serivce and Protocol part of a SRV owner name. The format of a SRV owner name is "_Service._Proto.Name". + */ +public class SrvServiceProto { + + public final DnsLabel service; + public final DnsLabel proto; + + public SrvServiceProto(DnsLabel service, DnsLabel proto) { + this.service = service; + this.proto = proto; + } +} diff --git a/src/main/java/org/minidns/hla/srv/SrvType.java b/src/main/java/org/minidns/hla/srv/SrvType.java new file mode 100644 index 000000000..d31af4b4e --- /dev/null +++ b/src/main/java/org/minidns/hla/srv/SrvType.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.hla.srv; + +public enum SrvType { + + // @formatter:off + xmpp_client(SrvService.xmpp_client, SrvProto.tcp), + xmpp_server(SrvService.xmpp_server, SrvProto.tcp), + ; + // @formatter:on + + public final SrvService service; + public final SrvProto proto; + + SrvType(SrvService service, SrvProto proto) { + this.service = service; + this.proto = proto; + } +} diff --git a/src/main/java/org/minidns/idna/DefaultIdnaTransformator.java b/src/main/java/org/minidns/idna/DefaultIdnaTransformator.java new file mode 100644 index 000000000..6a93a4488 --- /dev/null +++ b/src/main/java/org/minidns/idna/DefaultIdnaTransformator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.idna; + +import java.net.IDN; + +import org.minidns.dnsname.DnsName; + +public class DefaultIdnaTransformator implements IdnaTransformator { + + @Override + public String toASCII(String input) { + // Special case if input is ".", i.e. a string containing only a single dot character. This is a workaround for + // IDN.toASCII() implementations throwing an IllegalArgumentException on this input string (for example Android + // APIs level 26, see https://issuetracker.google.com/issues/113070416). + if (DnsName.ROOT.ace.equals(input)) { + return DnsName.ROOT.ace; + } + + return IDN.toASCII(input); + } + + @Override + public String toUnicode(String input) { + return IDN.toUnicode(input); + } + +} diff --git a/src/main/java/org/minidns/idna/IdnaTransformator.java b/src/main/java/org/minidns/idna/IdnaTransformator.java new file mode 100644 index 000000000..e078f44a0 --- /dev/null +++ b/src/main/java/org/minidns/idna/IdnaTransformator.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.idna; + +public interface IdnaTransformator { + + String toASCII(String input); + + String toUnicode(String input); + +} diff --git a/src/main/java/org/minidns/idna/MiniDnsIdna.java b/src/main/java/org/minidns/idna/MiniDnsIdna.java new file mode 100644 index 000000000..85b06ff2a --- /dev/null +++ b/src/main/java/org/minidns/idna/MiniDnsIdna.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.idna; + +public class MiniDnsIdna { + + private static IdnaTransformator idnaTransformator = new DefaultIdnaTransformator(); + + public static String toASCII(String string) { + return idnaTransformator.toASCII(string); + } + + public static String toUnicode(String string) { + return idnaTransformator.toUnicode(string); + } + + public static void setActiveTransformator(IdnaTransformator idnaTransformator) { + if (idnaTransformator == null) { + throw new IllegalArgumentException(); + } + MiniDnsIdna.idnaTransformator = idnaTransformator; + } +} diff --git a/src/main/java/org/minidns/integrationtest/AsyncApiTest.java b/src/main/java/org/minidns/integrationtest/AsyncApiTest.java new file mode 100644 index 000000000..9e03cc0b6 --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/AsyncApiTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.minidns.DnsClient; +import org.minidns.record.Record; +import org.minidns.MiniDnsFuture; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.source.AbstractDnsDataSource; +import org.minidns.source.AbstractDnsDataSource.QueryMode; +import org.minidns.source.async.AsyncNetworkDataSource; + +public class AsyncApiTest { + + public static void main(String[] args) throws IOException { + tcpAsyncApiTest(); + } + + public static void simpleAsyncApiTest() throws IOException { + DnsClient client = new DnsClient(); + client.setDataSource(new AsyncNetworkDataSource()); + client.getDataSource().setTimeout(60 * 60 * 1000); + + MiniDnsFuture future = client.queryAsync("example.com", Record.TYPE.NS); + DnsQueryResult result = future.getOrThrow(); + assertEquals(RESPONSE_CODE.NO_ERROR, result.response.responseCode); + } + + public static void tcpAsyncApiTest() throws IOException { + AbstractDnsDataSource dataSource = new AsyncNetworkDataSource(); + dataSource.setTimeout(60 * 60 * 1000); + dataSource.setUdpPayloadSize(256); + dataSource.setQueryMode(QueryMode.tcp); + + DnsClient client = new DnsClient(); + client.setDataSource(dataSource); + client.setAskForDnssec(true); + + MiniDnsFuture future = client.queryAsync("google.com", Record.TYPE.AAAA); + DnsQueryResult result = future.getOrThrow(); + assertEquals(RESPONSE_CODE.NO_ERROR, result.response.responseCode); + } +} diff --git a/src/main/java/org/minidns/integrationtest/CoreTest.java b/src/main/java/org/minidns/integrationtest/CoreTest.java new file mode 100644 index 000000000..3ecd23ea9 --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/CoreTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import org.minidns.DnsClient; +import org.minidns.cache.LruCache; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.Data; +import org.minidns.record.Record; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CoreTest { + @IntegrationTest + public static void testExampleCom() throws IOException { + DnsClient client = new DnsClient(new LruCache(1024)); + String exampleIp4 = "93.184.216.34"; // stable? + String exampleIp6 = "2606:2800:220:1:248:1893:25c8:1946"; // stable? + assertEquals(client.query("example.com", Record.TYPE.A).response.answerSection.get(0).payloadData.toString(), exampleIp4); + assertEquals(client.query("www.example.com", Record.TYPE.A).response.answerSection.get(0).payloadData.toString(), exampleIp4); + assertEquals(client.query("example.com", Record.TYPE.AAAA).response.answerSection.get(0).payloadData.toString(), exampleIp6); + assertEquals(client.query("www.example.com", Record.TYPE.AAAA).response.answerSection.get(0).payloadData.toString(), exampleIp6); + + DnsQueryResult nsResult = client.query("example.com", Record.TYPE.NS); + List values = new ArrayList<>(); + for (Record record : nsResult.response.answerSection) { + values.add(record.payloadData.toString()); + } + Collections.sort(values); + assertEquals(values.get(0), "a.iana-servers.net."); + assertEquals(values.get(1), "b.iana-servers.net."); + } + + @IntegrationTest + public static void testTcpAnswer() throws IOException { + DnsClient client = new DnsClient(new LruCache(1024)); + client.setAskForDnssec(true); + client.setDisableResultFilter(true); + DnsQueryResult result = client.query("www-nsec.example.com", Record.TYPE.A); + assertNotNull(result); + assertTrue(result.response.toArray().length > 512); + } +} diff --git a/src/main/java/org/minidns/integrationtest/DaneTest.java b/src/main/java/org/minidns/integrationtest/DaneTest.java new file mode 100644 index 000000000..ece5771ba --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/DaneTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import org.minidns.dane.DaneVerifier; + +import javax.net.ssl.HttpsURLConnection; + +import org.junit.Ignore; + +import java.io.IOException; +import java.net.URL; +import java.security.cert.CertificateException; + +public class DaneTest { + + @Ignore + @IntegrationTest + public static void testOarcDaneGood() throws IOException, CertificateException { + DaneVerifier daneVerifier = new DaneVerifier(); + daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://good.dane.dns-oarc.net/").openConnection()); + } + + @Ignore + @IntegrationTest() + public static void testOarcDaneBadHash() throws IOException, CertificateException { + DaneVerifier daneVerifier = new DaneVerifier(); + daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://bad-hash.dane.dns-oarc.net/").openConnection()); + } + + @Ignore + @IntegrationTest + public static void testOarcDaneBadParams() throws IOException, CertificateException { + DaneVerifier daneVerifier = new DaneVerifier(); + daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://bad-params.dane.dns-oarc.net/").openConnection()); + } +} diff --git a/src/main/java/org/minidns/integrationtest/DnssecTest.java b/src/main/java/org/minidns/integrationtest/DnssecTest.java new file mode 100644 index 000000000..e0adff523 --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/DnssecTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import java.io.IOException; +import java.util.Iterator; + +import org.junit.Ignore; +import org.minidns.cache.LruCache; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.dnssec.DnssecValidationFailedException; +import org.minidns.record.Record; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class DnssecTest { + + @Ignore + @IntegrationTest + public static void testOarcDaneBadSig() throws Exception { + DnssecClient client = new DnssecClient(new LruCache(1024)); + assertFalse(client.queryDnssec("_443._tcp.bad-sig.dane.dns-oarc.net", Record.TYPE.TLSA).isAuthenticData()); + } + + @IntegrationTest + public static void testUniDueSigOk() throws IOException { + DnssecClient client = new DnssecClient(new LruCache(1024)); + assertAuthentic(client.queryDnssec("sigok.verteiltesysteme.net", Record.TYPE.A)); + } + + @IntegrationTest(expected = DnssecValidationFailedException.class) + public static void testUniDueSigFail() throws IOException { + DnssecClient client = new DnssecClient(new LruCache(1024)); + client.query("sigfail.verteiltesysteme.net", Record.TYPE.A); + } + + @IntegrationTest + public static void testCloudFlare() throws IOException { + DnssecClient client = new DnssecClient(new LruCache(1024)); + assertAuthentic(client.queryDnssec("www.cloudflare-dnssec-auth.com", Record.TYPE.A)); + } + + private static void assertAuthentic(DnssecQueryResult dnssecMessage) { + if (dnssecMessage.isAuthenticData()) return; + + StringBuilder sb = new StringBuilder(); + sb.append("Answer should contain authentic data while it does not. Reasons:\n"); + for (Iterator it = dnssecMessage.getUnverifiedReasons().iterator(); it.hasNext(); ) { + DnssecUnverifiedReason unverifiedReason = it.next(); + sb.append(unverifiedReason); + if (it.hasNext()) sb.append('\n'); + } + throw new AssertionError(sb.toString()); + } +} diff --git a/src/main/java/org/minidns/integrationtest/HlaTest.java b/src/main/java/org/minidns/integrationtest/HlaTest.java new file mode 100644 index 000000000..f9bd59161 --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/HlaTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Set; + +import org.minidns.hla.ResolverApi; +import org.minidns.hla.ResolverResult; +import org.minidns.hla.SrvResolverResult; +import org.minidns.record.A; +import org.minidns.record.SRV; + +public class HlaTest { + + @IntegrationTest + public static void resolverTest() throws IOException { + ResolverResult res = ResolverApi.INSTANCE.resolve("geekplace.eu", A.class); + assertEquals(true, res.wasSuccessful()); + Set answers = res.getAnswers(); + assertEquals(1, answers.size()); + assertArrayEquals(new A(5, 45, 100, 158).toByteArray(), answers.iterator().next().toByteArray()); + } + + @IntegrationTest + public static void idnSrvTest() throws IOException { + ResolverResult res = ResolverApi.INSTANCE.resolve("_xmpp-client._tcp.im.plä.net", SRV.class); + Set answers = res.getAnswers(); + assertEquals(1, answers.size()); + + SRV srv = answers.iterator().next(); + + ResolverResult aRes = ResolverApi.INSTANCE.resolve(srv.target, A.class); + + assertTrue(aRes.wasSuccessful()); + } + + @IntegrationTest + public static void resolveSrvTest() throws IOException { + SrvResolverResult resolverResult = ResolverApi.INSTANCE.resolveSrv("_xmpp-client._tcp.jabber.org"); + Set answers = resolverResult.getAnswers(); + assertFalse(answers.isEmpty()); + } +} diff --git a/src/main/java/org/minidns/integrationtest/IntegrationTest.java b/src/main/java/org/minidns/integrationtest/IntegrationTest.java new file mode 100644 index 000000000..5ae3ed4fa --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/IntegrationTest.java @@ -0,0 +1,22 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface IntegrationTest { + Class expected() default Class.class; +} diff --git a/src/main/java/org/minidns/integrationtest/IntegrationTestHelper.java b/src/main/java/org/minidns/integrationtest/IntegrationTestHelper.java new file mode 100644 index 000000000..6117b1e03 --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/IntegrationTestHelper.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.Ignore; +import org.minidns.dnsname.DnsName; +import org.minidns.jul.MiniDnsJul; +import org.minidns.record.Record.TYPE; + +public class IntegrationTestHelper { + + public static final DnsName DNSSEC_DOMAIN = DnsName.from("verteiltesysteme.net"); + public static final TYPE RR_TYPE = TYPE.A; + + private static Set> testClasses = new HashSet<>(); + private static Logger LOGGER = Logger.getLogger(IntegrationTestHelper.class.getName()); + + enum TestResult { + Success, + Failure, + } + + static { + testClasses.add(CoreTest.class); + testClasses.add(DnssecTest.class); + testClasses.add(DaneTest.class); + testClasses.add(HlaTest.class); + testClasses.add(NsidTest.class); + testClasses.add(IterativeDnssecTest.class); + } + + private static final String MINTTEST = "minttest."; + + public static void main(String[] args) { + Properties systemProperties = System.getProperties(); + String debugString = systemProperties.getProperty(MINTTEST + "debug", Boolean.toString(false)); + boolean debug = Boolean.parseBoolean(debugString); + if (debug) { + LOGGER.info("Enabling debug and trace output"); + MiniDnsJul.enableMiniDnsTrace(); + } + + int testsRun = 0; + List successfulTests = new ArrayList<>(); + List failedTests = new ArrayList<>(); + List ignoredTests = new ArrayList<>(); + for (final Class aClass : testClasses) { + for (final Method method : aClass.getDeclaredMethods()) { + if (!method.isAnnotationPresent(IntegrationTest.class)) { + continue; + } + if (method.isAnnotationPresent(Ignore.class)) { + ignoredTests.add(method); + continue; + } + TestResult result = invokeTest(method, aClass); + testsRun++; + switch (result) { + case Success: + successfulTests.add(method); + break; + case Failure: + failedTests.add(method); + break; + } + } + } + StringBuilder resultMessage = new StringBuilder(); + resultMessage.append("MiniDNS Integration Test Result: [").append(successfulTests.size()).append('/').append(testsRun).append("] "); + if (!ignoredTests.isEmpty()) { + resultMessage.append("(Ignored: ").append(ignoredTests.size()).append(") "); + } + int exitStatus = 0; + if (failedTests.isEmpty()) { + resultMessage.append("SUCCESS \\o/"); + } else { + resultMessage.append("FAILURE :("); + exitStatus = 2; + } + LOGGER.info(resultMessage.toString()); + System.exit(exitStatus); + } + + public static TestResult invokeTest(Method method, Class aClass) { + Class expected = method.getAnnotation(IntegrationTest.class).expected(); + if (!Exception.class.isAssignableFrom(expected)) expected = null; + + String testClassName = method.getDeclaringClass().getSimpleName(); + String testMethodName = method.getName(); + + LOGGER.logp(Level.INFO, testClassName, testMethodName, "Test start."); + try { + method.invoke(null); + + if (expected != null) { + LOGGER.logp(Level.WARNING, testClassName, testMethodName, "Test failed: expected exception " + expected + " was not thrown!"); + return TestResult.Failure; + } else { + LOGGER.logp(Level.INFO, testClassName, testMethodName, "Test suceeded."); + return TestResult.Success; + } + } catch (InvocationTargetException e) { + if (expected != null && expected.isAssignableFrom(e.getTargetException().getClass())) { + LOGGER.logp(Level.INFO, testClassName, testMethodName, "Test suceeded."); + return TestResult.Success; + } else { + LOGGER.logp(Level.WARNING, testClassName, testMethodName, "Test failed: unexpected exception was thrown: ", e.getTargetException()); + return TestResult.Failure; + } + } catch (IllegalAccessException | NullPointerException e) { + LOGGER.logp(Level.SEVERE, testClassName, testMethodName, "Test failed: could not invoke test, is it public static?"); + return TestResult.Failure; + } + } +} diff --git a/src/main/java/org/minidns/integrationtest/IntegrationTestTools.java b/src/main/java/org/minidns/integrationtest/IntegrationTestTools.java new file mode 100644 index 000000000..5b1552437 --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/IntegrationTestTools.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import org.minidns.DnsCache; +import org.minidns.cache.ExtendedLruCache; +import org.minidns.cache.FullLruCache; +import org.minidns.cache.LruCache; +import org.minidns.dnssec.DnssecClient; +import org.minidns.source.NetworkDataSourceWithAccounting; + +public class IntegrationTestTools { + + public enum CacheConfig { + without, + normal, + extended, + full, + } + + public static DnssecClient getClient(CacheConfig cacheConfig) { + DnsCache cache; + switch (cacheConfig) { + case without: + cache = null; + break; + case normal: + cache = new LruCache(); + break; + case extended: + cache = new ExtendedLruCache(); + break; + case full: + cache = new FullLruCache(); + break; + default: + throw new IllegalStateException(); + } + + DnssecClient client = new DnssecClient(cache); + client.setDataSource(new NetworkDataSourceWithAccounting()); + return client; + } + +} diff --git a/src/main/java/org/minidns/integrationtest/IterativeDnssecTest.java b/src/main/java/org/minidns/integrationtest/IterativeDnssecTest.java new file mode 100644 index 000000000..dd90d580f --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/IterativeDnssecTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; +import org.minidns.integrationtest.IntegrationTestTools.CacheConfig; +import org.minidns.iterative.ReliableDnsClient.Mode; +import org.minidns.record.Record.TYPE; +import org.minidns.source.NetworkDataSourceWithAccounting; + +public class IterativeDnssecTest { + + private static final DnsName DNSSEC_DOMAIN = IntegrationTestHelper.DNSSEC_DOMAIN; + private static final TYPE RR_TYPE = IntegrationTestHelper.RR_TYPE; + + @IntegrationTest + public static void shouldRequireLessQueries() throws IOException { + DnssecClient normalCacheClient = getClient(CacheConfig.normal); + DnssecQueryResult normalCacheResult = normalCacheClient.queryDnssec(DNSSEC_DOMAIN, RR_TYPE); + assertTrue(normalCacheResult.isAuthenticData()); + NetworkDataSourceWithAccounting normalCacheNdswa = NetworkDataSourceWithAccounting.from(normalCacheClient); + + DnssecClient extendedCacheClient = getClient(CacheConfig.extended); + DnssecQueryResult extendedCacheResult = extendedCacheClient.queryDnssec(DNSSEC_DOMAIN, RR_TYPE); + assertTrue(extendedCacheResult.isAuthenticData()); + NetworkDataSourceWithAccounting extendedCacheNdswa = NetworkDataSourceWithAccounting.from(extendedCacheClient); + + final int normalCacheSuccessfulQueries = normalCacheNdswa.getStats().successfulQueries; + final int extendedCacheSuccessfulQueries = extendedCacheNdswa.getStats().successfulQueries; + assertTrue( + normalCacheSuccessfulQueries > extendedCacheSuccessfulQueries, + "Extend cache successful query count " + extendedCacheSuccessfulQueries + + " is not less than normal cache successful query count " + normalCacheSuccessfulQueries); + } + + private static DnssecClient getClient(CacheConfig cacheConfig) { + DnssecClient client = IntegrationTestTools.getClient(cacheConfig); + client.setMode(Mode.iterativeOnly); + return client; + } +} diff --git a/src/main/java/org/minidns/integrationtest/NsidTest.java b/src/main/java/org/minidns/integrationtest/NsidTest.java new file mode 100644 index 000000000..038f2f40c --- /dev/null +++ b/src/main/java/org/minidns/integrationtest/NsidTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.integrationtest; + +import java.io.IOException; +import java.net.InetAddress; + +import org.minidns.DnsClient; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.edns.Nsid; +import org.minidns.edns.Edns.OptionCode; +import org.minidns.iterative.IterativeDnsClient; +import org.minidns.record.Record.TYPE; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class NsidTest { + + @IntegrationTest + public static Nsid testNsidLRoot() { + DnsClient client = new DnsClient(null) { + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { + message.getEdnsBuilder().addEdnsOption(Nsid.REQUEST); + return super.newQuestion(message); + } + }; + DnsQueryResult result = null; + Question q = new Question("de", TYPE.NS); + for (InetAddress lRoot : IterativeDnsClient.getRootServer('l')) { + try { + result = client.query(q, lRoot); + } catch (IOException e) { + continue; + } + break; + } + Nsid nsid = result.response.getEdns().getEdnsOption(OptionCode.NSID); + assertNotNull(nsid); + return nsid; + } +} diff --git a/src/main/java/org/minidns/iterative/IterativeClientException.java b/src/main/java/org/minidns/iterative/IterativeClientException.java new file mode 100644 index 000000000..079ff59e9 --- /dev/null +++ b/src/main/java/org/minidns/iterative/IterativeClientException.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.iterative; + +import java.net.InetAddress; + +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; + +public abstract class IterativeClientException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + protected IterativeClientException(String message) { + super(message); + } + + public static class LoopDetected extends IterativeClientException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final InetAddress address; + public final Question question; + + public LoopDetected(InetAddress address, Question question) { + super("Resolution loop detected: We already asked " + address + " about " + question); + this.address = address; + this.question = question; + } + + } + + public static class MaxIterativeStepsReached extends IterativeClientException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public MaxIterativeStepsReached() { + super("Maxmimum steps reached"); + } + + } + + public static class NotAuthoritativeNorGlueRrFound extends IterativeClientException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage request; + private final DnsQueryResult result; + private final DnsName authoritativeZone; + + public NotAuthoritativeNorGlueRrFound(DnsMessage request, DnsQueryResult result, DnsName authoritativeZone) { + super("Did not receive an authoritative answer, nor did the result contain any glue records"); + this.request = request; + this.result = result; + this.authoritativeZone = authoritativeZone; + } + + public DnsMessage getRequest() { + return request; + } + + public DnsQueryResult getResult() { + return result; + } + + public DnsName getAuthoritativeZone() { + return authoritativeZone; + } + } +} diff --git a/src/main/java/org/minidns/iterative/IterativeDnsClient.java b/src/main/java/org/minidns/iterative/IterativeDnsClient.java new file mode 100644 index 000000000..34c746ad5 --- /dev/null +++ b/src/main/java/org/minidns/iterative/IterativeDnsClient.java @@ -0,0 +1,502 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.iterative; + +import static org.minidns.constants.DnsRootServer.getIpv4RootServerById; +import static org.minidns.constants.DnsRootServer.getIpv6RootServerById; +import static org.minidns.constants.DnsRootServer.getRandomIpv4RootServer; +import static org.minidns.constants.DnsRootServer.getRandomIpv6RootServer; + +import org.minidns.AbstractDnsClient; +import org.minidns.DnsCache; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.iterative.IterativeClientException.LoopDetected; +import org.minidns.iterative.IterativeClientException.NotAuthoritativeNorGlueRrFound; +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.RRWithTarget; +import org.minidns.record.Record; +import org.minidns.record.Record.TYPE; +import org.minidns.record.Data; +import org.minidns.record.InternetAddressRR; +import org.minidns.record.NS; +import org.minidns.util.MultipleIoException; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.logging.Level; + +public class IterativeDnsClient extends AbstractDnsClient { + + int maxSteps = 128; + + /** + * Create a new recursive DNS client using the global default cache. + */ + public IterativeDnsClient() { + super(); + } + + /** + * Create a new recursive DNS client with the given DNS cache. + * + * @param cache The backend DNS cache. + */ + public IterativeDnsClient(DnsCache cache) { + super(cache); + } + + /** + * Recursively query the DNS system for one entry. + * + * @param queryBuilder The query DNS message builder. + * @return The response (or null on timeout/error). + * @throws IOException if an IO error occurs. + */ + @Override + protected DnsQueryResult query(DnsMessage.Builder queryBuilder) throws IOException { + DnsMessage q = queryBuilder.build(); + ResolutionState resolutionState = new ResolutionState(this); + DnsQueryResult result = queryRecursive(resolutionState, q); + return result; + } + + private static InetAddress[] getTargets(Collection> primaryTargets, + Collection> secondaryTargets) { + InetAddress[] res = new InetAddress[2]; + + for (InternetAddressRR arr : primaryTargets) { + if (res[0] == null) { + res[0] = arr.getInetAddress(); + // If secondaryTargets is empty, then try to get the second target out of the set of primaryTargets. + if (secondaryTargets.isEmpty()) { + continue; + } + } + if (res[1] == null) { + res[1] = arr.getInetAddress(); + } + break; + } + + for (InternetAddressRR arr : secondaryTargets) { + if (res[0] == null) { + res[0] = arr.getInetAddress(); + continue; + } + if (res[1] == null) { + res[1] = arr.getInetAddress(); + } + break; + } + + return res; + } + + private DnsQueryResult queryRecursive(ResolutionState resolutionState, DnsMessage q) throws IOException { + InetAddress primaryTarget = null, secondaryTarget = null; + + Question question = q.getQuestion(); + DnsName parent = question.name.getParent(); + + switch (ipVersionSetting) { + case v4only: + for (A a : getCachedIPv4NameserverAddressesFor(parent)) { + if (primaryTarget == null) { + primaryTarget = a.getInetAddress(); + continue; + } + secondaryTarget = a.getInetAddress(); + break; + } + break; + case v6only: + for (AAAA aaaa : getCachedIPv6NameserverAddressesFor(parent)) { + if (primaryTarget == null) { + primaryTarget = aaaa.getInetAddress(); + continue; + } + secondaryTarget = aaaa.getInetAddress(); + break; + } + break; + case v4v6: + InetAddress[] v4v6targets = getTargets(getCachedIPv4NameserverAddressesFor(parent), getCachedIPv6NameserverAddressesFor(parent)); + primaryTarget = v4v6targets[0]; + secondaryTarget = v4v6targets[1]; + break; + case v6v4: + InetAddress[] v6v4targets = getTargets(getCachedIPv6NameserverAddressesFor(parent), getCachedIPv4NameserverAddressesFor(parent)); + primaryTarget = v6v4targets[0]; + secondaryTarget = v6v4targets[1]; + break; + default: + throw new AssertionError(); + } + + DnsName authoritativeZone = parent; + if (primaryTarget == null) { + authoritativeZone = DnsName.ROOT; + switch (ipVersionSetting) { + case v4only: + primaryTarget = getRandomIpv4RootServer(insecureRandom); + break; + case v6only: + primaryTarget = getRandomIpv6RootServer(insecureRandom); + break; + case v4v6: + primaryTarget = getRandomIpv4RootServer(insecureRandom); + secondaryTarget = getRandomIpv6RootServer(insecureRandom); + break; + case v6v4: + primaryTarget = getRandomIpv6RootServer(insecureRandom); + secondaryTarget = getRandomIpv4RootServer(insecureRandom); + break; + } + } + + List ioExceptions = new ArrayList<>(); + + try { + return queryRecursive(resolutionState, q, primaryTarget, authoritativeZone); + } catch (IOException ioException) { + abortIfFatal(ioException); + ioExceptions.add(ioException); + } + + if (secondaryTarget != null) { + try { + return queryRecursive(resolutionState, q, secondaryTarget, authoritativeZone); + } catch (IOException ioException) { + ioExceptions.add(ioException); + } + } + + MultipleIoException.throwIfRequired(ioExceptions); + return null; + } + + private DnsQueryResult queryRecursive(ResolutionState resolutionState, DnsMessage q, InetAddress address, DnsName authoritativeZone) throws IOException { + resolutionState.recurse(address, q); + + DnsQueryResult dnsQueryResult = query(q, address); + + DnsMessage resMessage = dnsQueryResult.response; + if (resMessage.authoritativeAnswer) { + return dnsQueryResult; + } + + if (cache != null) { + cache.offer(q, dnsQueryResult, authoritativeZone); + } + + List> authorities = resMessage.copyAuthority(); + + List ioExceptions = new ArrayList<>(); + + // Glued NS first + for (Iterator> iterator = authorities.iterator(); iterator.hasNext(); ) { + Record record = iterator.next().ifPossibleAs(NS.class); + if (record == null) { + iterator.remove(); + continue; + } + DnsName name = record.payloadData.target; + IpResultSet gluedNs = searchAdditional(resMessage, name); + for (Iterator addressIterator = gluedNs.addresses.iterator(); addressIterator.hasNext(); ) { + InetAddress target = addressIterator.next(); + DnsQueryResult recursive = null; + try { + recursive = queryRecursive(resolutionState, q, target, record.name); + } catch (IOException e) { + abortIfFatal(e); + LOGGER.log(Level.FINER, "Exception while recursing", e); + resolutionState.decrementSteps(); + ioExceptions.add(e); + if (!addressIterator.hasNext()) { + iterator.remove(); + } + continue; + } + return recursive; + } + } + + // Try non-glued NS + for (Record record : authorities) { + final Question question = q.getQuestion(); + DnsName name = ((NS) record.payloadData).target; + + // Loop prevention: If this non-glued NS equals the name we question for and if the question is about a A or + // AAAA RR, then we should not continue here as it would result in an endless loop. + if (question.name.equals(name) && (question.type == TYPE.A || question.type == TYPE.AAAA)) + continue; + + IpResultSet res = null; + try { + res = resolveIpRecursive(resolutionState, name); + } catch (IOException e) { + resolutionState.decrementSteps(); + ioExceptions.add(e); + } + if (res == null) { + continue; + } + + for (InetAddress target : res.addresses) { + DnsQueryResult recursive = null; + try { + recursive = queryRecursive(resolutionState, q, target, record.name); + } catch (IOException e) { + resolutionState.decrementSteps(); + ioExceptions.add(e); + continue; + } + return recursive; + } + } + + MultipleIoException.throwIfRequired(ioExceptions); + + // Reaching this point means we did not receive an authoritative answer, nor + // where we able to find glue records or the IPs of the next nameservers. + throw new NotAuthoritativeNorGlueRrFound(q, dnsQueryResult, authoritativeZone); + } + + private IpResultSet resolveIpRecursive(ResolutionState resolutionState, DnsName name) throws IOException { + IpResultSet.Builder res = newIpResultSetBuilder(); + + if (ipVersionSetting.v4) { + // TODO Try to retrieve A records for name out from cache. + Question question = new Question(name, TYPE.A); + final DnsMessage query = getQueryFor(question); + DnsQueryResult aDnsQueryResult = queryRecursive(resolutionState, query); + // TODO: queryRecurisve() should probably never return null. Verify that and then remove the follwing null check. + DnsMessage aMessage = aDnsQueryResult != null ? aDnsQueryResult.response : null; + if (aMessage != null) { + for (Record answer : aMessage.answerSection) { + if (answer.isAnswer(question)) { + InetAddress inetAddress = inetAddressFromRecord(name.ace, (A) answer.payloadData); + res.ipv4Addresses.add(inetAddress); + } else if (answer.type == TYPE.CNAME && answer.name.equals(name)) { + return resolveIpRecursive(resolutionState, ((RRWithTarget) answer.payloadData).target); + } + } + } + } + + if (ipVersionSetting.v6) { + // TODO Try to retrieve AAAA records for name out from cache. + Question question = new Question(name, TYPE.AAAA); + final DnsMessage query = getQueryFor(question); + DnsQueryResult aDnsQueryResult = queryRecursive(resolutionState, query); + // TODO: queryRecurisve() should probably never return null. Verify that and then remove the follwing null check. + DnsMessage aMessage = aDnsQueryResult != null ? aDnsQueryResult.response : null; + if (aMessage != null) { + for (Record answer : aMessage.answerSection) { + if (answer.isAnswer(question)) { + InetAddress inetAddress = inetAddressFromRecord(name.ace, (AAAA) answer.payloadData); + res.ipv6Addresses.add(inetAddress); + } else if (answer.type == TYPE.CNAME && answer.name.equals(name)) { + return resolveIpRecursive(resolutionState, ((RRWithTarget) answer.payloadData).target); + } + } + } + } + + return res.build(); + } + + @SuppressWarnings("incomplete-switch") + private IpResultSet searchAdditional(DnsMessage message, DnsName name) { + IpResultSet.Builder res = newIpResultSetBuilder(); + for (Record record : message.additionalSection) { + if (!record.name.equals(name)) { + continue; + } + switch (record.type) { + case A: + res.ipv4Addresses.add(inetAddressFromRecord(name.ace, (A) record.payloadData)); + break; + case AAAA: + res.ipv6Addresses.add(inetAddressFromRecord(name.ace, (AAAA) record.payloadData)); + break; + default: + break; + } + } + return res.build(); + } + + private static InetAddress inetAddressFromRecord(String name, A recordPayload) { + try { + return InetAddress.getByAddress(name, recordPayload.getIp()); + } catch (UnknownHostException e) { + // This will never happen + throw new RuntimeException(e); + } + } + + private static InetAddress inetAddressFromRecord(String name, AAAA recordPayload) { + try { + return InetAddress.getByAddress(name, recordPayload.getIp()); + } catch (UnknownHostException e) { + // This will never happen + throw new RuntimeException(e); + } + } + + public static List getRootServer(char rootServerId) { + return getRootServer(rootServerId, DEFAULT_IP_VERSION_SETTING); + } + + public static List getRootServer(char rootServerId, IpVersionSetting setting) { + Inet4Address ipv4Root = getIpv4RootServerById(rootServerId); + Inet6Address ipv6Root = getIpv6RootServerById(rootServerId); + List res = new ArrayList<>(2); + switch (setting) { + case v4only: + if (ipv4Root != null) { + res.add(ipv4Root); + } + break; + case v6only: + if (ipv6Root != null) { + res.add(ipv6Root); + } + break; + case v4v6: + if (ipv4Root != null) { + res.add(ipv4Root); + } + if (ipv6Root != null) { + res.add(ipv6Root); + } + break; + case v6v4: + if (ipv6Root != null) { + res.add(ipv6Root); + } + if (ipv4Root != null) { + res.add(ipv4Root); + } + break; + } + return res; + } + + @Override + protected boolean isResponseCacheable(Question q, DnsQueryResult result) { + return result.response.authoritativeAnswer; + } + + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { + message.setRecursionDesired(false); + message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()); + return message; + } + + private IpResultSet.Builder newIpResultSetBuilder() { + return new IpResultSet.Builder(this.insecureRandom); + } + + private static final class IpResultSet { + + final List addresses; + + private IpResultSet(List ipv4Addresses, List ipv6Addresses, Random random) { + int size; + switch (DEFAULT_IP_VERSION_SETTING) { + case v4only: + size = ipv4Addresses.size(); + break; + case v6only: + size = ipv6Addresses.size(); + break; + case v4v6: + case v6v4: + default: + size = ipv4Addresses.size() + ipv6Addresses.size(); + break; + } + + if (size == 0) { + // Fast-path in case there were no addresses, which could happen e.g., if the NS records where not + // glued. + addresses = Collections.emptyList(); + } else { + // Shuffle the addresses first, so that the load is better balanced. + if (DEFAULT_IP_VERSION_SETTING.v4) { + Collections.shuffle(ipv4Addresses, random); + } + if (DEFAULT_IP_VERSION_SETTING.v6) { + Collections.shuffle(ipv6Addresses, random); + } + + List addresses = new ArrayList<>(size); + + // Now add the shuffled addresses to the result list. + switch (DEFAULT_IP_VERSION_SETTING) { + case v4only: + addresses.addAll(ipv4Addresses); + break; + case v6only: + addresses.addAll(ipv6Addresses); + break; + case v4v6: + addresses.addAll(ipv4Addresses); + addresses.addAll(ipv6Addresses); + break; + case v6v4: + addresses.addAll(ipv6Addresses); + addresses.addAll(ipv4Addresses); + break; + } + + this.addresses = Collections.unmodifiableList(addresses); + } + } + + private static final class Builder { + private final Random random; + private final List ipv4Addresses = new ArrayList<>(8); + private final List ipv6Addresses = new ArrayList<>(8); + + private Builder(Random random) { + this.random = random; + } + + public IpResultSet build() { + return new IpResultSet(ipv4Addresses, ipv6Addresses, random); + } + } + } + + protected static void abortIfFatal(IOException ioException) throws IOException { + if (ioException instanceof LoopDetected) { + throw ioException; + } + } + +} diff --git a/src/main/java/org/minidns/iterative/ReliableDnsClient.java b/src/main/java/org/minidns/iterative/ReliableDnsClient.java new file mode 100644 index 000000000..715bd6cb8 --- /dev/null +++ b/src/main/java/org/minidns/iterative/ReliableDnsClient.java @@ -0,0 +1,190 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.iterative; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import org.minidns.AbstractDnsClient; +import org.minidns.DnsCache; +import org.minidns.DnsClient; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.source.DnsDataSource; +import org.minidns.util.MultipleIoException; + +/** + * A DNS client using a reliable strategy. First the configured resolver of the + * system are used, then, in case there is no answer, a fall back to iterative + * resolving is performed. + */ +public class ReliableDnsClient extends AbstractDnsClient { + + public enum Mode { + /** + * Try the recursive servers first and fallback to iterative resolving if it fails. This is the default mode. + */ + recursiveWithIterativeFallback, + + /** + * Only try the recursive servers. This makes {@code ReliableDnsClient} behave like a {@link DnsClient}. + */ + recursiveOnly, + + /** + * Only use iterative resolving. This makes {@code ReliableDnsClient} behave like a {@link IterativeDnsClient}. + */ + iterativeOnly, + } + + private final IterativeDnsClient recursiveDnsClient; + private final DnsClient dnsClient; + + private Mode mode = Mode.recursiveWithIterativeFallback; + + public ReliableDnsClient(DnsCache dnsCache) { + super(dnsCache); + recursiveDnsClient = new IterativeDnsClient(dnsCache) { + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) { + questionMessage = super.newQuestion(questionMessage); + return ReliableDnsClient.this.newQuestion(questionMessage); + } + // TODO: Rename dnsMessage to result. + @Override + protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) { + boolean res = super.isResponseCacheable(q, dnsMessage); + return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res; + } + }; + dnsClient = new DnsClient(dnsCache) { + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) { + questionMessage = super.newQuestion(questionMessage); + return ReliableDnsClient.this.newQuestion(questionMessage); + } + // TODO: Rename dnsMessage to result. + @Override + protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) { + boolean res = super.isResponseCacheable(q, dnsMessage); + return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res; + } + }; + } + + public ReliableDnsClient() { + this(DEFAULT_CACHE); + } + + @Override + protected DnsQueryResult query(DnsMessage.Builder q) throws IOException { + DnsQueryResult dnsMessage = null; + String unacceptableReason = null; + List ioExceptions = new ArrayList<>(); + + if (mode != Mode.iterativeOnly) { + // Try a recursive query. + try { + dnsMessage = dnsClient.query(q); + if (dnsMessage != null) { + unacceptableReason = isResponseAcceptable(dnsMessage.response); + if (unacceptableReason == null) { + return dnsMessage; + } + } + } catch (IOException ioException) { + ioExceptions.add(ioException); + } + } + + // Abort if we the are in "recursive only" mode. + if (mode == Mode.recursiveOnly) return dnsMessage; + + // Eventually log that we fall back to iterative mode. + final Level FALLBACK_LOG_LEVEL = Level.FINE; + if (LOGGER.isLoggable(FALLBACK_LOG_LEVEL) && mode != Mode.iterativeOnly) { + String logString = "Resolution fall back to iterative mode because: "; + if (!ioExceptions.isEmpty()) { + logString += ioExceptions.get(0); + } else if (dnsMessage == null) { + logString += " DnsClient did not return a response"; + } else if (unacceptableReason != null) { + logString += unacceptableReason + ". Response:\n" + dnsMessage; + } else { + throw new AssertionError("This should never been reached"); + } + LOGGER.log(FALLBACK_LOG_LEVEL, logString); + } + + try { + dnsMessage = recursiveDnsClient.query(q); + assert dnsMessage != null; + } catch (IOException ioException) { + ioExceptions.add(ioException); + } + + if (dnsMessage == null) { + assert !ioExceptions.isEmpty(); + MultipleIoException.throwIfRequired(ioExceptions); + } + + return dnsMessage; + } + + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) { + return questionMessage; + } + + @Override + protected boolean isResponseCacheable(Question q, DnsQueryResult result) { + return isResponseAcceptable(result.response) == null; + } + + /** + * Check if the response from the system's nameserver is acceptable. Must return null if the response + * is acceptable, or a String describing why it is not acceptable. If the response is not acceptable then + * {@link ReliableDnsClient} will fall back to resolve the query iteratively. + * + * @param response the response we got from the system's nameserver. + * @return null if the response is acceptable, or a String if not. + */ + protected String isResponseAcceptable(DnsMessage response) { + return null; + } + + @Override + public void setDataSource(DnsDataSource dataSource) { + super.setDataSource(dataSource); + recursiveDnsClient.setDataSource(dataSource); + dnsClient.setDataSource(dataSource); + } + + /** + * Set the mode used when resolving queries. + * + * @param mode the mode to use. + */ + public void setMode(Mode mode) { + if (mode == null) { + throw new IllegalArgumentException("Mode must not be null."); + } + this.mode = mode; + } + + public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) { + dnsClient.setUseHardcodedDnsServers(useHardcodedDnsServers); + } + +} diff --git a/src/main/java/org/minidns/iterative/ResolutionState.java b/src/main/java/org/minidns/iterative/ResolutionState.java new file mode 100644 index 000000000..ca59c5a26 --- /dev/null +++ b/src/main/java/org/minidns/iterative/ResolutionState.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.iterative; + +import java.net.InetAddress; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.iterative.IterativeClientException.LoopDetected; +import org.minidns.iterative.IterativeClientException.MaxIterativeStepsReached; + +public class ResolutionState { + + private final IterativeDnsClient recursiveDnsClient; + private final HashMap> map = new HashMap<>(); + private int steps; + + ResolutionState(IterativeDnsClient recursiveDnsClient) { + this.recursiveDnsClient = recursiveDnsClient; + } + + void recurse(InetAddress address, DnsMessage query) throws LoopDetected, MaxIterativeStepsReached { + Question question = query.getQuestion(); + if (!map.containsKey(address)) { + map.put(address, new HashSet()); + } else if (map.get(address).contains(question)) { + throw new IterativeClientException.LoopDetected(address, question); + } + + if (++steps > recursiveDnsClient.maxSteps) { + throw new IterativeClientException.MaxIterativeStepsReached(); + } + + boolean isNew = map.get(address).add(question); + assert isNew; + } + + void decrementSteps() { + steps--; + } + +} diff --git a/src/main/java/org/minidns/jul/MiniDnsJul.java b/src/main/java/org/minidns/jul/MiniDnsJul.java new file mode 100644 index 000000000..b7f95549c --- /dev/null +++ b/src/main/java/org/minidns/jul/MiniDnsJul.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.jul; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +@SuppressWarnings("DateFormatConstant") +public class MiniDnsJul { + + private static final Logger LOGGER = Logger.getLogger(MiniDnsJul.class.getName()); + + private static final InputStream LOG_MANAGER_CONFIG = new ByteArrayInputStream(( +// @formatter:off +"org.minidns.level=FINEST" + '\n' +).getBytes(StandardCharsets.UTF_8) +); +// @formatter:on + + private static final DateTimeFormatter LONG_LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("hh:mm:ss.SSS"); + + private static final DateTimeFormatter SHORT_LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("mm:ss.SSS"); + + private static final Handler CONSOLE_HANDLER = new ConsoleHandler(); + + private static boolean shortLog = true; + + static { + try { + LogManager.getLogManager().readConfiguration(LOG_MANAGER_CONFIG); + } catch (SecurityException | IOException e) { + LOGGER.log(Level.SEVERE, "Could not apply MiniDNS JUL configuration", e); + } + + CONSOLE_HANDLER.setLevel(Level.OFF); + CONSOLE_HANDLER.setFormatter(new Formatter() { + @Override + public String format(LogRecord logRecord) { + StringBuilder sb = new StringBuilder(256); + + Instant date = Instant.ofEpochMilli(logRecord.getMillis()); + String dateString; + if (shortLog) { + dateString = SHORT_LOG_TIME_FORMAT.format(date); + } else { + dateString = LONG_LOG_TIME_FORMAT.format(date); + } + sb.append(dateString).append(' '); + + String level = logRecord.getLevel().toString(); + if (shortLog) { + level = level.substring(0, 1); + } + sb.append(level).append(' '); + + String loggerName = logRecord.getLoggerName(); + if (shortLog) { + String[] parts = loggerName.split("\\."); + loggerName = parts[parts.length > 1 ? parts.length - 1 : 0]; + } + sb.append(loggerName); + sb.append(' ').append(logRecord.getSourceMethodName()); + + if (shortLog) { + sb.append(' '); + } else { + sb.append('\n'); + } + + sb.append(formatMessage(logRecord)); + if (logRecord.getThrown() != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + // CHECKSTYLE:OFF + pw.println(); + logRecord.getThrown().printStackTrace(pw); + // CHECKSTYLE:ON + pw.close(); + sb.append(sw); + } + sb.append('\n'); + return sb.toString(); + } + }); + Logger.getLogger("org.minidns").addHandler(CONSOLE_HANDLER); + } + + public static void enableMiniDnsTrace() { + enableMiniDnsTrace(true); + } + + public static void enableMiniDnsTrace(boolean shortLog) { + MiniDnsJul.shortLog = shortLog; + CONSOLE_HANDLER.setLevel(Level.FINEST); + } + + public static void disableMiniDnsTrace() { + CONSOLE_HANDLER.setLevel(Level.OFF); + } +} diff --git a/src/main/java/org/minidns/minidnsrepl/DnssecStats.java b/src/main/java/org/minidns/minidnsrepl/DnssecStats.java new file mode 100644 index 000000000..c9c3e9fe3 --- /dev/null +++ b/src/main/java/org/minidns/minidnsrepl/DnssecStats.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.minidnsrepl; + +import java.io.IOException; + +import org.minidns.cache.ExtendedLruCache; +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.integrationtest.IntegrationTestTools.CacheConfig; +import org.minidns.iterative.ReliableDnsClient.Mode; +import org.minidns.jul.MiniDnsJul; +import org.minidns.record.Record.TYPE; + +public class DnssecStats { + + private static final DnsName DOMAIN = DnsName.from("verteiltesysteme.net"); + private static final TYPE RR_TYPE = TYPE.A; + + public static void iterativeDnssecLookupNormalVsExtendedCache() throws IOException { + // iterativeDnssecLookup(CacheConfig.normal); + iterativeDnssecLookup(CacheConfig.extended); + } + + private static void iterativeDnssecLookup(CacheConfig cacheConfig) throws IOException { + DnssecClient client = MiniDnsStats.getClient(cacheConfig); + client.setMode(Mode.iterativeOnly); + DnssecQueryResult secRes = client.queryDnssec(DOMAIN, RR_TYPE); + + StringBuilder stats = MiniDnsStats.getStats(client); + stats.append('\n'); + stats.append(secRes); + stats.append('\n'); + for (DnssecUnverifiedReason r : secRes.getUnverifiedReasons()) { + stats.append(r); + } + stats.append("\n\n"); + // CHECKSTYLE:OFF + System.out.println(stats); + // CHECKSTYLE:ON + } + + public static void iterativeDnsssecTest() throws SecurityException, IllegalArgumentException, IOException { + MiniDnsJul.enableMiniDnsTrace(); + DnssecClient client = new DnssecClient(new ExtendedLruCache()); + client.setMode(Mode.iterativeOnly); + + DnssecQueryResult secRes = client.queryDnssec("verteiltesysteme.net", TYPE.A); + + // CHECKSTYLE:OFF + System.out.println(secRes); + // CHECKSTYLE:ON + } + +} diff --git a/src/main/java/org/minidns/minidnsrepl/MiniDnsRepl.java b/src/main/java/org/minidns/minidnsrepl/MiniDnsRepl.java new file mode 100644 index 000000000..831fb32d5 --- /dev/null +++ b/src/main/java/org/minidns/minidnsrepl/MiniDnsRepl.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.minidnsrepl; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; + +import org.minidns.AbstractDnsClient; +import org.minidns.DnsClient; +import org.minidns.cache.LruCache; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnssec.DnssecClient; +import org.minidns.hla.DnssecResolverApi; +import org.minidns.hla.ResolverResult; +import org.minidns.iterative.IterativeDnsClient; +import org.minidns.jul.MiniDnsJul; +import org.minidns.record.A; + +public class MiniDnsRepl { + + public static final DnsClient DNSCLIENT = new DnsClient(); + public static final IterativeDnsClient ITERATIVEDNSCLIENT = new IterativeDnsClient(); + public static final DnssecClient DNSSECCLIENT = new DnssecClient(); + + static { + LruCache cache = null; + try { + Field defaultCacheField = AbstractDnsClient.class.getDeclaredField("DEFAULT_CACHE"); + defaultCacheField.setAccessible(true); + cache = (LruCache) defaultCacheField.get(null); + } catch (IllegalAccessException | NoSuchFieldException | SecurityException e) { + throw new IllegalStateException(e); + } + DEFAULT_CACHE = cache; + } + + public static final LruCache DEFAULT_CACHE; + + public static void init() { + // CHECKSTYLE:OFF + System.out.println("MiniDNS REPL"); + // CHECKSTYLE:ON + } + + public static void clearCache() throws SecurityException, IllegalArgumentException { + DEFAULT_CACHE.clear(); + } + + public static void main(String[] args) throws IOException, SecurityException, IllegalArgumentException { + MiniDnsJul.enableMiniDnsTrace(); + + ResolverResult res = DnssecResolverApi.INSTANCE.resolveDnssecReliable("verteiltesysteme.net", A.class); + /* + DnssecStats.iterativeDnssecLookupNormalVsExtendedCache(); + DnssecClient client = new DNSSECClient(new LRUCache(1024)); + DnssecMessage secRes = client.queryDnssec("verteiltesysteme.net", TYPE.A); + */ + + /* + DnssecStats.iterativeDnssecLookupNormalVsExtendedCache(); + Nsid nsid = NSIDTest.testNsidLRoot(); + DnsMessage res = RECURSIVEDNSCLIENT.query("mate.geekplace.eu", TYPE.A); + */ + // CHECKSTYLE:OFF + System.out.println(res); +// System.out.println(nsid); +// System.out.println(secRes); +// System.out.println(res); + // CHCECKSTYLE:ON + } + + public static void writeToFile(DnsMessage dnsMessage, String path) throws IOException { + try (FileOutputStream fos = new FileOutputStream(path)) { + dnsMessage.writeTo(fos, true); + } + } +} diff --git a/src/main/java/org/minidns/minidnsrepl/MiniDnsStats.java b/src/main/java/org/minidns/minidnsrepl/MiniDnsStats.java new file mode 100644 index 000000000..f0689a03a --- /dev/null +++ b/src/main/java/org/minidns/minidnsrepl/MiniDnsStats.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.minidnsrepl; + +import static java.lang.System.out; + +import java.io.IOException; +import java.util.Arrays; + +import org.minidns.AbstractDnsClient; +import org.minidns.DnsCache; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnssec.DnssecClient; +import org.minidns.integrationtest.IntegrationTestTools; +import org.minidns.integrationtest.IntegrationTestTools.CacheConfig; +import org.minidns.record.Record.TYPE; +import org.minidns.source.NetworkDataSourceWithAccounting; + +public class MiniDnsStats { + + public static void main(String[] args) throws IOException { + showDnssecStats(); + } + + public static void showDnssecStats() throws IOException { + showDnssecStats("siccegge.de", TYPE.A); + } + + public static void showDnssecStats(String name, TYPE type) throws IOException { + DnssecClient client; + + client = getClient(CacheConfig.without); + // CHECKSTYLE:OFF + out.println(gatherStatsFor(client, "Without Cache", name, type)); + // CHECKSTYLE:ON + + client = getClient(CacheConfig.normal); + // CHECKSTYLE:OFF + out.println(gatherStatsFor(client, "With Cache", name, type)); + // CHECKSTYLE:ON + + client = getClient(CacheConfig.extended); + // CHECKSTYLE:OFF + out.println(gatherStatsFor(client, "With Extended Cache", name, type)); + // CHECKSTYLE:ON + + client = getClient(CacheConfig.full); + // CHECKSTYLE:OFF + out.println(gatherStatsFor(client, "With Full Cache", name, type)); + // CHECKSTYLE:ON + } + + public static StringBuilder gatherStatsFor(DnssecClient client, String testName, String name, TYPE type) throws IOException { + DnsQueryResult result; + long start, stop; + + start = System.currentTimeMillis(); + result = client.query(name, type); + stop = System.currentTimeMillis(); + + StringBuilder sb = new StringBuilder(); + sb.append(testName).append('\n'); + char[] headline = new char[testName.length()]; + Arrays.fill(headline, '#'); + sb.append(headline).append('\n'); + sb.append(result).append('\n'); + sb.append("Took ").append(stop - start).append("ms").append('\n'); + sb.append(getStats(client)).append('\n'); + sb.append('\n'); + + return sb; + } + + public static DnssecClient getClient(CacheConfig cacheConfig) { + return IntegrationTestTools.getClient(cacheConfig); + } + + public static StringBuilder getStats(AbstractDnsClient client) { + StringBuilder sb = new StringBuilder(); + + NetworkDataSourceWithAccounting ndswa = NetworkDataSourceWithAccounting.from(client); + if (ndswa != null) { + sb.append(ndswa.getStats().toString()); + } else { + sb.append("Client is not using " + NetworkDataSourceWithAccounting.class.getSimpleName()); + } + + DnsCache dnsCache = client.getCache(); + if (dnsCache != null) { + sb.append(dnsCache); + } else { + sb.append("Client is not using a Cache"); + } + + return sb; + } +} diff --git a/src/main/java/org/minidns/record/A.java b/src/main/java/org/minidns/record/A.java new file mode 100644 index 000000000..3bfea97ce --- /dev/null +++ b/src/main/java/org/minidns/record/A.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; +import java.net.Inet4Address; + +import org.minidns.record.Record.TYPE; +import org.minidns.util.InetAddressUtil; + +/** + * A record payload (ip pointer). + */ +public class A extends InternetAddressRR { + + @Override + public TYPE getType() { + return TYPE.A; + } + + public A(Inet4Address inet4Address) { + super(inet4Address); + assert ip.length == 4; + } + + public A(int q1, int q2, int q3, int q4) { + super(new byte[] { (byte) q1, (byte) q2, (byte) q3, (byte) q4 }); + if (q1 < 0 || q1 > 255 || q2 < 0 || q2 > 255 || q3 < 0 || q3 > 255 || q4 < 0 || q4 > 255) { + throw new IllegalArgumentException(); + } + } + + public A(byte[] ip) { + super(ip); + if (ip.length != 4) { + throw new IllegalArgumentException("IPv4 address in A record is always 4 byte"); + } + } + + public A(CharSequence ipv4CharSequence) { + this(InetAddressUtil.ipv4From(ipv4CharSequence)); + } + + public static A parse(DataInputStream dis) + throws IOException { + byte[] ip = new byte[4]; + dis.readFully(ip); + return new A(ip); + } + + @Override + public String toString() { + return Integer.toString(ip[0] & 0xff) + "." + + Integer.toString(ip[1] & 0xff) + "." + + Integer.toString(ip[2] & 0xff) + "." + + Integer.toString(ip[3] & 0xff); + } + +} diff --git a/src/main/java/org/minidns/record/AAAA.java b/src/main/java/org/minidns/record/AAAA.java new file mode 100644 index 000000000..c2076ca0a --- /dev/null +++ b/src/main/java/org/minidns/record/AAAA.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; +import java.net.Inet6Address; + +import org.minidns.record.Record.TYPE; +import org.minidns.util.InetAddressUtil; + +/** + * AAAA payload (an ipv6 pointer). + */ +public class AAAA extends InternetAddressRR { + + @Override + public TYPE getType() { + return TYPE.AAAA; + } + + public AAAA(Inet6Address inet6address) { + super(inet6address); + assert ip.length == 16; + } + + public AAAA(byte[] ip) { + super(ip); + if (ip.length != 16) { + throw new IllegalArgumentException("IPv6 address in AAAA record is always 16 byte"); + } + } + + public AAAA(CharSequence ipv6CharSequence) { + this(InetAddressUtil.ipv6From(ipv6CharSequence)); + } + + public static AAAA parse(DataInputStream dis) + throws IOException { + byte[] ip = new byte[16]; + dis.readFully(ip); + return new AAAA(ip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ip.length; i += 2) { + if (i != 0) { + sb.append(':'); + } + sb.append(Integer.toHexString( + ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff) + )); + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/minidns/record/CNAME.java b/src/main/java/org/minidns/record/CNAME.java new file mode 100644 index 000000000..0defe8aee --- /dev/null +++ b/src/main/java/org/minidns/record/CNAME.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * CNAME payload (pointer to another domain / address). + */ +public class CNAME extends RRWithTarget { + + public static CNAME parse(DataInputStream dis, byte[] data) throws IOException { + DnsName target = DnsName.parse(dis, data); + return new CNAME(target); + } + + public CNAME(String target) { + this(DnsName.from(target)); + } + + public CNAME(DnsName target) { + super(target); + } + + @Override + public TYPE getType() { + return TYPE.CNAME; + } + +} diff --git a/src/main/java/org/minidns/record/DLV.java b/src/main/java/org/minidns/record/DLV.java new file mode 100644 index 000000000..7340d0069 --- /dev/null +++ b/src/main/java/org/minidns/record/DLV.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; + +/** + * DLV record payload. + * + * According to RFC4431, DLV has exactly the same format as DS records. + */ +public class DLV extends DelegatingDnssecRR { + + public static DLV parse (DataInputStream dis, int length) throws IOException { + SharedData parsedData = DelegatingDnssecRR.parseSharedData(dis, length); + return new DLV(parsedData.keyTag, parsedData.algorithm, parsedData.digestType, parsedData.digest); + } + + public DLV(int keyTag, byte algorithm, byte digestType, byte[] digest) { + super(keyTag, algorithm, digestType, digest); + } + + public DLV(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) { + super(keyTag, algorithm, digestType, digest); + } + + @Override + public Record.TYPE getType() { + return Record.TYPE.DLV; + } +} diff --git a/src/main/java/org/minidns/record/DNAME.java b/src/main/java/org/minidns/record/DNAME.java new file mode 100644 index 000000000..4f715fe3c --- /dev/null +++ b/src/main/java/org/minidns/record/DNAME.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * A DNAME resource record. + * + * @see RFC 6672 - DNAME Redirection in the DNS + */ +public class DNAME extends RRWithTarget { + + public static DNAME parse(DataInputStream dis, byte[] data) throws IOException { + DnsName target = DnsName.parse(dis, data); + return new DNAME(target); + } + + public DNAME(String target) { + this(DnsName.from(target)); + } + + public DNAME(DnsName target) { + super(target); + } + + @Override + public TYPE getType() { + return TYPE.DNAME; + } + +} diff --git a/src/main/java/org/minidns/record/DNSKEY.java b/src/main/java/org/minidns/record/DNSKEY.java new file mode 100644 index 000000000..875df020d --- /dev/null +++ b/src/main/java/org/minidns/record/DNSKEY.java @@ -0,0 +1,194 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.record.Record.TYPE; +import org.minidns.util.Base64; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; + +/** + * DNSKEY record payload. + */ +public class DNSKEY extends Data { + /** + * Whether the key should be used as a secure entry point key. + * + * see RFC 3757 + */ + public static final short FLAG_SECURE_ENTRY_POINT = 0x1; + + /** + * Whether the record holds a revoked key. + */ + public static final short FLAG_REVOKE = 0x80; + + /** + * Whether the record holds a DNS zone key. + */ + public static final short FLAG_ZONE = 0x100; + + /** + * Use the protocol defined in RFC 4034. + */ + public static final byte PROTOCOL_RFC4034 = 3; + + /** + * Bitmap of flags: {@link #FLAG_SECURE_ENTRY_POINT}, {@link #FLAG_REVOKE}, {@link #FLAG_ZONE}. + * + * @see IANA - DNSKEY RR Flags + */ + public final short flags; + + /** + * Must be {@link #PROTOCOL_RFC4034}. + */ + public final byte protocol; + + /** + * The public key's cryptographic algorithm used. + * + */ + public final SignatureAlgorithm algorithm; + + /** + * The byte value of the public key's cryptographic algorithm used. + * + */ + public final byte algorithmByte; + + /** + * The public key material. The format depends on the algorithm of the key being stored. + */ + private final byte[] key; + + /** + * This DNSKEY's key tag. Calculated just-in-time when using {@link #getKeyTag()} + */ + private transient Integer keyTag; + + public static DNSKEY parse(DataInputStream dis, int length) throws IOException { + short flags = dis.readShort(); + byte protocol = dis.readByte(); + byte algorithm = dis.readByte(); + byte[] key = new byte[length - 4]; + dis.readFully(key); + return new DNSKEY(flags, protocol, algorithm, key); + } + + private DNSKEY(short flags, byte protocol, SignatureAlgorithm algorithm, byte algorithmByte, byte[] key) { + this.flags = flags; + this.protocol = protocol; + + assert algorithmByte == (algorithm != null ? algorithm.number : algorithmByte); + this.algorithmByte = algorithmByte; + this.algorithm = algorithm != null ? algorithm : SignatureAlgorithm.forByte(algorithmByte); + + this.key = key; + } + + public DNSKEY(short flags, byte protocol, byte algorithm, byte[] key) { + this(flags, protocol, SignatureAlgorithm.forByte(algorithm), algorithm, key); + } + + public DNSKEY(short flags, byte protocol, SignatureAlgorithm algorithm, byte[] key) { + this(flags, protocol, algorithm, algorithm.number, key); + } + + @Override + public TYPE getType() { + return TYPE.DNSKEY; + } + + /** + * Retrieve the key tag identifying this DNSKEY. + * The key tag is used within the DS and RRSIG record to distinguish multiple keys for the same name. + * + * This implementation is based on the reference implementation shown in RFC 4034 Appendix B. + * + * @return this DNSKEY's key tag + */ + public /* unsigned short */ int getKeyTag() { + if (keyTag == null) { + byte[] recordBytes = toByteArray(); + long ac = 0; + + for (int i = 0; i < recordBytes.length; ++i) { + ac += ((i & 1) > 0) ? recordBytes[i] & 0xFFL : ((recordBytes[i] & 0xFFL) << 8); + } + ac += (ac >> 16) & 0xFFFF; + keyTag = (int) (ac & 0xFFFF); + } + return keyTag; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeShort(flags); + dos.writeByte(protocol); + dos.writeByte(algorithmByte); + dos.write(key); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(flags).append(' ') + .append(protocol).append(' ') + .append(algorithm).append(' ') + .append(Base64.encodeToString(key)); + return sb.toString(); + } + + public int getKeyLength() { + return key.length; + } + + public byte[] getKey() { + return key.clone(); + } + + public DataInputStream getKeyAsDataInputStream() { + return new DataInputStream(new ByteArrayInputStream(key)); + } + + private transient String keyBase64Cache; + + public String getKeyBase64() { + if (keyBase64Cache == null) { + keyBase64Cache = Base64.encodeToString(key); + } + return keyBase64Cache; + } + + private transient BigInteger keyBigIntegerCache; + + public BigInteger getKeyBigInteger() { + if (keyBigIntegerCache == null) { + keyBigIntegerCache = new BigInteger(key); + } + return keyBigIntegerCache; + } + + public boolean keyEquals(byte[] otherKey) { + return Arrays.equals(key, otherKey); + } + + public boolean isSecureEntryPoint() { + return (flags & FLAG_SECURE_ENTRY_POINT) == 1; + } +} diff --git a/src/main/java/org/minidns/record/DS.java b/src/main/java/org/minidns/record/DS.java new file mode 100644 index 000000000..6db98d7f2 --- /dev/null +++ b/src/main/java/org/minidns/record/DS.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.record.Record.TYPE; + +import java.io.DataInputStream; +import java.io.IOException; +/** + * DS (Delegation Signer) record payload. + * + * @see RFC 4034 § 5 + */ +public class DS extends DelegatingDnssecRR { + + public static DS parse(DataInputStream dis, int length) throws IOException { + SharedData parsedData = DelegatingDnssecRR.parseSharedData(dis, length); + return new DS(parsedData.keyTag, parsedData.algorithm, parsedData.digestType, parsedData.digest); + } + + public DS(int keyTag, byte algorithm, byte digestType, byte[] digest) { + super(keyTag, algorithm, digestType, digest); + } + + public DS(int keyTag, SignatureAlgorithm algorithm, byte digestType, byte[] digest) { + super(keyTag, algorithm, digestType, digest); + } + + public DS(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) { + super(keyTag, algorithm, digestType, digest); + } + + @Override + public TYPE getType() { + return TYPE.DS; + } + +} diff --git a/src/main/java/org/minidns/record/Data.java b/src/main/java/org/minidns/record/Data.java new file mode 100644 index 000000000..ce40c280e --- /dev/null +++ b/src/main/java/org/minidns/record/Data.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import org.minidns.record.Record.TYPE; + +/** + * Generic payload class. + */ +public abstract class Data { + + /** + * The payload type. + * @return The payload type. + */ + public abstract TYPE getType(); + + /** + * The internal method used to serialize Data subclasses. + * + * @param dos the output stream to serialize to. + * @throws IOException if an I/O error occurs. + */ + protected abstract void serialize(DataOutputStream dos) throws IOException; + + private byte[] bytes; + + private void setBytes() { + if (bytes != null) return; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + try { + serialize(dos); + } catch (IOException e) { + // Should never happen. + throw new AssertionError(e); + } + bytes = baos.toByteArray(); + } + + public final int length() { + setBytes(); + return bytes.length; + } + + public final void toOutputStream(OutputStream outputStream) throws IOException { + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + toOutputStream(dataOutputStream); + } + + /** + * Write the binary representation of this payload to the given {@link DataOutputStream}. + * + * @param dos the DataOutputStream to write to. + * @throws IOException if an I/O error occurs. + */ + public final void toOutputStream(DataOutputStream dos) throws IOException { + setBytes(); + dos.write(bytes); + } + + public final byte[] toByteArray() { + setBytes(); + return bytes.clone(); + } + + private transient Integer hashCodeCache; + + @Override + public final int hashCode() { + if (hashCodeCache == null) { + setBytes(); + hashCodeCache = Arrays.hashCode(bytes); + } + return hashCodeCache; + } + + @Override + public final boolean equals(Object other) { + if (!(other instanceof Data)) { + return false; + } + if (other == this) { + return true; + } + Data otherData = (Data) other; + otherData.setBytes(); + setBytes(); + + return Arrays.equals(bytes, otherData.bytes); + } +} diff --git a/src/main/java/org/minidns/record/DelegatingDnssecRR.java b/src/main/java/org/minidns/record/DelegatingDnssecRR.java new file mode 100644 index 000000000..5d089ec43 --- /dev/null +++ b/src/main/java/org/minidns/record/DelegatingDnssecRR.java @@ -0,0 +1,157 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Locale; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; + +/** + * DS (Delegation Signer) record payload. + * + * @see RFC 4034 § 5 + */ +public abstract class DelegatingDnssecRR extends Data { + + /** + * The key tag value of the DNSKEY RR that validates this signature. + */ + public final int /* unsigned short */ keyTag; + + /** + * The cryptographic algorithm used to create the signature. If MiniDNS + * isn't aware of the signature algorithm, then this field will be + * null. + * + * @see #algorithmByte + */ + public final SignatureAlgorithm algorithm; + + /** + * The byte value of the cryptographic algorithm used to create the signature. + */ + public final byte algorithmByte; + + /** + * The algorithm used to construct the digest. If MiniDNS + * isn't aware of the digest algorithm, then this field will be + * null. + * + * @see #digestTypeByte + */ + public final DigestAlgorithm digestType; + + /** + * The byte value of algorithm used to construct the digest. + */ + public final byte digestTypeByte; + + /** + * The digest build from a DNSKEY. + */ + protected final byte[] digest; + + protected static SharedData parseSharedData(DataInputStream dis, int length) throws IOException { + int keyTag = dis.readUnsignedShort(); + byte algorithm = dis.readByte(); + byte digestType = dis.readByte(); + byte[] digest = new byte[length - 4]; + if (dis.read(digest) != digest.length) throw new IOException(); + return new SharedData(keyTag, algorithm, digestType, digest); + } + + protected static final class SharedData { + final int keyTag; + final byte algorithm; + final byte digestType; + final byte[] digest; + + private SharedData(int keyTag, byte algorithm, byte digestType, byte[] digest) { + this.keyTag = keyTag; + this.algorithm = algorithm; + this.digestType = digestType; + this.digest = digest; + } + } + + protected DelegatingDnssecRR(int keyTag, SignatureAlgorithm algorithm, byte algorithmByte, DigestAlgorithm digestType, byte digestTypeByte, byte[] digest) { + this.keyTag = keyTag; + + assert algorithmByte == (algorithm != null ? algorithm.number : algorithmByte); + this.algorithmByte = algorithmByte; + this.algorithm = algorithm != null ? algorithm : SignatureAlgorithm.forByte(algorithmByte); + + assert digestTypeByte == (digestType != null ? digestType.value : digestTypeByte); + this.digestTypeByte = digestTypeByte; + this.digestType = digestType != null ? digestType : DigestAlgorithm.forByte(digestTypeByte); + + assert digest != null; + this.digest = digest; + } + + protected DelegatingDnssecRR(int keyTag, byte algorithm, byte digestType, byte[] digest) { + this(keyTag, null, algorithm, null, digestType, digest); + } + + protected DelegatingDnssecRR(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) { + this(keyTag, algorithm, algorithm.number, digestType, digestType.value, digest); + } + + protected DelegatingDnssecRR(int keyTag, SignatureAlgorithm algorithm, byte digestType, byte[] digest) { + this(keyTag, algorithm, algorithm.number, null, digestType, digest); + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeShort(keyTag); + dos.writeByte(algorithmByte); + dos.writeByte(digestTypeByte); + dos.write(digest); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(keyTag).append(' ') + .append(algorithm).append(' ') + .append(digestType).append(' ') + .append(new BigInteger(1, digest).toString(16).toUpperCase(Locale.ROOT)); + return sb.toString(); + } + + private transient BigInteger digestBigIntCache; + + public BigInteger getDigestBigInteger() { + if (digestBigIntCache == null) { + digestBigIntCache = new BigInteger(1, digest); + } + return digestBigIntCache; + } + + private transient String digestHexCache; + + public String getDigestHex() { + if (digestHexCache == null) { + digestHexCache = getDigestBigInteger().toString(16).toUpperCase(Locale.ROOT); + } + return digestHexCache; + } + + public boolean digestEquals(byte[] otherDigest) { + return Arrays.equals(digest, otherDigest); + } +} diff --git a/src/main/java/org/minidns/record/InternetAddressRR.java b/src/main/java/org/minidns/record/InternetAddressRR.java new file mode 100644 index 000000000..a3dbcdf2d --- /dev/null +++ b/src/main/java/org/minidns/record/InternetAddressRR.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * A resource record representing a internet address. Provides {@link #getInetAddress()}. + */ +public abstract class InternetAddressRR extends Data { + + + /** + * Target IP. + */ + protected final byte[] ip; + + /** + * Cache for the {@link InetAddress} this record presents. + */ + private transient IA inetAddress; + + protected InternetAddressRR(byte[] ip) { + this.ip = ip; + } + + protected InternetAddressRR(IA inetAddress) { + this(inetAddress.getAddress()); + this.inetAddress = inetAddress; + } + + @Override + public final void serialize(DataOutputStream dos) throws IOException { + dos.write(ip); + } + + /** + * Allocates a new byte buffer and fills the buffer with the bytes representing the IP address of this resource record. + * + * @return a new byte buffer containing the bytes of the IP. + */ + public final byte[] getIp() { + return ip.clone(); + } + + @SuppressWarnings("unchecked") + public final IA getInetAddress() { + if (inetAddress == null) { + try { + inetAddress = (IA) InetAddress.getByAddress(ip); + } catch (UnknownHostException e) { + throw new IllegalStateException(e); + } + } + return inetAddress; + } + + public static InternetAddressRR from(InetAddress inetAddress) { + if (inetAddress instanceof Inet4Address) { + return new A((Inet4Address) inetAddress); + } + + return new AAAA((Inet6Address) inetAddress); + } +} diff --git a/src/main/java/org/minidns/record/MX.java b/src/main/java/org/minidns/record/MX.java new file mode 100644 index 000000000..3cb11737d --- /dev/null +++ b/src/main/java/org/minidns/record/MX.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +/** + * MX record payload (mail service pointer). + */ +public class MX extends Data { + + /** + * The priority of this service. Lower values mean higher priority. + */ + public final int priority; + + /** + * The name of the target server. + */ + public final DnsName target; + + /** + * The name of the target server. + * + * @deprecated use {@link #target} instead. + */ + @Deprecated + public final DnsName name; + + public static MX parse(DataInputStream dis, byte[] data) + throws IOException { + int priority = dis.readUnsignedShort(); + DnsName name = DnsName.parse(dis, data); + return new MX(priority, name); + } + + public MX(int priority, String name) { + this(priority, DnsName.from(name)); + } + + public MX(int priority, DnsName name) { + this.priority = priority; + this.target = name; + this.name = target; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeShort(priority); + target.writeToStream(dos); + } + + @Override + public String toString() { + return priority + " " + target + '.'; + } + + @Override + public TYPE getType() { + return TYPE.MX; + } + +} diff --git a/src/main/java/org/minidns/record/NS.java b/src/main/java/org/minidns/record/NS.java new file mode 100644 index 000000000..dbc3fe806 --- /dev/null +++ b/src/main/java/org/minidns/record/NS.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +/** + * Nameserver record. + */ +public class NS extends RRWithTarget { + + public static NS parse(DataInputStream dis, byte[] data) throws IOException { + DnsName target = DnsName.parse(dis, data); + return new NS(target); + } + + public NS(DnsName name) { + super(name); + } + + @Override + public TYPE getType() { + return TYPE.NS; + } + +} diff --git a/src/main/java/org/minidns/record/NSEC.java b/src/main/java/org/minidns/record/NSEC.java new file mode 100644 index 000000000..112eb3705 --- /dev/null +++ b/src/main/java/org/minidns/record/NSEC.java @@ -0,0 +1,161 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * NSEC record payload. + */ +public class NSEC extends Data { + + private static final Logger LOGGER = Logger.getLogger(NSEC.class.getName()); + + /** + * The next owner name that contains a authoritative data or a delegation point. + */ + public final DnsName next; + + private final byte[] typeBitmap; + + /** + * The RR types existing at the owner name. + */ + public final List types; + + public static NSEC parse(DataInputStream dis, byte[] data, int length) throws IOException { + DnsName next = DnsName.parse(dis, data); + + byte[] typeBitmap = new byte[length - next.size()]; + if (dis.read(typeBitmap) != typeBitmap.length) throw new IOException(); + List types = readTypeBitMap(typeBitmap); + return new NSEC(next, types); + } + + public NSEC(String next, List types) { + this(DnsName.from(next), types); + } + + public NSEC(String next, TYPE... types) { + this(DnsName.from(next), Arrays.asList(types)); + } + + public NSEC(DnsName next, List types) { + this.next = next; + this.types = Collections.unmodifiableList(types); + this.typeBitmap = createTypeBitMap(types); + } + + @Override + public TYPE getType() { + return TYPE.NSEC; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + next.writeToStream(dos); + dos.write(typeBitmap); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(next).append('.'); + for (TYPE type : types) { + sb.append(' ').append(type); + } + return sb.toString(); + } + + @SuppressWarnings("NarrowingCompoundAssignment") + static byte[] createTypeBitMap(List types) { + List typeList = new ArrayList(types.size()); + for (TYPE type : types) { + typeList.add(type.getValue()); + } + Collections.sort(typeList); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + + try { + int windowBlock = -1; + byte[] bitmap = null; + for (Integer type : typeList) { + if (windowBlock == -1 || (type >> 8) != windowBlock) { + if (windowBlock != -1) writeOutBlock(bitmap, dos); + windowBlock = type >> 8; + dos.writeByte(windowBlock); + bitmap = new byte[32]; + } + int a = (type >> 3) % 32; + int b = type % 8; + bitmap[a] |= (byte) (128 >> b); + } + if (windowBlock != -1) writeOutBlock(bitmap, dos); + } catch (IOException e) { + // Should never happen. + throw new RuntimeException(e); + } + + return baos.toByteArray(); + } + + private static void writeOutBlock(byte[] values, DataOutputStream dos) throws IOException { + int n = 0; + for (int i = 0; i < values.length; i++) { + if (values[i] != 0) n = i + 1; + } + dos.writeByte(n); + for (int i = 0; i < n; i++) { + dos.writeByte(values[i]); + } + } + + // TODO: This method should probably just return List so that unknown types can be act on later. + static List readTypeBitMap(byte[] typeBitmap) throws IOException { + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(typeBitmap)); + int read = 0; + ArrayList typeList = new ArrayList(); + while (typeBitmap.length > read) { + int windowBlock = dis.readUnsignedByte(); + int bitmapLength = dis.readUnsignedByte(); + for (int i = 0; i < bitmapLength; i++) { + int b = dis.readUnsignedByte(); + for (int j = 0; j < 8; j++) { + if (((b >> j) & 0x1) > 0) { + int typeInt = (windowBlock << 8) + (i * 8) + (7 - j); + TYPE type = TYPE.getType(typeInt); + if (type == TYPE.UNKNOWN) { + LOGGER.warning("Skipping unknown type in type bitmap: " + typeInt); + continue; + } + typeList.add(type); + } + } + } + read += bitmapLength + 2; + } + return typeList; + } +} diff --git a/src/main/java/org/minidns/record/NSEC3.java b/src/main/java/org/minidns/record/NSEC3.java new file mode 100644 index 000000000..bcc99c71a --- /dev/null +++ b/src/main/java/org/minidns/record/NSEC3.java @@ -0,0 +1,212 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.dnslabel.DnsLabel; +import org.minidns.record.Record.TYPE; +import org.minidns.util.Base32; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * NSEC3 record payload. + */ +public class NSEC3 extends Data { + + /** + * This Flag indicates whether this NSEC3 RR may cover unsigned + * delegations. + */ + public static final byte FLAG_OPT_OUT = 0x1; + + private static final Map HASH_ALGORITHM_LUT = new HashMap<>(); + + /** + * DNSSEC NSEC3 Hash Algorithms. + * + * @see + * IANA DNSSEC NSEC3 Hash Algorithms + */ + public enum HashAlgorithm { + RESERVED(0, "Reserved"), + SHA1(1, "SHA-1"), + ; + + HashAlgorithm(int value, String description) { + if (value < 0 || value > 255) { + throw new IllegalArgumentException(); + } + this.value = (byte) value; + this.description = description; + HASH_ALGORITHM_LUT.put(this.value, this); + } + + public final byte value; + public final String description; + + public static HashAlgorithm forByte(byte b) { + return HASH_ALGORITHM_LUT.get(b); + } + } + + /** + * The cryptographic hash algorithm used. If MiniDNS + * isn't aware of the hash algorithm, then this field will be + * null. + * + * @see #hashAlgorithmByte + */ + public final HashAlgorithm hashAlgorithm; + + /** + * The byte value of the cryptographic hash algorithm used. + */ + public final byte hashAlgorithmByte; + + /** + * Bitmap of flags: {@link #FLAG_OPT_OUT}. + */ + public final byte flags; + + /** + * The number of iterations the hash algorithm is applied. + */ + public final int /* unsigned short */ iterations; + + /** + * The salt appended to the next owner name before hashing. + */ + private final byte[] salt; + + /** + * The next hashed owner name in hash order. + */ + private final byte[] nextHashed; + + private final byte[] typeBitmap; + + /** + * The RR types existing at the original owner name. + */ + public final List types; + + public static NSEC3 parse(DataInputStream dis, int length) throws IOException { + byte hashAlgorithm = dis.readByte(); + byte flags = dis.readByte(); + int iterations = dis.readUnsignedShort(); + int saltLength = dis.readUnsignedByte(); + byte[] salt = new byte[saltLength]; + if (dis.read(salt) != salt.length) throw new IOException(); + int hashLength = dis.readUnsignedByte(); + byte[] nextHashed = new byte[hashLength]; + if (dis.read(nextHashed) != nextHashed.length) throw new IOException(); + byte[] typeBitmap = new byte[length - (6 + saltLength + hashLength)]; + if (dis.read(typeBitmap) != typeBitmap.length) throw new IOException(); + List types = NSEC.readTypeBitMap(typeBitmap); + return new NSEC3(hashAlgorithm, flags, iterations, salt, nextHashed, types); + } + + private NSEC3(HashAlgorithm hashAlgorithm, byte hashAlgorithmByte, byte flags, int iterations, byte[] salt, byte[] nextHashed, List types) { + assert hashAlgorithmByte == (hashAlgorithm != null ? hashAlgorithm.value : hashAlgorithmByte); + this.hashAlgorithmByte = hashAlgorithmByte; + this.hashAlgorithm = hashAlgorithm != null ? hashAlgorithm : HashAlgorithm.forByte(hashAlgorithmByte); + + this.flags = flags; + this.iterations = iterations; + this.salt = salt; + this.nextHashed = nextHashed; + this.types = types; + this.typeBitmap = NSEC.createTypeBitMap(types); + } + + public NSEC3(byte hashAlgorithm, byte flags, int iterations, byte[] salt, byte[] nextHashed, List types) { + this(null, hashAlgorithm, flags, iterations, salt, nextHashed, types); + } + + public NSEC3(byte hashAlgorithm, byte flags, int iterations, byte[] salt, byte[] nextHashed, TYPE... types) { + this(null, hashAlgorithm, flags, iterations, salt, nextHashed, Arrays.asList(types)); + } + + @Override + public TYPE getType() { + return TYPE.NSEC3; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeByte(hashAlgorithmByte); + dos.writeByte(flags); + dos.writeShort(iterations); + dos.writeByte(salt.length); + dos.write(salt); + dos.writeByte(nextHashed.length); + dos.write(nextHashed); + dos.write(typeBitmap); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(hashAlgorithm).append(' ') + .append(flags).append(' ') + .append(iterations).append(' ') + .append(salt.length == 0 ? "-" : new BigInteger(1, salt).toString(16).toUpperCase(Locale.ROOT)).append(' ') + .append(Base32.encodeToString(nextHashed)); + for (TYPE type : types) { + sb.append(' ').append(type); + } + return sb.toString(); + } + + public byte[] getSalt() { + return salt.clone(); + } + + public int getSaltLength() { + return salt.length; + } + + public byte[] getNextHashed() { + return nextHashed.clone(); + } + + private String nextHashedBase32Cache; + + public String getNextHashedBase32() { + if (nextHashedBase32Cache == null) { + nextHashedBase32Cache = Base32.encodeToString(nextHashed); + } + return nextHashedBase32Cache; + } + + private DnsLabel nextHashedDnsLabelCache; + + public DnsLabel getNextHashedDnsLabel() { + if (nextHashedDnsLabelCache == null) { + String nextHashedBase32 = getNextHashedBase32(); + nextHashedDnsLabelCache = DnsLabel.from(nextHashedBase32); + } + return nextHashedDnsLabelCache; + } + + public void copySaltInto(byte[] dest, int destPos) { + System.arraycopy(salt, 0, dest, destPos, salt.length); + } +} diff --git a/src/main/java/org/minidns/record/NSEC3PARAM.java b/src/main/java/org/minidns/record/NSEC3PARAM.java new file mode 100644 index 000000000..62b4df3da --- /dev/null +++ b/src/main/java/org/minidns/record/NSEC3PARAM.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.record.NSEC3.HashAlgorithm; +import org.minidns.record.Record.TYPE; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Locale; + +/** + * NSEC3PARAM record payload. + */ +public class NSEC3PARAM extends Data { + + /** + * The cryptographic hash algorithm used. + * + */ + public final HashAlgorithm hashAlgorithm; + + /** + * The cryptographic hash algorithm used. + * + */ + public final byte hashAlgorithmByte; + + public final byte flags; + + /** + * The number of iterations the hash algorithm is applied. + */ + public final int /* unsigned short */ iterations; + + /** + * The salt appended to the next owner name before hashing. + */ + private final byte[] salt; + + public static NSEC3PARAM parse(DataInputStream dis) throws IOException { + byte hashAlgorithm = dis.readByte(); + byte flags = dis.readByte(); + int iterations = dis.readUnsignedShort(); + int saltLength = dis.readUnsignedByte(); + byte[] salt = new byte[saltLength]; + if (dis.read(salt) != salt.length && salt.length != 0) throw new IOException(); + return new NSEC3PARAM(hashAlgorithm, flags, iterations, salt); + } + + private NSEC3PARAM(HashAlgorithm hashAlgorithm, byte hashAlgorithmByte, byte flags, int iterations, byte[] salt) { + assert hashAlgorithmByte == (hashAlgorithm != null ? hashAlgorithm.value : hashAlgorithmByte); + this.hashAlgorithmByte = hashAlgorithmByte; + this.hashAlgorithm = hashAlgorithm != null ? hashAlgorithm : HashAlgorithm.forByte(hashAlgorithmByte); + + this.flags = flags; + this.iterations = iterations; + this.salt = salt; + } + + NSEC3PARAM(byte hashAlgorithm, byte flags, int iterations, byte[] salt) { + this(null, hashAlgorithm, flags, iterations, salt); + } + + @Override + public TYPE getType() { + return TYPE.NSEC3PARAM; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeByte(hashAlgorithmByte); + dos.writeByte(flags); + dos.writeShort(iterations); + dos.writeByte(salt.length); + dos.write(salt); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(hashAlgorithm).append(' ') + .append(flags).append(' ') + .append(iterations).append(' ') + .append(salt.length == 0 ? "-" : new BigInteger(1, salt).toString(16).toUpperCase(Locale.ROOT)); + return sb.toString(); + } + + public int getSaltLength() { + return salt.length; + } +} diff --git a/src/main/java/org/minidns/record/OPENPGPKEY.java b/src/main/java/org/minidns/record/OPENPGPKEY.java new file mode 100644 index 000000000..5bef24de3 --- /dev/null +++ b/src/main/java/org/minidns/record/OPENPGPKEY.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.util.Base64; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class OPENPGPKEY extends Data { + + private final byte[] publicKeyPacket; + + public static OPENPGPKEY parse(DataInputStream dis, int length) throws IOException { + byte[] publicKeyPacket = new byte[length]; + dis.readFully(publicKeyPacket); + return new OPENPGPKEY(publicKeyPacket); + } + + OPENPGPKEY(byte[] publicKeyPacket) { + this.publicKeyPacket = publicKeyPacket; + } + + @Override + public Record.TYPE getType() { + return Record.TYPE.OPENPGPKEY; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.write(publicKeyPacket); + } + + @Override + public String toString() { + return getPublicKeyPacketBase64(); + } + + private transient String publicKeyPacketBase64Cache; + + public String getPublicKeyPacketBase64() { + if (publicKeyPacketBase64Cache == null) { + publicKeyPacketBase64Cache = Base64.encodeToString(publicKeyPacket); + } + return publicKeyPacketBase64Cache; + } + + public byte[] getPublicKeyPacket() { + return publicKeyPacket.clone(); + } +} diff --git a/src/main/java/org/minidns/record/OPT.java b/src/main/java/org/minidns/record/OPT.java new file mode 100644 index 000000000..fce4c5d32 --- /dev/null +++ b/src/main/java/org/minidns/record/OPT.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.edns.EdnsOption; +import org.minidns.record.Record.TYPE; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * OPT payload (see RFC 2671 for details). + */ +public class OPT extends Data { + + public final List variablePart; + + public OPT() { + this(Collections.emptyList()); + } + + public OPT(List variablePart) { + this.variablePart = Collections.unmodifiableList(variablePart); + } + + public static OPT parse(DataInputStream dis, int payloadLength) throws IOException { + List variablePart; + if (payloadLength == 0) { + variablePart = Collections.emptyList(); + } else { + int payloadLeft = payloadLength; + variablePart = new ArrayList<>(4); + while (payloadLeft > 0) { + int optionCode = dis.readUnsignedShort(); + int optionLength = dis.readUnsignedShort(); + byte[] optionData = new byte[optionLength]; + dis.read(optionData); + EdnsOption ednsOption = EdnsOption.parse(optionCode, optionData); + variablePart.add(ednsOption); + payloadLeft -= 2 + 2 + optionLength; + // Assert that payloadLeft never becomes negative + assert payloadLeft >= 0; + } + } + return new OPT(variablePart); + } + + @Override + public TYPE getType() { + return TYPE.OPT; + } + + @Override + protected void serialize(DataOutputStream dos) throws IOException { + for (EdnsOption endsOption : variablePart) { + endsOption.writeToDos(dos); + } + } + +} diff --git a/src/main/java/org/minidns/record/PTR.java b/src/main/java/org/minidns/record/PTR.java new file mode 100644 index 000000000..e2e155803 --- /dev/null +++ b/src/main/java/org/minidns/record/PTR.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +/** + * A PTR record is handled like a CNAME. + */ +public class PTR extends RRWithTarget { + + public static PTR parse(DataInputStream dis, byte[] data) throws IOException { + DnsName target = DnsName.parse(dis, data); + return new PTR(target); + } + + PTR(String name) { + this(DnsName.from(name)); + } + + PTR(DnsName name) { + super(name); + } + + @Override + public TYPE getType() { + return TYPE.PTR; + } + +} diff --git a/src/main/java/org/minidns/record/RRSIG.java b/src/main/java/org/minidns/record/RRSIG.java new file mode 100644 index 000000000..8c089d9fd --- /dev/null +++ b/src/main/java/org/minidns/record/RRSIG.java @@ -0,0 +1,199 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; +import org.minidns.util.Base64; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * RRSIG record payload. + */ +public class RRSIG extends Data { + + /** + * The type of RRset covered by this signature. + */ + public final TYPE typeCovered; + + /** + * The cryptographic algorithm used to create the signature. + */ + public final SignatureAlgorithm algorithm; + + /** + * The cryptographic algorithm used to create the signature. + */ + public final byte algorithmByte; + + /** + * The number of labels in the original RRSIG RR owner name. + */ + public final byte labels; + + /** + * The TTL of the covered RRset. + */ + public final long /* unsigned int */ originalTtl; + + /** + * The date and time this RRSIG records expires. + */ + public final Date signatureExpiration; + + /** + * The date and time this RRSIG records starts to be valid. + */ + public final Date signatureInception; + + /** + * The key tag value of the DNSKEY RR that validates this signature. + */ + public final int /* unsigned short */ keyTag; + + /** + * The owner name of the DNSKEY RR that a validator is supposed to use. + */ + public final DnsName signerName; + + /** + * Signature that covers RRSIG RDATA (excluding the signature field) and RRset data. + */ + private final byte[] signature; + + @SuppressWarnings("JavaUtilDate") + public static RRSIG parse(DataInputStream dis, byte[] data, int length) throws IOException { + TYPE typeCovered = TYPE.getType(dis.readUnsignedShort()); + byte algorithm = dis.readByte(); + byte labels = dis.readByte(); + long originalTtl = dis.readInt() & 0xFFFFFFFFL; + Date signatureExpiration = new Date((dis.readInt() & 0xFFFFFFFFL) * 1000); + Date signatureInception = new Date((dis.readInt() & 0xFFFFFFFFL) * 1000); + int keyTag = dis.readUnsignedShort(); + DnsName signerName = DnsName.parse(dis, data); + int sigSize = length - signerName.size() - 18; + byte[] signature = new byte[sigSize]; + if (dis.read(signature) != signature.length) throw new IOException(); + return new RRSIG(typeCovered, null, algorithm, labels, originalTtl, signatureExpiration, signatureInception, keyTag, signerName, + signature); + } + + private RRSIG(TYPE typeCovered, SignatureAlgorithm algorithm, byte algorithmByte, byte labels, long originalTtl, Date signatureExpiration, + Date signatureInception, int keyTag, DnsName signerName, byte[] signature) { + this.typeCovered = typeCovered; + + assert algorithmByte == (algorithm != null ? algorithm.number : algorithmByte); + this.algorithmByte = algorithmByte; + this.algorithm = algorithm != null ? algorithm : SignatureAlgorithm.forByte(algorithmByte); + + this.labels = labels; + this.originalTtl = originalTtl; + this.signatureExpiration = signatureExpiration; + this.signatureInception = signatureInception; + this.keyTag = keyTag; + this.signerName = signerName; + this.signature = signature; + } + + public RRSIG(TYPE typeCovered, int algorithm, byte labels, long originalTtl, Date signatureExpiration, + Date signatureInception, int keyTag, DnsName signerName, byte[] signature) { + this(typeCovered, null, (byte) algorithm, labels, originalTtl, signatureExpiration, signatureInception, keyTag, signerName, signature); + } + + public RRSIG(TYPE typeCovered, int algorithm, byte labels, long originalTtl, Date signatureExpiration, + Date signatureInception, int keyTag, String signerName, byte[] signature) { + this(typeCovered, null, (byte) algorithm, labels, originalTtl, signatureExpiration, signatureInception, keyTag, DnsName.from(signerName), signature); + } + + public RRSIG(TYPE typeCovered, SignatureAlgorithm algorithm, byte labels, + long originalTtl, Date signatureExpiration, Date signatureInception, + int keyTag, DnsName signerName, byte[] signature) { + this(typeCovered, algorithm.number, labels, originalTtl, signatureExpiration, signatureInception, + keyTag, signerName, signature); + } + + public RRSIG(TYPE typeCovered, SignatureAlgorithm algorithm, byte labels, + long originalTtl, Date signatureExpiration, Date signatureInception, + int keyTag, String signerName, byte[] signature) { + this(typeCovered, algorithm.number, labels, originalTtl, signatureExpiration, signatureInception, + keyTag, DnsName.from(signerName), signature); + } + + public byte[] getSignature() { + return signature.clone(); + } + + public DataInputStream getSignatureAsDataInputStream() { + return new DataInputStream(new ByteArrayInputStream(signature)); + } + + public int getSignatureLength() { + return signature.length; + } + + private transient String base64SignatureCache; + + public String getSignatureBase64() { + if (base64SignatureCache == null) { + base64SignatureCache = Base64.encodeToString(signature); + } + return base64SignatureCache; + } + + @Override + public TYPE getType() { + return TYPE.RRSIG; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + writePartialSignature(dos); + dos.write(signature); + } + + @SuppressWarnings("JavaUtilDate") + public void writePartialSignature(DataOutputStream dos) throws IOException { + dos.writeShort(typeCovered.getValue()); + dos.writeByte(algorithmByte); + dos.writeByte(labels); + dos.writeInt((int) originalTtl); + dos.writeInt((int) (signatureExpiration.getTime() / 1000)); + dos.writeInt((int) (signatureInception.getTime() / 1000)); + dos.writeShort(keyTag); + signerName.writeToStream(dos); + } + + @Override + public String toString() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + StringBuilder sb = new StringBuilder() + .append(typeCovered).append(' ') + .append(algorithm).append(' ') + .append(labels).append(' ') + .append(originalTtl).append(' ') + .append(dateFormat.format(signatureExpiration)).append(' ') + .append(dateFormat.format(signatureInception)).append(' ') + .append(keyTag).append(' ') + .append(signerName).append(". ") + .append(getSignatureBase64()); + return sb.toString(); + } +} diff --git a/src/main/java/org/minidns/record/RRWithTarget.java b/src/main/java/org/minidns/record/RRWithTarget.java new file mode 100644 index 000000000..6deeb022d --- /dev/null +++ b/src/main/java/org/minidns/record/RRWithTarget.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataOutputStream; +import java.io.IOException; + +import org.minidns.dnsname.DnsName; + +/** + * A resource record pointing to a target. + */ +public abstract class RRWithTarget extends Data { + + public final DnsName target; + + /** + * The target of this resource record. + * @deprecated {@link #target} instead. + */ + @Deprecated + public final DnsName name; + + @Override + public void serialize(DataOutputStream dos) throws IOException { + target.writeToStream(dos); + } + + protected RRWithTarget(DnsName target) { + this.target = target; + this.name = target; + } + + @Override + public String toString() { + return target + "."; + } + + public final DnsName getTarget() { + return target; + } +} diff --git a/src/main/java/org/minidns/record/Record.java b/src/main/java/org/minidns/record/Record.java new file mode 100644 index 000000000..0e6317c57 --- /dev/null +++ b/src/main/java/org/minidns/record/Record.java @@ -0,0 +1,628 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; + +/** + * A generic DNS record. + */ +public final class Record { + + /** + * The resource record type. + * + * @see + * IANA DNS Parameters - Resource Record (RR) TYPEs + */ + public enum TYPE { + UNKNOWN(-1), + A(1, A.class), + NS(2, NS.class), + MD(3), + MF(4), + CNAME(5, CNAME.class), + SOA(6, SOA.class), + MB(7), + MG(8), + MR(9), + NULL(10), + WKS(11), + PTR(12, PTR.class), + HINFO(13), + MINFO(14), + MX(15, MX.class), + TXT(16, TXT.class), + RP(17), + AFSDB(18), + X25(19), + ISDN(20), + RT(21), + NSAP(22), + NSAP_PTR(23), + SIG(24), + KEY(25), + PX(26), + GPOS(27), + AAAA(28, AAAA.class), + LOC(29), + NXT(30), + EID(31), + NIMLOC(32), + SRV(33, SRV.class), + ATMA(34), + NAPTR(35), + KX(36), + CERT(37), + A6(38), + DNAME(39, DNAME.class), + SINK(40), + OPT(41, OPT.class), + APL(42), + DS(43, DS.class), + SSHFP(44), + IPSECKEY(45), + RRSIG(46, RRSIG.class), + NSEC(47, NSEC.class), + DNSKEY(48, DNSKEY.class), + DHCID(49), + NSEC3(50, NSEC3.class), + NSEC3PARAM(51, NSEC3PARAM.class), + TLSA(52, TLSA.class), + HIP(55), + NINFO(56), + RKEY(57), + TALINK(58), + CDS(59), + CDNSKEY(60), + OPENPGPKEY(61, OPENPGPKEY.class), + CSYNC(62), + SPF(99), + UINFO(100), + UID(101), + GID(102), + UNSPEC(103), + NID(104), + L32(105), + L64(106), + LP(107), + EUI48(108), + EUI64(109), + TKEY(249), + TSIG(250), + IXFR(251), + AXFR(252), + MAILB(253), + MAILA(254), + ANY(255), + URI(256), + CAA(257), + TA(32768), + DLV(32769, DLV.class), + ; + + /** + * The value of this DNS record type. + */ + private final int value; + + private final Class dataClass; + + /** + * Internal lookup table to map values to types. + */ + private static final Map INVERSE_LUT = new HashMap<>(); + + private static final Map, TYPE> DATA_LUT = new HashMap<>(); + + static { + // Initialize the reverse lookup table. + for (TYPE t : TYPE.values()) { + INVERSE_LUT.put(t.getValue(), t); + if (t.dataClass != null) { + DATA_LUT.put(t.dataClass, t); + } + } + } + + /** + * Create a new record type. + * + * @param value The binary value of this type. + */ + TYPE(int value) { + this(value, null); + } + + /** + * Create a new record type. + * + * @param The class for this type. + * @param dataClass The class for this type. + * @param value The binary value of this type. + */ + TYPE(int value, Class dataClass) { + this.value = value; + this.dataClass = dataClass; + } + + /** + * Retrieve the binary value of this type. + * @return The binary value. + */ + public int getValue() { + return value; + } + + /** + * Get the {@link Data} class for this type. + * + * @param The class for this type. + * @return the {@link Data} class for this type. + */ + @SuppressWarnings("unchecked") + public Class getDataClass() { + return (Class) dataClass; + } + + /** + * Retrieve the symbolic type of the binary value. + * @param value The binary type value. + * @return The symbolic tpye. + */ + public static TYPE getType(int value) { + TYPE type = INVERSE_LUT.get(value); + if (type == null) return UNKNOWN; + return type; + } + + /** + * Retrieve the type for a given {@link Data} class. + * + * @param The class for this type. + * @param dataClass the class to lookup the type for. + * @return the type for the given data class. + */ + public static TYPE getType(Class dataClass) { + return DATA_LUT.get(dataClass); + } + } + + /** + * The symbolic class of a DNS record (usually {@link CLASS#IN} for Internet). + * + * @see IANA Domain Name System (DNS) Parameters - DNS CLASSes + */ + public enum CLASS { + + /** + * The Internet class. This is the most common class used by todays DNS systems. + */ + IN(1), + + /** + * The Chaos class. + */ + CH(3), + + /** + * The Hesiod class. + */ + HS(4), + NONE(254), + ANY(255); + + /** + * Internal reverse lookup table to map binary class values to symbolic + * names. + */ + private static final HashMap INVERSE_LUT = + new HashMap(); + + static { + // Initialize the interal reverse lookup table. + for (CLASS c : CLASS.values()) { + INVERSE_LUT.put(c.getValue(), c); + } + } + + /** + * The binary value of this dns class. + */ + private final int value; + + /** + * Create a new DNS class based on a binary value. + * @param value The binary value of this DNS class. + */ + CLASS(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this DNS class. + * @return The binary value of this DNS class. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic DNS class for a binary class value. + * @param value The binary DNS class value. + * @return The symbolic class instance. + */ + public static CLASS getClass(int value) { + return INVERSE_LUT.get(value); + } + + } + + /** + * The generic name of this record. + */ + public final DnsName name; + + /** + * The type (and payload type) of this record. + */ + public final TYPE type; + + /** + * The record class (usually CLASS.IN). + */ + public final CLASS clazz; + + /** + * The value of the class field of a RR. + * + * According to RFC 2671 (OPT RR) this is not necessarily representable + * using clazz field and unicastQuery bit + */ + public final int clazzValue; + + /** + * The ttl of this record. + */ + public final long ttl; + + /** + * The payload object of this record. + */ + public final D payloadData; + + /** + * MDNS defines the highest bit of the class as the unicast query bit. + */ + public final boolean unicastQuery; + + /** + * Parse a given record based on the full message data and the current + * stream position. + * + * @param dis The DataInputStream positioned at the first record byte. + * @param data The full message data. + * @return the record which was parsed. + * @throws IOException In case of malformed replies. + */ + public static Record parse(DataInputStream dis, byte[] data) throws IOException { + DnsName name = DnsName.parse(dis, data); + int typeValue = dis.readUnsignedShort(); + TYPE type = TYPE.getType(typeValue); + int clazzValue = dis.readUnsignedShort(); + CLASS clazz = CLASS.getClass(clazzValue & 0x7fff); + boolean unicastQuery = (clazzValue & 0x8000) > 0; + long ttl = (((long) dis.readUnsignedShort()) << 16) + + dis.readUnsignedShort(); + int payloadLength = dis.readUnsignedShort(); + Data payloadData; + switch (type) { + case SOA: + payloadData = SOA.parse(dis, data); + break; + case SRV: + payloadData = SRV.parse(dis, data); + break; + case MX: + payloadData = MX.parse(dis, data); + break; + case AAAA: + payloadData = AAAA.parse(dis); + break; + case A: + payloadData = A.parse(dis); + break; + case NS: + payloadData = NS.parse(dis, data); + break; + case CNAME: + payloadData = CNAME.parse(dis, data); + break; + case DNAME: + payloadData = DNAME.parse(dis, data); + break; + case PTR: + payloadData = PTR.parse(dis, data); + break; + case TXT: + payloadData = TXT.parse(dis, payloadLength); + break; + case OPT: + payloadData = OPT.parse(dis, payloadLength); + break; + case DNSKEY: + payloadData = DNSKEY.parse(dis, payloadLength); + break; + case RRSIG: + payloadData = RRSIG.parse(dis, data, payloadLength); + break; + case DS: + payloadData = DS.parse(dis, payloadLength); + break; + case NSEC: + payloadData = NSEC.parse(dis, data, payloadLength); + break; + case NSEC3: + payloadData = NSEC3.parse(dis, payloadLength); + break; + case NSEC3PARAM: + payloadData = NSEC3PARAM.parse(dis); + break; + case TLSA: + payloadData = TLSA.parse(dis, payloadLength); + break; + case OPENPGPKEY: + payloadData = OPENPGPKEY.parse(dis, payloadLength); + break; + case DLV: + payloadData = DLV.parse(dis, payloadLength); + break; + case UNKNOWN: + default: + payloadData = UNKNOWN.parse(dis, payloadLength, type); + break; + } + return new Record<>(name, type, clazz, clazzValue, ttl, payloadData, unicastQuery); + } + + public Record(DnsName name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) { + this(name, type, clazz, clazz.getValue() + (unicastQuery ? 0x8000 : 0), ttl, payloadData, unicastQuery); + } + + public Record(String name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) { + this(DnsName.from(name), type, clazz, ttl, payloadData, unicastQuery); + } + + public Record(String name, TYPE type, int clazzValue, long ttl, D payloadData) { + this(DnsName.from(name), type, CLASS.NONE, clazzValue, ttl, payloadData, false); + } + + public Record(DnsName name, TYPE type, int clazzValue, long ttl, D payloadData) { + this(name, type, CLASS.NONE, clazzValue, ttl, payloadData, false); + } + + private Record(DnsName name, TYPE type, CLASS clazz, int clazzValue, long ttl, D payloadData, boolean unicastQuery) { + this.name = name; + this.type = type; + this.clazz = clazz; + this.clazzValue = clazzValue; + this.ttl = ttl; + this.payloadData = payloadData; + this.unicastQuery = unicastQuery; + } + + public void toOutputStream(OutputStream outputStream) throws IOException { + if (payloadData == null) { + throw new IllegalStateException("Empty Record has no byte representation"); + } + + DataOutputStream dos = new DataOutputStream(outputStream); + + name.writeToStream(dos); + dos.writeShort(type.getValue()); + dos.writeShort(clazzValue); + dos.writeInt((int) ttl); + + dos.writeShort(payloadData.length()); + payloadData.toOutputStream(dos); + } + + private transient byte[] bytes; + + public byte[] toByteArray() { + if (bytes == null) { + int totalSize = name.size() + + 10 // 2 byte short type + 2 byte short classValue + 4 byte int ttl + 2 byte short payload length. + + payloadData.length(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(totalSize); + DataOutputStream dos = new DataOutputStream(baos); + try { + toOutputStream(dos); + } catch (IOException e) { + // Should never happen. + throw new AssertionError(e); + } + bytes = baos.toByteArray(); + } + return bytes.clone(); + } + + /** + * Retrieve a textual representation of this resource record. + * @return String + */ + @Override + public String toString() { + return name.getRawAce() + ".\t" + ttl + '\t' + clazz + '\t' + type + '\t' + payloadData; + } + + /** + * Check if this record answers a given query. + * @param q The query. + * @return True if this record is a valid answer. + */ + public boolean isAnswer(Question q) { + return (q.type == type || q.type == TYPE.ANY) && + (q.clazz == clazz || q.clazz == CLASS.ANY) && + q.name.equals(name); + } + + /** + * See if this query/response was a unicast query (highest class bit set). + * @return True if it is a unicast query/response record. + */ + public boolean isUnicastQuery() { + return unicastQuery; + } + + /** + * The payload data, usually a subclass of data (A, AAAA, CNAME, ...). + * @return The payload data. + */ + public D getPayload() { + return payloadData; + } + + /** + * Retrieve the record ttl. + * @return The record ttl. + */ + public long getTtl() { + return ttl; + } + + /** + * Get the question asking for this resource record. This will return null if the record is not retrievable, i.e. + * {@link TYPE#OPT}. + * + * @return the question for this resource record or null. + */ + public Question getQuestion() { + switch (type) { + case OPT: + // OPT records are not retrievable. + return null; + case RRSIG: + RRSIG rrsig = (RRSIG) payloadData; + return new Question(name, rrsig.typeCovered, clazz); + default: + return new Question(name, type, clazz); + } + } + + public DnsMessage.Builder getQuestionMessage() { + Question question = getQuestion(); + if (question == null) { + return null; + } + return question.asMessageBuilder(); + } + + private transient Integer hashCodeCache; + + @Override + public int hashCode() { + if (hashCodeCache == null) { + int hashCode = 1; + hashCode = 37 * hashCode + name.hashCode(); + hashCode = 37 * hashCode + type.hashCode(); + hashCode = 37 * hashCode + clazz.hashCode(); + hashCode = 37 * hashCode + payloadData.hashCode(); + hashCodeCache = hashCode; + } + return hashCodeCache; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Record)) { + return false; + } + if (other == this) { + return true; + } + Record otherRecord = (Record) other; + if (!name.equals(otherRecord.name)) return false; + if (type != otherRecord.type) return false; + if (clazz != otherRecord.clazz) return false; + // Note that we do not compare the TTL here, since we consider two Records with everything but the TTL equal to + // be equal too. + if (!payloadData.equals(otherRecord.payloadData)) return false; + + return true; + } + + /** + * Return the record if possible as record with the given {@link Data} class. If the record does not hold payload of + * the given data class type, then {@code null} will be returned. + * + * @param dataClass a class of the {@link Data} type. + * @param a subtype of {@link Data}. + * @return the record with a specialized payload type or {@code null}. + * @see #as(Class) + */ + @SuppressWarnings("unchecked") + public Record ifPossibleAs(Class dataClass) { + if (type.dataClass == dataClass) { + return (Record) this; + } + return null; + } + + /** + * Return the record as record with the given {@link Data} class. If the record does not hold payload of + * the given data class type, then a {@link IllegalArgumentException} will be thrown. + * + * @param dataClass a class of the {@link Data} type. + * @param a subtype of {@link Data}. + * @return the record with a specialized payload type. + * @see #ifPossibleAs(Class) + */ + public Record as(Class dataClass) { + Record eRecord = ifPossibleAs(dataClass); + if (eRecord == null) { + throw new IllegalArgumentException("The instance " + this + " can not be cast to a Record with" + dataClass); + } + return eRecord; + } + + public static void filter(Collection> result, Class dataClass, + Collection> input) { + for (Record record : input) { + Record filteredRecord = record.ifPossibleAs(dataClass); + if (filteredRecord == null) + continue; + + result.add(filteredRecord); + } + } + + public static List> filter(Class dataClass, + Collection> input) { + List> result = new ArrayList<>(input.size()); + filter(result, dataClass, input); + return result; + } +} diff --git a/src/main/java/org/minidns/record/SOA.java b/src/main/java/org/minidns/record/SOA.java new file mode 100644 index 000000000..9872b624e --- /dev/null +++ b/src/main/java/org/minidns/record/SOA.java @@ -0,0 +1,116 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * SOA (start of authority) record payload. + */ +public class SOA extends Data { + + /** + * The domain name of the name server that was the original or primary source of data for this zone. + */ + public final DnsName mname; + + /** + * A domain name which specifies the mailbox of the person responsible for this zone. + */ + public final DnsName rname; + + /** + * The unsigned 32 bit version number of the original copy of the zone. Zone transfers preserve this value. This + * value wraps and should be compared using sequence space arithmetic. + */ + public final long /* unsigned int */ serial; + + /** + * A 32 bit time interval before the zone should be refreshed. + */ + public final int refresh; + + /** + * A 32 bit time interval that should elapse before a failed refresh should be retried. + */ + public final int retry; + + /** + * A 32 bit time value that specifies the upper limit on the time interval that can elapse before the zone is no + * longer authoritative. + */ + public final int expire; + + /** + * The unsigned 32 bit minimum TTL field that should be exported with any RR from this zone. + */ + public final long /* unsigned int */ minimum; + + public static SOA parse(DataInputStream dis, byte[] data) + throws IOException { + DnsName mname = DnsName.parse(dis, data); + DnsName rname = DnsName.parse(dis, data); + long serial = dis.readInt() & 0xFFFFFFFFL; + int refresh = dis.readInt(); + int retry = dis.readInt(); + int expire = dis.readInt(); + long minimum = dis.readInt() & 0xFFFFFFFFL; + return new SOA(mname, rname, serial, refresh, retry, expire, minimum); + } + + public SOA(String mname, String rname, long serial, int refresh, int retry, int expire, long minimum) { + this(DnsName.from(mname), DnsName.from(rname), serial, refresh, retry, expire, minimum); + } + + public SOA(DnsName mname, DnsName rname, long serial, int refresh, int retry, int expire, long minimum) { + this.mname = mname; + this.rname = rname; + this.serial = serial; + this.refresh = refresh; + this.retry = retry; + this.expire = expire; + this.minimum = minimum; + } + + @Override + public TYPE getType() { + return TYPE.SOA; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + mname.writeToStream(dos); + rname.writeToStream(dos); + dos.writeInt((int) serial); + dos.writeInt(refresh); + dos.writeInt(retry); + dos.writeInt(expire); + dos.writeInt((int) minimum); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(mname).append(". ") + .append(rname).append(". ") + .append(serial).append(' ') + .append(refresh).append(' ') + .append(retry).append(' ') + .append(expire).append(' ') + .append(minimum); + return sb.toString(); + } +} diff --git a/src/main/java/org/minidns/record/SRV.java b/src/main/java/org/minidns/record/SRV.java new file mode 100644 index 000000000..a5ec5be93 --- /dev/null +++ b/src/main/java/org/minidns/record/SRV.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.Record.TYPE; + +/** + * SRV record payload (service pointer). + */ +public class SRV extends RRWithTarget implements Comparable { + + /** + * The priority of this service. Lower values mean higher priority. + */ + public final int priority; + + /** + * The weight of this service. Services with the same priority should be + * balanced based on weight. + */ + public final int weight; + + /** + * The target port. + */ + public final int port; + + public static SRV parse(DataInputStream dis, byte[] data) + throws IOException { + int priority = dis.readUnsignedShort(); + int weight = dis.readUnsignedShort(); + int port = dis.readUnsignedShort(); + DnsName target = DnsName.parse(dis, data); + return new SRV(priority, weight, port, target); + } + + public SRV(int priority, int weight, int port, String target) { + this(priority, weight, port, DnsName.from(target)); + } + + public SRV(int priority, int weight, int port, DnsName target) { + super(target); + this.priority = priority; + this.weight = weight; + this.port = port; + } + + /** + * Check if the service is available at this domain. This checks f the target points to the root label. As per RFC + * 2782 the service is decidedly not available if there is only a single SRV answer pointing to the root label. From + * RFC 2782: + * + *
A Target of "." means that the service is decidedly not available at this domain.
+ * + * @return true if the service is available at this domain. + */ + public boolean isServiceAvailable() { + return !target.isRootLabel(); + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeShort(priority); + dos.writeShort(weight); + dos.writeShort(port); + super.serialize(dos); + } + + @Override + public String toString() { + return priority + " " + weight + " " + port + " " + target + "."; + } + + @Override + public TYPE getType() { + return TYPE.SRV; + } + + @Override + public int compareTo(SRV other) { + int res = other.priority - this.priority; + if (res == 0) { + res = this.weight - other.weight; + } + return res; + } +} diff --git a/src/main/java/org/minidns/record/TLSA.java b/src/main/java/org/minidns/record/TLSA.java new file mode 100644 index 000000000..a1d58debb --- /dev/null +++ b/src/main/java/org/minidns/record/TLSA.java @@ -0,0 +1,203 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class TLSA extends Data { + + private static final Map CERT_USAGE_LUT = new HashMap<>(); + + /** + * The certificate usage field. + * + * @see RFC 6698 § 2.1.1 + * + */ + public enum CertUsage { + + /** + * The given CA certificate (or its public key) MUST be found in at least + * one PKIX path to the end entity certificate. + * + *

+ * PKIX-TA(0) + *

+ */ + caConstraint((byte) 0), + + /** + * The given certificate (or its public key) MUST match the end entity + * certificate and MUST pass PKIX validation. Note that the requirement to pass + * PKIX validation is what makes this different from + * {@link #domainIssuedCertificate}. + * + *

+ * PKIX-EE(1) + *

+ */ + serviceCertificateConstraint((byte) 1), + + /** + * The given certificate (or its public key) MUST be used as trust anchor when + * validating the end entity certificate. + * + *

+ * DANE-TA(2) + *

+ */ + trustAnchorAssertion((byte) 2), + + /** + * The given certificate (or its public key) MUST match the end entity + * certificate. Unlike {@link #serviceCertificateConstraint}, this does not + * require PKIX validation. + * + *

+ * DANE-EE(3) + *

+ */ + domainIssuedCertificate((byte) 3), + ; + + public final byte byteValue; + + CertUsage(byte byteValue) { + this.byteValue = byteValue; + CERT_USAGE_LUT.put(byteValue, this); + } + } + + private static final Map SELECTOR_LUT = new HashMap<>(); + + public enum Selector { + fullCertificate((byte) 0), + subjectPublicKeyInfo((byte) 1), + ; + + public final byte byteValue; + + Selector(byte byteValue) { + this.byteValue = byteValue; + SELECTOR_LUT.put(byteValue, this); + } + } + + private static final Map MATCHING_TYPE_LUT = new HashMap<>(); + + public enum MatchingType { + noHash((byte) 0), + sha256((byte) 1), + sha512((byte) 2), + ; + + public final byte byteValue; + + MatchingType(byte byteValue) { + this.byteValue = byteValue; + MATCHING_TYPE_LUT.put(byteValue, this); + } + } + + static { + // Ensure that the LUTs are initialized. + CertUsage.values(); + Selector.values(); + MatchingType.values(); + } + + /** + * The provided association that will be used to match the certificate presented in + * the TLS handshake. + */ + public final byte certUsageByte; + + public final CertUsage certUsage; + + /** + * Which part of the TLS certificate presented by the server will be matched against the + * association data. + */ + public final byte selectorByte; + + public final Selector selector; + + /** + * How the certificate association is presented. + */ + public final byte matchingTypeByte; + + public final MatchingType matchingType; + + /** + * The "certificate association data" to be matched. + */ + private final byte[] certificateAssociation; + + public static TLSA parse(DataInputStream dis, int length) throws IOException { + byte certUsage = dis.readByte(); + byte selector = dis.readByte(); + byte matchingType = dis.readByte(); + byte[] certificateAssociation = new byte[length - 3]; + if (dis.read(certificateAssociation) != certificateAssociation.length) throw new IOException(); + return new TLSA(certUsage, selector, matchingType, certificateAssociation); + } + + TLSA(byte certUsageByte, byte selectorByte, byte matchingTypeByte, byte[] certificateAssociation) { + this.certUsageByte = certUsageByte; + this.certUsage = CERT_USAGE_LUT.get(certUsageByte); + + this.selectorByte = selectorByte; + this.selector = SELECTOR_LUT.get(selectorByte); + + this.matchingTypeByte = matchingTypeByte; + this.matchingType = MATCHING_TYPE_LUT.get(matchingTypeByte); + + this.certificateAssociation = certificateAssociation; + } + + @Override + public Record.TYPE getType() { + return Record.TYPE.TLSA; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.writeByte(certUsageByte); + dos.writeByte(selectorByte); + dos.writeByte(matchingTypeByte); + dos.write(certificateAssociation); + } + + @Override + @SuppressWarnings("UnnecessaryStringBuilder") + public String toString() { + return new StringBuilder() + .append(certUsageByte).append(' ') + .append(selectorByte).append(' ') + .append(matchingTypeByte).append(' ') + .append(new BigInteger(1, certificateAssociation).toString(16)).toString(); + } + + public byte[] getCertificateAssociation() { + return certificateAssociation.clone(); + } + + public boolean certificateAssociationEquals(byte[] otherCertificateAssociation) { + return Arrays.equals(certificateAssociation, otherCertificateAssociation); + } +} diff --git a/src/main/java/org/minidns/record/TXT.java b/src/main/java/org/minidns/record/TXT.java new file mode 100644 index 000000000..fe8c4f162 --- /dev/null +++ b/src/main/java/org/minidns/record/TXT.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.minidns.record.Record.TYPE; + +/** + * A TXT record. Actually a binary blob containing extents, each of which is a one-byte count + * followed by that many bytes of data, which can usually be interpreted as ASCII strings + * but not always. + */ +public class TXT extends Data { + + private final byte[] blob; + + public static TXT parse(DataInputStream dis, int length) throws IOException { + byte[] blob = new byte[length]; + dis.readFully(blob); + return new TXT(blob); + } + + public TXT(byte[] blob) { + this.blob = blob; + } + + public byte[] getBlob() { + return blob.clone(); + } + + private transient String textCache; + + public String getText() { + if (textCache == null) { + StringBuilder sb = new StringBuilder(); + Iterator it = getCharacterStrings().iterator(); + while (it.hasNext()) { + sb.append(it.next()); + if (it.hasNext()) { + sb.append(" / "); + } + } + textCache = sb.toString(); + } + return textCache; + } + + private transient List characterStringsCache; + + public List getCharacterStrings() { + if (characterStringsCache == null) { + List extents = getExtents(); + List characterStrings = new ArrayList<>(extents.size()); + for (byte[] extent : extents) { + characterStrings.add(new String(extent, StandardCharsets.UTF_8)); + } + + characterStringsCache = Collections.unmodifiableList(characterStrings); + } + return characterStringsCache; + } + + public List getExtents() { + ArrayList extents = new ArrayList(); + int segLength = 0; + for (int used = 0; used < blob.length; used += segLength) { + segLength = 0x00ff & blob[used]; + int end = ++used + segLength; + byte[] extent = Arrays.copyOfRange(blob, used, end); + extents.add(extent); + } + return extents; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.write(blob); + } + + @Override + public TYPE getType() { + return TYPE.TXT; + } + + @Override + public String toString() { + return "\"" + getText() + "\""; + } + +} diff --git a/src/main/java/org/minidns/record/UNKNOWN.java b/src/main/java/org/minidns/record/UNKNOWN.java new file mode 100644 index 000000000..c98b55e0a --- /dev/null +++ b/src/main/java/org/minidns/record/UNKNOWN.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.record; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.minidns.record.Record.TYPE; + +public final class UNKNOWN extends Data { + + private final TYPE type; + private final byte[] data; + + private UNKNOWN(DataInputStream dis, int payloadLength, TYPE type) throws IOException { + this.type = type; + this.data = new byte[payloadLength]; + dis.readFully(data); + } + + @Override + public TYPE getType() { + return type; + } + + @Override + public void serialize(DataOutputStream dos) throws IOException { + dos.write(data); + } + + public static UNKNOWN parse(DataInputStream dis, int payloadLength, TYPE type) + throws IOException { + return new UNKNOWN(dis, payloadLength, type); + } + +} diff --git a/src/main/java/org/minidns/source/AbstractDnsDataSource.java b/src/main/java/org/minidns/source/AbstractDnsDataSource.java new file mode 100644 index 000000000..9e86c5b22 --- /dev/null +++ b/src/main/java/org/minidns/source/AbstractDnsDataSource.java @@ -0,0 +1,113 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source; + +import org.minidns.DnsCache; +import org.minidns.MiniDnsFuture; +import org.minidns.MiniDnsFuture.InternalMiniDnsFuture; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; + +import java.io.IOException; +import java.net.InetAddress; + +public abstract class AbstractDnsDataSource implements DnsDataSource { + + @Override + public abstract DnsQueryResult query(DnsMessage message, InetAddress address, int port) throws IOException; + + @Override + public MiniDnsFuture queryAsync(DnsMessage message, InetAddress address, int port, OnResponseCallback onResponseCallback) { + InternalMiniDnsFuture future = new InternalMiniDnsFuture<>(); + DnsQueryResult result; + try { + result = query(message, address, port); + } catch (IOException e) { + future.setException(e); + return future; + } + future.setResult(result); + return future; + } + + protected int udpPayloadSize = 1232; + + /** + * DNS timeout. + */ + protected int timeout = 5000; + + @Override + public int getTimeout() { + return timeout; + } + + @Override + public void setTimeout(int timeout) { + if (timeout <= 0) { + throw new IllegalArgumentException("Timeout must be greater than zero"); + } + this.timeout = timeout; + } + + @Override + public int getUdpPayloadSize() { + return udpPayloadSize; + } + + public void setUdpPayloadSize(int udpPayloadSize) { + if (udpPayloadSize <= 0) { + throw new IllegalArgumentException("UDP payload size must be greater than zero"); + } + this.udpPayloadSize = udpPayloadSize; + } + + private DnsCache cache; + + protected final void cacheResult(DnsMessage request, DnsQueryResult response) { + final DnsCache activeCache = cache; + if (activeCache == null) { + return; + } + activeCache.put(request, response); + } + + public enum QueryMode { + /** + * Perform the query mode that is assumed "best" for that particular case. + */ + dontCare, + + /** + * Try UDP first, and if the result is bigger than the maximum UDP payload size, or if something else goes wrong, fallback to TCP. + */ + udpTcp, + + /** + * Always use only TCP when querying DNS servers. + */ + tcp, + } + + private QueryMode queryMode = QueryMode.dontCare; + + public void setQueryMode(QueryMode queryMode) { + if (queryMode == null) { + throw new IllegalArgumentException(); + } + this.queryMode = queryMode; + } + + public QueryMode getQueryMode() { + return queryMode; + } + +} diff --git a/src/main/java/org/minidns/source/DnsDataSource.java b/src/main/java/org/minidns/source/DnsDataSource.java new file mode 100644 index 000000000..4a51e6b27 --- /dev/null +++ b/src/main/java/org/minidns/source/DnsDataSource.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source; + +import java.io.IOException; +import java.net.InetAddress; + +import org.minidns.MiniDnsFuture; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; + +public interface DnsDataSource { + + DnsQueryResult query(DnsMessage message, InetAddress address, int port) throws IOException; + + MiniDnsFuture queryAsync(DnsMessage message, InetAddress address, int port, OnResponseCallback onResponseCallback); + + int getUdpPayloadSize(); + + /** + * Retrieve the current dns query timeout, in milliseconds. + * + * @return the current dns query timeout in milliseconds. + */ + int getTimeout(); + + /** + * Change the dns query timeout for all future queries. The timeout + * must be specified in milliseconds. + * + * @param timeout new dns query timeout in milliseconds. + */ + void setTimeout(int timeout); + + interface OnResponseCallback { + void onResponse(DnsMessage request, DnsQueryResult result); + } + +} diff --git a/src/main/java/org/minidns/source/NetworkDataSource.java b/src/main/java/org/minidns/source/NetworkDataSource.java new file mode 100644 index 000000000..62b16bd9d --- /dev/null +++ b/src/main/java/org/minidns/source/NetworkDataSource.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source; + +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult.QueryMethod; +import org.minidns.dnsqueryresult.StandardDnsQueryResult; +import org.minidns.util.MultipleIoException; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class NetworkDataSource extends AbstractDnsDataSource { + + protected static final Logger LOGGER = Logger.getLogger(NetworkDataSource.class.getName()); + + // TODO: Rename 'message' parameter to query. + @Override + public StandardDnsQueryResult query(DnsMessage message, InetAddress address, int port) throws IOException { + final QueryMode queryMode = getQueryMode(); + boolean doUdpFirst; + switch (queryMode) { + case dontCare: + case udpTcp: + doUdpFirst = true; + break; + case tcp: + doUdpFirst = false; + break; + default: + throw new IllegalStateException("Unsupported query mode: " + queryMode); + } + + List ioExceptions = new ArrayList<>(2); + DnsMessage dnsMessage = null; + + if (doUdpFirst) { + try { + dnsMessage = queryUdp(message, address, port); + } catch (IOException e) { + ioExceptions.add(e); + } + + // TODO: This null check could probably be removed by now. + if (dnsMessage != null && !dnsMessage.truncated) { + return new StandardDnsQueryResult(address, port, QueryMethod.udp, message, dnsMessage); + } + + assert dnsMessage == null || dnsMessage.truncated || ioExceptions.size() == 1; + LOGGER.log(Level.FINE, "Fallback to TCP because {0}", + new Object[] { dnsMessage != null ? "response is truncated" : ioExceptions.get(0) }); + } + + try { + dnsMessage = queryTcp(message, address, port); + } catch (IOException e) { + ioExceptions.add(e); + MultipleIoException.throwIfRequired(ioExceptions); + } + + return new StandardDnsQueryResult(address, port, QueryMethod.tcp, message, dnsMessage); + } + + protected DnsMessage queryUdp(DnsMessage message, InetAddress address, int port) throws IOException { + // TODO Use a try-with-resource statement here once miniDNS minimum + // required Android API level is >= 19 + DatagramSocket socket = null; + DatagramPacket packet = message.asDatagram(address, port); + byte[] buffer = new byte[udpPayloadSize]; + try { + socket = createDatagramSocket(); + socket.setSoTimeout(timeout); + socket.send(packet); + packet = new DatagramPacket(buffer, buffer.length); + socket.receive(packet); + DnsMessage dnsMessage = new DnsMessage(packet.getData()); + if (dnsMessage.id != message.id) { + throw new MiniDnsException.IdMismatch(message, dnsMessage); + } + return dnsMessage; + } finally { + if (socket != null) { + socket.close(); + } + } + } + + protected DnsMessage queryTcp(DnsMessage message, InetAddress address, int port) throws IOException { + // TODO Use a try-with-resource statement here once miniDNS minimum + // required Android API level is >= 19 + Socket socket = null; + try { + socket = createSocket(); + SocketAddress socketAddress = new InetSocketAddress(address, port); + socket.connect(socketAddress, timeout); + socket.setSoTimeout(timeout); + DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); + message.writeTo(dos); + dos.flush(); + DataInputStream dis = new DataInputStream(socket.getInputStream()); + int length = dis.readUnsignedShort(); + byte[] data = new byte[length]; + int read = 0; + while (read < length) { + read += dis.read(data, read, length - read); + } + DnsMessage dnsMessage = new DnsMessage(data); + if (dnsMessage.id != message.id) { + throw new MiniDnsException.IdMismatch(message, dnsMessage); + } + return dnsMessage; + } finally { + if (socket != null) { + socket.close(); + } + } + } + + /** + * Create a {@link Socket} using the system default {@link javax.net.SocketFactory}. + * + * @return The new {@link Socket} instance + */ + protected Socket createSocket() { + return new Socket(); + } + + /** + * Create a {@link DatagramSocket} using the system defaults. + * + * @return The new {@link DatagramSocket} instance + * @throws SocketException If creation of the {@link DatagramSocket} fails + */ + protected DatagramSocket createDatagramSocket() throws SocketException { + return new DatagramSocket(); + } +} diff --git a/src/main/java/org/minidns/source/NetworkDataSourceWithAccounting.java b/src/main/java/org/minidns/source/NetworkDataSourceWithAccounting.java new file mode 100644 index 000000000..c0d972402 --- /dev/null +++ b/src/main/java/org/minidns/source/NetworkDataSourceWithAccounting.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import org.minidns.AbstractDnsClient; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.StandardDnsQueryResult; + +public class NetworkDataSourceWithAccounting extends NetworkDataSource { + + private final AtomicInteger successfulQueries = new AtomicInteger(); + private final AtomicInteger responseSize = new AtomicInteger(); + private final AtomicInteger failedQueries = new AtomicInteger(); + + private final AtomicInteger successfulUdpQueries = new AtomicInteger(); + private final AtomicInteger udpResponseSize = new AtomicInteger(); + private final AtomicInteger failedUdpQueries = new AtomicInteger(); + + private final AtomicInteger successfulTcpQueries = new AtomicInteger(); + private final AtomicInteger tcpResponseSize = new AtomicInteger(); + private final AtomicInteger failedTcpQueries = new AtomicInteger(); + + @Override + public StandardDnsQueryResult query(DnsMessage message, InetAddress address, int port) throws IOException { + StandardDnsQueryResult response; + try { + response = super.query(message, address, port); + } catch (IOException e) { + failedQueries.incrementAndGet(); + throw e; + } + + successfulQueries.incrementAndGet(); + responseSize.addAndGet(response.response.toArray().length); + + return response; + } + + @Override + protected DnsMessage queryUdp(DnsMessage message, InetAddress address, int port) throws IOException { + DnsMessage response; + try { + response = super.queryUdp(message, address, port); + } catch (IOException e) { + failedUdpQueries.incrementAndGet(); + throw e; + } + + successfulUdpQueries.incrementAndGet(); + udpResponseSize.addAndGet(response.toArray().length); + + return response; + } + + @Override + protected DnsMessage queryTcp(DnsMessage message, InetAddress address, int port) throws IOException { + DnsMessage response; + try { + response = super.queryTcp(message, address, port); + } catch (IOException e) { + failedTcpQueries.incrementAndGet(); + throw e; + } + + successfulTcpQueries.incrementAndGet(); + tcpResponseSize.addAndGet(response.toArray().length); + + return response; + } + + public Stats getStats() { + return new Stats(this); + } + + public static NetworkDataSourceWithAccounting from(AbstractDnsClient client) { + DnsDataSource ds = client.getDataSource(); + if (ds instanceof NetworkDataSourceWithAccounting) { + return (NetworkDataSourceWithAccounting) ds; + } + return null; + } + + public static final class Stats { + public final int successfulQueries; + public final int responseSize; + public final int averageResponseSize; + public final int failedQueries; + + public final int successfulUdpQueries; + public final int udpResponseSize; + public final int averageUdpResponseSize; + public final int failedUdpQueries; + + public final int successfulTcpQueries; + public final int tcpResponseSize; + public final int averageTcpResponseSize; + public final int failedTcpQueries; + + private String stringCache; + + private Stats(NetworkDataSourceWithAccounting ndswa) { + successfulQueries = ndswa.successfulQueries.get(); + responseSize = ndswa.responseSize.get(); + failedQueries = ndswa.failedQueries.get(); + + successfulUdpQueries = ndswa.successfulUdpQueries.get(); + udpResponseSize = ndswa.udpResponseSize.get(); + failedUdpQueries = ndswa.failedUdpQueries.get(); + + successfulTcpQueries = ndswa.successfulTcpQueries.get(); + tcpResponseSize = ndswa.tcpResponseSize.get(); + failedTcpQueries = ndswa.failedTcpQueries.get(); + + // Calculated stats section + averageResponseSize = successfulQueries > 0 ? responseSize / successfulQueries : 0; + averageUdpResponseSize = successfulUdpQueries > 0 ? udpResponseSize / successfulUdpQueries : 0; + averageTcpResponseSize = successfulTcpQueries > 0 ? tcpResponseSize / successfulTcpQueries : 0; + } + + @Override + public String toString() { + if (stringCache != null) + return stringCache; + + StringBuilder sb = new StringBuilder(); + + sb.append("Stats\t").append("# Successful").append('\t').append("# Failed").append('\t') + .append("Resp. Size").append('\t').append("Avg. Resp. Size").append('\n'); + sb.append("Total\t").append(toString(successfulQueries)).append('\t').append(toString(failedQueries)) + .append('\t').append(toString(responseSize)).append('\t').append(toString(averageResponseSize)) + .append('\n'); + sb.append("UDP\t").append(toString(successfulUdpQueries)).append('\t').append(toString(failedUdpQueries)) + .append('\t').append(toString(udpResponseSize)).append('\t') + .append(toString(averageUdpResponseSize)).append('\n'); + sb.append("TCP\t").append(toString(successfulTcpQueries)).append('\t').append(toString(failedTcpQueries)) + .append('\t').append(toString(tcpResponseSize)).append('\t') + .append(toString(averageTcpResponseSize)).append('\n'); + + stringCache = sb.toString(); + return stringCache; + } + + private static String toString(int i) { + return String.format(Locale.US, "%,09d", i); + } + } +} diff --git a/src/main/java/org/minidns/source/async/AsyncDnsRequest.java b/src/main/java/org/minidns/source/async/AsyncDnsRequest.java new file mode 100644 index 000000000..e276646c3 --- /dev/null +++ b/src/main/java/org/minidns/source/async/AsyncDnsRequest.java @@ -0,0 +1,547 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source.async; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.minidns.MiniDnsException; +import org.minidns.MiniDnsFuture; +import org.minidns.MiniDnsFuture.InternalMiniDnsFuture; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsqueryresult.DnsQueryResult.QueryMethod; +import org.minidns.dnsqueryresult.StandardDnsQueryResult; +import org.minidns.source.DnsDataSource.OnResponseCallback; +import org.minidns.source.AbstractDnsDataSource.QueryMode; +import org.minidns.util.MultipleIoException; + +/** + * A DNS request that is performed asynchronously. + */ +public class AsyncDnsRequest { + + private static final Logger LOGGER = Logger.getLogger(AsyncDnsRequest.class.getName()); + + private final InternalMiniDnsFuture future = new InternalMiniDnsFuture() { + @SuppressWarnings("UnsynchronizedOverridesSynchronized") + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean res = super.cancel(mayInterruptIfRunning); + cancelAsyncDnsRequest(); + return res; + } + }; + + private final DnsMessage request; + + private final int udpPayloadSize; + + private final InetSocketAddress socketAddress; + + private final AsyncNetworkDataSource asyncNds; + + private final OnResponseCallback onResponseCallback; + + private final boolean skipUdp; + + private ByteBuffer writeBuffer; + + private List exceptions; + + private SelectionKey selectionKey; + + final long deadline; + + /** + * Creates a new AsyncDnsRequest instance. + * + * @param request the DNS message of the request. + * @param inetAddress The IP address of the DNS server to ask. + * @param port The port of the DNS server to ask. + * @param udpPayloadSize The configured UDP payload size. + * @param asyncNds A reference to the {@link AsyncNetworkDataSource} instance manageing the requests. + * @param onResponseCallback the optional callback when a response was received. + */ + AsyncDnsRequest(DnsMessage request, InetAddress inetAddress, int port, int udpPayloadSize, AsyncNetworkDataSource asyncNds, OnResponseCallback onResponseCallback) { + this.request = request; + this.udpPayloadSize = udpPayloadSize; + this.asyncNds = asyncNds; + this.onResponseCallback = onResponseCallback; + + final QueryMode queryMode = asyncNds.getQueryMode(); + switch (queryMode) { + case dontCare: + case udpTcp: + skipUdp = false; + break; + case tcp: + skipUdp = true; + break; + default: + throw new IllegalStateException("Unsupported query mode: " + queryMode); + + } + deadline = System.currentTimeMillis() + asyncNds.getTimeout(); + socketAddress = new InetSocketAddress(inetAddress, port); + } + + private void ensureWriteBufferIsInitialized() { + if (writeBuffer != null) { + if (!writeBuffer.hasRemaining()) { + ((java.nio.Buffer) writeBuffer).rewind(); + } + return; + } + writeBuffer = request.getInByteBuffer(); + } + + private synchronized void cancelAsyncDnsRequest() { + if (selectionKey != null) { + selectionKey.cancel(); + } + asyncNds.cancelled(this); + } + + private synchronized void registerWithSelector(SelectableChannel channel, int ops, ChannelSelectedHandler handler) + throws ClosedChannelException { + if (future.isCancelled()) { + return; + } + selectionKey = asyncNds.registerWithSelector(channel, ops, handler); + } + + private void addException(IOException e) { + if (exceptions == null) { + exceptions = new ArrayList<>(4); + } + exceptions.add(e); + } + + private void gotResult(DnsQueryResult result) { + if (onResponseCallback != null) { + onResponseCallback.onResponse(request, result); + } + asyncNds.finished(this); + future.setResult(result); + } + + MiniDnsFuture getFuture() { + return future; + } + + boolean wasDeadlineMissedAndFutureNotified() { + if (System.currentTimeMillis() < deadline) { + return false; + } + + future.setException(new IOException("Timeout")); + return true; + } + + void startHandling() { + if (!skipUdp) { + startUdpRequest(); + } else { + startTcpRequest(); + } + } + + private void abortRequestAndCleanup(Channel channel, String errorMessage, IOException exception) { + if (exception == null) { + // TODO: Can this case be removed? Is 'exception' ever null? + LOGGER.info("Exception was null in abortRequestAndCleanup()"); + exception = new IOException(errorMessage); + } + LOGGER.log(Level.SEVERE, "Error connecting " + channel + ": " + errorMessage, exception); + addException(exception); + + if (selectionKey != null) { + selectionKey.cancel(); + } + + if (channel != null && channel.isOpen()) { + try { + channel.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Exception closing socket channel", e); + addException(e); + } + } + } + + private void abortUdpRequestAndCleanup(DatagramChannel datagramChannel, String errorMessage, IOException exception) { + abortRequestAndCleanup(datagramChannel, errorMessage, exception); + startTcpRequest(); + } + + private void startUdpRequest() { + if (future.isCancelled()) { + return; + } + + DatagramChannel datagramChannel; + try { + datagramChannel = DatagramChannel.open(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Exception opening datagram channel", e); + addException(e); + startTcpRequest(); + return; + } + + try { + datagramChannel.configureBlocking(false); + } catch (IOException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception configuring datagram channel", e); + return; + } + + try { + datagramChannel.connect(socketAddress); + } catch (IOException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception connecting datagram channel to " + socketAddress, e); + return; + } + + try { + registerWithSelector(datagramChannel, SelectionKey.OP_WRITE, new UdpWritableChannelSelectedHandler(future)); + } catch (ClosedChannelException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception registering datagram channel for OP_WRITE", e); + return; + } + } + + class UdpWritableChannelSelectedHandler extends ChannelSelectedHandler { + + UdpWritableChannelSelectedHandler(Future future) { + super(future); + } + + @Override + public void handleChannelSelectedAndNotCancelled(SelectableChannel channel, SelectionKey selectionKey) { + DatagramChannel datagramChannel = (DatagramChannel) channel; + + ensureWriteBufferIsInitialized(); + + try { + datagramChannel.write(writeBuffer); + } catch (IOException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception writing to datagram channel", e); + return; + } + + if (writeBuffer.hasRemaining()) { + try { + registerWithSelector(datagramChannel, SelectionKey.OP_WRITE, this); + } catch (ClosedChannelException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception registering datagram channel for OP_WRITE", e); + } + return; + } + + try { + registerWithSelector(datagramChannel, SelectionKey.OP_READ, new UdpReadableChannelSelectedHandler(future)); + } catch (ClosedChannelException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception registering datagram channel for OP_READ", e); + return; + } + } + + } + + class UdpReadableChannelSelectedHandler extends ChannelSelectedHandler { + + UdpReadableChannelSelectedHandler(Future future) { + super(future); + } + + final ByteBuffer byteBuffer = ByteBuffer.allocate(udpPayloadSize); + + @Override + public void handleChannelSelectedAndNotCancelled(SelectableChannel channel, SelectionKey selectionKey) { + DatagramChannel datagramChannel = (DatagramChannel) channel; + + try { + datagramChannel.read(byteBuffer); + } catch (IOException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception reading from datagram channel", e); + return; + } + + selectionKey.cancel(); + try { + datagramChannel.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Exception closing datagram channel", e); + addException(e); + } + + DnsMessage response; + try { + response = new DnsMessage(byteBuffer.array()); + } catch (IOException e) { + abortUdpRequestAndCleanup(datagramChannel, "Exception constructing dns message from datagram channel", e); + return; + } + + if (response.id != request.id) { + addException(new MiniDnsException.IdMismatch(request, response)); + startTcpRequest(); + return; + } + + if (response.truncated) { + startTcpRequest(); + return; + } + + DnsQueryResult result = new StandardDnsQueryResult(socketAddress.getAddress(), socketAddress.getPort(), + QueryMethod.asyncUdp, request, response); + gotResult(result); + } + } + + private void abortTcpRequestAndCleanup(SocketChannel socketChannel, String errorMessage, IOException exception) { + abortRequestAndCleanup(socketChannel, errorMessage, exception); + future.setException(MultipleIoException.toIOException(exceptions)); + } + + private void startTcpRequest() { + SocketChannel socketChannel = null; + try { + socketChannel = SocketChannel.open(); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception opening socket channel", e); + return; + } + + try { + socketChannel.configureBlocking(false); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception configuring socket channel", e); + return; + } + + try { + registerWithSelector(socketChannel, SelectionKey.OP_CONNECT, new TcpConnectedChannelSelectedHandler(future)); + } catch (ClosedChannelException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception registering socket channel", e); + return; + } + + try { + socketChannel.connect(socketAddress); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception connecting socket channel to " + socketAddress, e); + return; + } + } + + class TcpConnectedChannelSelectedHandler extends ChannelSelectedHandler { + + TcpConnectedChannelSelectedHandler(Future future) { + super(future); + } + + @Override + public void handleChannelSelectedAndNotCancelled(SelectableChannel channel, SelectionKey selectionKey) { + SocketChannel socketChannel = (SocketChannel) channel; + + boolean connected; + try { + connected = socketChannel.finishConnect(); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception finish connecting socket channel", e); + return; + } + + assert connected; + + try { + registerWithSelector(socketChannel, SelectionKey.OP_WRITE, new TcpWritableChannelSelectedHandler(future)); + } catch (ClosedChannelException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception registering socket channel for OP_WRITE", e); + return; + } + } + + } + + class TcpWritableChannelSelectedHandler extends ChannelSelectedHandler { + + TcpWritableChannelSelectedHandler(Future future) { + super(future); + } + + /** + * ByteBuffer array of length 2. First buffer is for the length of the DNS message, second one is the actual DNS message. + */ + private ByteBuffer[] writeBuffers; + + @Override + public void handleChannelSelectedAndNotCancelled(SelectableChannel channel, SelectionKey selectionKey) { + SocketChannel socketChannel = (SocketChannel) channel; + + if (writeBuffers == null) { + ensureWriteBufferIsInitialized(); + + ByteBuffer messageLengthByteBuffer = ByteBuffer.allocate(2); + int messageLength = writeBuffer.capacity(); + assert messageLength <= Short.MAX_VALUE; + messageLengthByteBuffer.putShort((short) (messageLength & 0xffff)); + ((java.nio.Buffer) messageLengthByteBuffer).rewind(); + + writeBuffers = new ByteBuffer[2]; + writeBuffers[0] = messageLengthByteBuffer; + writeBuffers[1] = writeBuffer; + } + + try { + socketChannel.write(writeBuffers); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception writing to socket channel", e); + return; + } + + if (moreToWrite()) { + try { + registerWithSelector(socketChannel, SelectionKey.OP_WRITE, this); + } catch (ClosedChannelException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception registering socket channel for OP_WRITE", e); + } + return; + } + + try { + registerWithSelector(socketChannel, SelectionKey.OP_READ, new TcpReadableChannelSelectedHandler(future)); + } catch (ClosedChannelException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception registering socket channel for OP_READ", e); + return; + } + } + + private boolean moreToWrite() { + for (int i = 0; i < writeBuffers.length; i++) { + if (writeBuffers[i].hasRemaining()) { + return true; + } + } + return false; + } + } + + class TcpReadableChannelSelectedHandler extends ChannelSelectedHandler { + + TcpReadableChannelSelectedHandler(Future future) { + super(future); + } + + final ByteBuffer messageLengthByteBuffer = ByteBuffer.allocate(2); + + ByteBuffer byteBuffer; + + @Override + public void handleChannelSelectedAndNotCancelled(SelectableChannel channel, SelectionKey selectionKey) { + SocketChannel socketChannel = (SocketChannel) channel; + + int bytesRead; + if (byteBuffer == null) { + try { + bytesRead = socketChannel.read(messageLengthByteBuffer); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception reading from socket channel", e); + return; + } + + if (bytesRead < 0) { + abortTcpRequestAndCleanup(socketChannel, "Socket closed by remote host " + socketAddress, null); + return; + } + + if (messageLengthByteBuffer.hasRemaining()) { + try { + registerWithSelector(socketChannel, SelectionKey.OP_READ, this); + } catch (ClosedChannelException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception registering socket channel for OP_READ", e); + } + return; + } + + ((java.nio.Buffer) messageLengthByteBuffer).rewind(); + short messageLengthSignedShort = messageLengthByteBuffer.getShort(); + int messageLength = messageLengthSignedShort & 0xffff; + byteBuffer = ByteBuffer.allocate(messageLength); + } + + try { + bytesRead = socketChannel.read(byteBuffer); + } catch (IOException e) { + throw new Error(e); + } + + if (bytesRead < 0) { + abortTcpRequestAndCleanup(socketChannel, "Socket closed by remote host " + socketAddress, null); + return; + } + + if (byteBuffer.hasRemaining()) { + try { + registerWithSelector(socketChannel, SelectionKey.OP_READ, this); + } catch (ClosedChannelException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception registering socket channel for OP_READ", e); + } + return; + } + + selectionKey.cancel(); + try { + socketChannel.close(); + } catch (IOException e) { + addException(e); + } + + DnsMessage response; + try { + response = new DnsMessage(byteBuffer.array()); + } catch (IOException e) { + abortTcpRequestAndCleanup(socketChannel, "Exception creating DNS message form socket channel bytes", e); + return; + } + + if (request.id != response.id) { + MiniDnsException idMismatchException = new MiniDnsException.IdMismatch(request, response); + addException(idMismatchException); + AsyncDnsRequest.this.future.setException(MultipleIoException.toIOException(exceptions)); + return; + } + + DnsQueryResult result = new StandardDnsQueryResult(socketAddress.getAddress(), socketAddress.getPort(), + QueryMethod.asyncTcp, request, response); + gotResult(result); + } + + } + +} diff --git a/src/main/java/org/minidns/source/async/AsyncNetworkDataSource.java b/src/main/java/org/minidns/source/async/AsyncNetworkDataSource.java new file mode 100644 index 000000000..acec52341 --- /dev/null +++ b/src/main/java/org/minidns/source/async/AsyncNetworkDataSource.java @@ -0,0 +1,311 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source.async; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.minidns.MiniDnsFuture; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.source.AbstractDnsDataSource; + +/** + * A DNS data sources that resolves requests via the network asynchronously. + */ +public class AsyncNetworkDataSource extends AbstractDnsDataSource { + + /** + * The logger of this data source. + */ + protected static final Logger LOGGER = Logger.getLogger(AsyncNetworkDataSource.class.getName()); + + private static final int REACTOR_THREAD_COUNT = 1; + + private static final Queue INCOMING_REQUESTS = new ConcurrentLinkedQueue<>(); + + private static final Selector SELECTOR; + + private static final Lock REGISTRATION_LOCK = new ReentrantLock(); + + private static final Queue PENDING_SELECTION_KEYS = new ConcurrentLinkedQueue<>(); + + private static final Thread[] REACTOR_THREADS = new Thread[REACTOR_THREAD_COUNT]; + + private static final PriorityQueue DEADLINE_QUEUE = new PriorityQueue<>(16, new Comparator() { + @Override + public int compare(AsyncDnsRequest o1, AsyncDnsRequest o2) { + if (o1.deadline > o2.deadline) { + return 1; + } else if (o1.deadline < o2.deadline) { + return -1; + } + return 0; + } + }); + + static { + try { + SELECTOR = Selector.open(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + for (int i = 0; i < REACTOR_THREAD_COUNT; i++) { + Thread reactorThread = new Thread(new Reactor()); + reactorThread.setDaemon(true); + reactorThread.setName("MiniDNS Reactor Thread #" + i); + reactorThread.start(); + REACTOR_THREADS[i] = reactorThread; + } + } + + @Override + public MiniDnsFuture queryAsync(DnsMessage message, InetAddress address, int port, OnResponseCallback onResponseCallback) { + AsyncDnsRequest asyncDnsRequest = new AsyncDnsRequest(message, address, port, udpPayloadSize, this, onResponseCallback); + INCOMING_REQUESTS.add(asyncDnsRequest); + synchronized (DEADLINE_QUEUE) { + DEADLINE_QUEUE.add(asyncDnsRequest); + } + SELECTOR.wakeup(); + return asyncDnsRequest.getFuture(); + } + + @Override + public DnsQueryResult query(DnsMessage message, InetAddress address, int port) throws IOException { + MiniDnsFuture future = queryAsync(message, address, port, null); + try { + return future.get(); + } catch (InterruptedException e) { + // This should never happen. + throw new AssertionError(e); + } catch (ExecutionException e) { + Throwable wrappedThrowable = e.getCause(); + if (wrappedThrowable instanceof IOException) { + throw (IOException) wrappedThrowable; + } + // This should never happen. + throw new AssertionError(e); + } + } + + SelectionKey registerWithSelector(SelectableChannel channel, int ops, Object attachment) throws ClosedChannelException { + REGISTRATION_LOCK.lock(); + try { + SELECTOR.wakeup(); + return channel.register(SELECTOR, ops, attachment); + } finally { + REGISTRATION_LOCK.unlock(); + } + } + + void finished(AsyncDnsRequest asyncDnsRequest) { + synchronized (DEADLINE_QUEUE) { + DEADLINE_QUEUE.remove(asyncDnsRequest); + } + } + + void cancelled(AsyncDnsRequest asyncDnsRequest) { + finished(asyncDnsRequest); + // Wakeup since the async DNS request was removed from the deadline queue. + SELECTOR.wakeup(); + } + + private static final class Reactor implements Runnable { + @Override + public void run() { + while (!Thread.interrupted()) { + Collection mySelectedKeys = performSelect(); + handleSelectedKeys(mySelectedKeys); + + handlePendingSelectionKeys(); + + handleIncomingRequests(); + } + } + + private static void handleSelectedKeys(Collection selectedKeys) { + for (SelectionKey selectionKey : selectedKeys) { + ChannelSelectedHandler channelSelectedHandler = (ChannelSelectedHandler) selectionKey.attachment(); + SelectableChannel channel = selectionKey.channel(); + channelSelectedHandler.handleChannelSelected(channel, selectionKey); + } + } + + @SuppressWarnings({"LockNotBeforeTry", "MixedMutabilityReturnType"}) + private static Collection performSelect() { + AsyncDnsRequest nearestDeadline = null; + AsyncDnsRequest nextInQueue; + + synchronized (DEADLINE_QUEUE) { + while ((nextInQueue = DEADLINE_QUEUE.peek()) != null) { + if (nextInQueue.wasDeadlineMissedAndFutureNotified()) { + // We notified the future, associated with the AsyncDnsRequest nearestDeadline, + // that the deadline has passed, hence remove it from the queue. + DEADLINE_QUEUE.poll(); + } else { + // We found a nearest deadline that has not yet passed, break out of the loop. + nearestDeadline = nextInQueue; + break; + } + } + + } + + long selectWait; + if (nearestDeadline == null) { + // There is no deadline, wait indefinitely in select(). + selectWait = 0; + } else { + // There is a deadline in the future, only block in select() until the deadline. + selectWait = nextInQueue.deadline - System.currentTimeMillis(); + if (selectWait < 0) { + // We already have a missed deadline. Do not call select() and handle the tasks which are past their + // deadline. + return Collections.emptyList(); + } + } + + List selectedKeys; + int newSelectedKeysCount; + synchronized (SELECTOR) { + // Ensure that a wakeup() in registerWithSelector() gives the corresponding + // register() in the same method the chance to actually register the channel. In + // other words: This construct ensure that there is never another select() + // between a corresponding wakeup() and register() calls. + // See also https://stackoverflow.com/a/1112809/194894 + REGISTRATION_LOCK.lock(); + REGISTRATION_LOCK.unlock(); + + try { + newSelectedKeysCount = SELECTOR.select(selectWait); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "IOException while using select()", e); + return Collections.emptyList(); + } + + if (newSelectedKeysCount == 0) { + return Collections.emptyList(); + } + + Set selectedKeySet = SELECTOR.selectedKeys(); + for (SelectionKey selectionKey : selectedKeySet) { + selectionKey.interestOps(0); + } + + selectedKeys = new ArrayList<>(selectedKeySet.size()); + selectedKeys.addAll(selectedKeySet); + selectedKeySet.clear(); + } + + int selectedKeysCount = selectedKeys.size(); + + final Level LOG_LEVEL = Level.FINER; + if (LOGGER.isLoggable(LOG_LEVEL)) { + LOGGER.log(LOG_LEVEL, "New selected key count: " + newSelectedKeysCount + ". Total selected key count " + + selectedKeysCount); + } + + int myKeyCount = selectedKeysCount / REACTOR_THREAD_COUNT; + Collection mySelectedKeys = new ArrayList<>(myKeyCount); + Iterator it = selectedKeys.iterator(); + for (int i = 0; i < myKeyCount; i++) { + SelectionKey selectionKey = it.next(); + mySelectedKeys.add(selectionKey); + } + while (it.hasNext()) { + // Drain to PENDING_SELECTION_KEYS + SelectionKey selectionKey = it.next(); + PENDING_SELECTION_KEYS.add(selectionKey); + } + return mySelectedKeys; + } + + private static void handlePendingSelectionKeys() { + int pendingSelectionKeysSize = PENDING_SELECTION_KEYS.size(); + if (pendingSelectionKeysSize == 0) { + return; + } + + int myKeyCount = pendingSelectionKeysSize / REACTOR_THREAD_COUNT; + Collection selectedKeys = new ArrayList<>(myKeyCount); + for (int i = 0; i < myKeyCount; i++) { + SelectionKey selectionKey = PENDING_SELECTION_KEYS.poll(); + if (selectionKey == null) { + // We lost a race :) + break; + } + selectedKeys.add(selectionKey); + } + + if (!PENDING_SELECTION_KEYS.isEmpty()) { + // There is more work in the pending selection keys queue, wakeup another thread to handle it. + SELECTOR.wakeup(); + } + + handleSelectedKeys(selectedKeys); + } + + private static void handleIncomingRequests() { + int incomingRequestsSize = INCOMING_REQUESTS.size(); + if (incomingRequestsSize == 0) { + return; + } + + int myRequestsCount = incomingRequestsSize / REACTOR_THREAD_COUNT; + // The division could result in myRequestCount being zero despite pending incoming + // requests. Therefore, ensure this thread tries to get at least one incoming + // request by invoking poll(). Otherwise, we might end up in a busy loop + // where myRequestCount is zero, and this thread invokes a selector.wakeup() below + // because incomingRequestsSize is not empty, but the woken-up reactor thread + // will end up with myRequestCount being zero again, restarting the busy-loop cycle. + if (myRequestsCount == 0) myRequestsCount = 1; + Collection requests = new ArrayList<>(myRequestsCount); + for (int i = 0; i < myRequestsCount; i++) { + AsyncDnsRequest asyncDnsRequest = INCOMING_REQUESTS.poll(); + if (asyncDnsRequest == null) { + // We lost a race :) + break; + } + requests.add(asyncDnsRequest); + } + + if (!INCOMING_REQUESTS.isEmpty()) { + SELECTOR.wakeup(); + } + + for (AsyncDnsRequest asyncDnsRequest : requests) { + asyncDnsRequest.startHandling(); + } + } + + } + +} diff --git a/src/main/java/org/minidns/source/async/ChannelSelectedHandler.java b/src/main/java/org/minidns/source/async/ChannelSelectedHandler.java new file mode 100644 index 000000000..8088a8427 --- /dev/null +++ b/src/main/java/org/minidns/source/async/ChannelSelectedHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.source.async; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; + +abstract class ChannelSelectedHandler { + + private static final Logger LOGGER = Logger.getLogger(ChannelSelectedHandler.class.getName()); + + final Future future; + + ChannelSelectedHandler(Future future) { + this.future = future; + } + + void handleChannelSelected(SelectableChannel channel, SelectionKey selectionKey) { + if (future.isCancelled()) { + try { + channel.close(); + } catch (IOException e) { + LOGGER.log(Level.INFO, "Could not close channel", e); + } + return; + } + handleChannelSelectedAndNotCancelled(channel, selectionKey); + } + + protected abstract void handleChannelSelectedAndNotCancelled(SelectableChannel channel, SelectionKey selectionKey); + +} diff --git a/src/main/java/org/minidns/util/Base32.java b/src/main/java/org/minidns/util/Base32.java new file mode 100644 index 000000000..037f71fc3 --- /dev/null +++ b/src/main/java/org/minidns/util/Base32.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +/** + * Very minimal Base32 encoder. + */ +public final class Base32 { + private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + private static final String PADDING = "======"; + + /** + * Do not allow to instantiate Base32 + */ + private Base32() { + } + + public static String encodeToString(byte[] bytes) { + int paddingCount = (int) (8 - (bytes.length % 5) * 1.6) % 8; + byte[] padded = new byte[bytes.length + paddingCount]; + System.arraycopy(bytes, 0, padded, 0, bytes.length); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i += 5) { + long j = ((long) (padded[i] & 0xff) << 32) + ((long) (padded[i + 1] & 0xff) << 24) + + ((padded[i + 2] & 0xff) << 16) + ((padded[i + 3] & 0xff) << 8) + (padded[i + 4] & 0xff); + sb.append(ALPHABET.charAt((int) ((j >> 35) & 0x1f))).append(ALPHABET.charAt((int) ((j >> 30) & 0x1f))) + .append(ALPHABET.charAt((int) ((j >> 25) & 0x1f))).append(ALPHABET.charAt((int) ((j >> 20) & 0x1f))) + .append(ALPHABET.charAt((int) ((j >> 15) & 0x1f))).append(ALPHABET.charAt((int) ((j >> 10) & 0x1f))) + .append(ALPHABET.charAt((int) ((j >> 5) & 0x1f))).append(ALPHABET.charAt((int) (j & 0x1f))); + } + return sb.substring(0, sb.length() - paddingCount) + PADDING.substring(0, paddingCount); + } +} diff --git a/src/main/java/org/minidns/util/Base64.java b/src/main/java/org/minidns/util/Base64.java new file mode 100644 index 000000000..73c56d1cf --- /dev/null +++ b/src/main/java/org/minidns/util/Base64.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +/** + * Very minimal Base64 encoder. + */ +public final class Base64 { + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private static final String PADDING = "=="; + + /** + * Do not allow to instantiate Base64 + */ + private Base64() { + } + + public static String encodeToString(byte[] bytes) { + int paddingCount = (3 - (bytes.length % 3)) % 3; + byte[] padded = new byte[bytes.length + paddingCount]; + System.arraycopy(bytes, 0, padded, 0, bytes.length); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i += 3) { + int j = ((padded[i] & 0xff) << 16) + ((padded[i + 1] & 0xff) << 8) + (padded[i + 2] & 0xff); + sb.append(ALPHABET.charAt((j >> 18) & 0x3f)).append(ALPHABET.charAt((j >> 12) & 0x3f)) + .append(ALPHABET.charAt((j >> 6) & 0x3f)).append(ALPHABET.charAt(j & 0x3f)); + } + return sb.substring(0, sb.length() - paddingCount) + PADDING.substring(0, paddingCount); + } +} diff --git a/src/main/java/org/minidns/util/CallbackRecipient.java b/src/main/java/org/minidns/util/CallbackRecipient.java new file mode 100644 index 000000000..f1f158aa4 --- /dev/null +++ b/src/main/java/org/minidns/util/CallbackRecipient.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +/** + * A recipient of success and exception callbacks. + * + * @param the type of the success value. + * @param the type of the exception. + */ +public interface CallbackRecipient { + + CallbackRecipient onSuccess(SuccessCallback successCallback); + + CallbackRecipient onError(ExceptionCallback exceptionCallback); + +} diff --git a/src/main/java/org/minidns/util/CollectionsUtil.java b/src/main/java/org/minidns/util/CollectionsUtil.java new file mode 100644 index 000000000..304d7404a --- /dev/null +++ b/src/main/java/org/minidns/util/CollectionsUtil.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +import java.util.Iterator; +import java.util.Random; +import java.util.Set; + +public class CollectionsUtil { + + public static T getRandomFrom(Set set, Random random) { + int randomIndex = random.nextInt(set.size()); + Iterator iterator = set.iterator(); + for (int i = 0; i < randomIndex; i++) { + if (!iterator.hasNext()) break; + iterator.next(); + } + return iterator.next(); + } +} diff --git a/src/main/java/org/minidns/util/ExceptionCallback.java b/src/main/java/org/minidns/util/ExceptionCallback.java new file mode 100644 index 000000000..9790f6541 --- /dev/null +++ b/src/main/java/org/minidns/util/ExceptionCallback.java @@ -0,0 +1,17 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +public interface ExceptionCallback { + + void processException(E exception); + +} diff --git a/src/main/java/org/minidns/util/Hex.java b/src/main/java/org/minidns/util/Hex.java new file mode 100644 index 000000000..f33b10b2d --- /dev/null +++ b/src/main/java/org/minidns/util/Hex.java @@ -0,0 +1,22 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +public class Hex { + + public static StringBuilder from(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb; + } +} diff --git a/src/main/java/org/minidns/util/InetAddressUtil.java b/src/main/java/org/minidns/util/InetAddressUtil.java new file mode 100644 index 000000000..c0c12df57 --- /dev/null +++ b/src/main/java/org/minidns/util/InetAddressUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +import org.minidns.dnsname.DnsName; + +public class InetAddressUtil { + + public static Inet4Address ipv4From(CharSequence cs) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(cs.toString()); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(e); + } + if (inetAddress instanceof Inet4Address) { + return (Inet4Address) inetAddress; + } + throw new IllegalArgumentException(); + } + + public static Inet6Address ipv6From(CharSequence cs) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(cs.toString()); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(e); + } + if (inetAddress instanceof Inet6Address) { + return (Inet6Address) inetAddress; + } + throw new IllegalArgumentException(); + } + + // IPV4_REGEX from http://stackoverflow.com/a/46168/194894 by Kevin Wong (http://stackoverflow.com/users/4792/kevin-wong) licensed under + // CC BY-SA 3.0. + private static final Pattern IPV4_PATTERN = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + + public static boolean isIpV4Address(CharSequence address) { + if (address == null) { + return false; + } + return IPV4_PATTERN.matcher(address).matches(); + } + + // IPv6 Regular Expression from http://stackoverflow.com/a/17871737/194894 by David M. Syzdek + // (http://stackoverflow.com/users/903194/david-m-syzdek) licensed under CC BY-SA 3.0. + private static final Pattern IPV6_PATTERN = Pattern.compile( + "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); + + public static boolean isIpV6Address(CharSequence address) { + if (address == null) { + return false; + } + return IPV6_PATTERN.matcher(address).matches(); + } + + public static boolean isIpAddress(CharSequence address) { + return isIpV6Address(address) || isIpV4Address(address); + } + + public static InetAddress convertToInetAddressIfPossible(CharSequence address) { + if (!isIpAddress(address)) { + return null; + } + + String addressString = address.toString(); + try { + return InetAddress.getByName(addressString); + } catch (UnknownHostException e) { + // Should never happen. + throw new AssertionError(e); + } + } + + public static DnsName reverseIpAddressOf(Inet6Address inet6Address) { + final String ipAddress = inet6Address.getHostAddress(); + final String[] ipAddressParts = ipAddress.split(":"); + + String[] parts = new String[32]; + int currentPartNum = 0; + for (int i = ipAddressParts.length - 1; i >= 0; i--) { + final String currentPart = ipAddressParts[i]; + final int missingPlaces = 4 - currentPart.length(); + for (int j = 0; j < missingPlaces; j++) { + parts[currentPartNum++] = "0"; + } + for (int j = 0; j < currentPart.length(); j++) { + parts[currentPartNum++] = Character.toString(currentPart.charAt(j)); + } + } + + return DnsName.from(parts); + } + + public static DnsName reverseIpAddressOf(Inet4Address inet4Address) { + final String[] ipAddressParts = inet4Address.getHostAddress().split("\\."); + assert ipAddressParts.length == 4; + + return DnsName.from(ipAddressParts); + } +} diff --git a/src/main/java/org/minidns/util/MultipleIoException.java b/src/main/java/org/minidns/util/MultipleIoException.java new file mode 100644 index 000000000..8b7c81c15 --- /dev/null +++ b/src/main/java/org/minidns/util/MultipleIoException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +public final class MultipleIoException extends IOException { + + /** + * + */ + private static final long serialVersionUID = -5932211337552319515L; + + private final List ioExceptions; + + private MultipleIoException(List ioExceptions) { + super(getMessage(ioExceptions)); + assert !ioExceptions.isEmpty(); + this.ioExceptions = Collections.unmodifiableList(ioExceptions); + } + + public List getExceptions() { + return ioExceptions; + } + + private static String getMessage(Collection exceptions) { + StringBuilder sb = new StringBuilder(); + Iterator it = exceptions.iterator(); + while (it.hasNext()) { + sb.append(it.next().getMessage()); + if (it.hasNext()) { + sb.append(", "); + } + } + return sb.toString(); + } + + public static void throwIfRequired(List ioExceptions) throws IOException { + if (ioExceptions == null || ioExceptions.isEmpty()) { + return; + } + if (ioExceptions.size() == 1) { + throw ioExceptions.get(0); + } + throw new MultipleIoException(ioExceptions); + } + + public static IOException toIOException(List ioExceptions) { + int size = ioExceptions.size(); + if (size == 1) { + return ioExceptions.get(0); + } else if (size > 1) { + return new MultipleIoException(ioExceptions); + } + return null; + } +} diff --git a/src/main/java/org/minidns/util/NameUtil.java b/src/main/java/org/minidns/util/NameUtil.java new file mode 100644 index 000000000..e095ed499 --- /dev/null +++ b/src/main/java/org/minidns/util/NameUtil.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +import org.minidns.dnsname.DnsName; + +/** + * Utilities related to internationalized domain names and dns name handling. + */ +public final class NameUtil { + + /** + * Check if two internationalized domain names are equal, possibly causing + * a serialization of both domain names. + * + * @param name1 The first domain name. + * @param name2 The second domain name. + * @return True if both domain names are the same. + */ + @SuppressWarnings("ReferenceEquality") + public static boolean idnEquals(String name1, String name2) { + if (name1 == name2) return true; // catches null, null + if (name1 == null) return false; + if (name2 == null) return false; + if (name1.equals(name2)) return true; + + return DnsName.from(name1).compareTo(DnsName.from(name2)) == 0; + } + +} diff --git a/src/main/java/org/minidns/util/PlatformDetection.java b/src/main/java/org/minidns/util/PlatformDetection.java new file mode 100644 index 000000000..42fc49777 --- /dev/null +++ b/src/main/java/org/minidns/util/PlatformDetection.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +public class PlatformDetection { + + private static Boolean android; + + public static boolean isAndroid() { + if (android == null) { + try { + Class.forName("android.Manifest"); // throws execption when not on Android + android = true; + } catch (Exception e) { + android = false; + } + } + return android; + } +} diff --git a/src/main/java/org/minidns/util/SafeCharSequence.java b/src/main/java/org/minidns/util/SafeCharSequence.java new file mode 100644 index 000000000..a7114a919 --- /dev/null +++ b/src/main/java/org/minidns/util/SafeCharSequence.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +public class SafeCharSequence implements CharSequence { + + @Override + public final int length() { + return toSafeString().length(); + } + + @Override + public final char charAt(int index) { + return toSafeString().charAt(index); + } + + @Override + public final CharSequence subSequence(int start, int end) { + return toSafeString().subSequence(end, end); + } + + public String toSafeString() { + // The default implementation assumes that toString() returns a safe + // representation. Subclasses may override toSafeString() if this assumption is + // not correct. + return toString(); + } +} diff --git a/src/main/java/org/minidns/util/SrvUtil.java b/src/main/java/org/minidns/util/SrvUtil.java new file mode 100644 index 000000000..77b4442d6 --- /dev/null +++ b/src/main/java/org/minidns/util/SrvUtil.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.minidns.dnsname.DnsName; +import org.minidns.record.SRV; + +public class SrvUtil { + + /** + * Sort the given collection of {@link SRV} resource records by their priority and weight. + *

+ * Sorting by priority is easy. Sorting the buckets of SRV records with the same priority by weight requires to choose those records + * randomly but taking the weight into account. + *

+ * + * @param srvRecords + * a collection of SRV records. + * @return a sorted list of the given records. + */ + @SuppressWarnings({"MixedMutabilityReturnType", "JdkObsolete"}) + public static List sortSrvRecords(Collection srvRecords) { + // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." + // (the root domain), abort." + if (srvRecords.size() == 1 && srvRecords.iterator().next().target.equals(DnsName.ROOT)) { + return Collections.emptyList(); + } + + // Create the priority buckets. + SortedMap> buckets = new TreeMap<>(); + for (SRV srvRecord : srvRecords) { + Integer priority = srvRecord.priority; + List bucket = buckets.get(priority); + if (bucket == null) { + bucket = new LinkedList<>(); + buckets.put(priority, bucket); + } + bucket.add(srvRecord); + } + + List sortedSrvRecords = new ArrayList<>(srvRecords.size()); + + for (List bucket : buckets.values()) { + // The list of buckets will be sorted by priority, thanks to SortedMap. We now have determine the order of + // the SRV records with the same priority, i.e., within the same bucket, by their weight. This is done by + // creating an array 'totals' which reflects the percentage of the SRV RRs weight by the total weight of all + // SRV RRs in the bucket. For every entry in the bucket, we choose one using a random number and the sum of + // all weights left in the bucket. We then select RRs position based on the according index of the selected + // value in the 'total' array. This ensures that its weight is taken into account. + int bucketSize; + while ((bucketSize = bucket.size()) > 0) { + int[] totals = new int[bucketSize]; + + int zeroWeight = 1; + for (SRV srv : bucket) { + if (srv.weight > 0) { + zeroWeight = 0; + break; + } + } + + int bucketWeightSum = 0, count = 0; + for (SRV srv : bucket) { + bucketWeightSum += srv.weight + zeroWeight; + totals[count++] = bucketWeightSum; + } + + int selectedPosition; + if (bucketWeightSum == 0) { + // If total priority is 0, then the sum of all weights in this priority bucket is 0. So we simply + // select one of the weights randomly as the other algorithm performed in the else block is unable + // to handle this case. + selectedPosition = (int) (Math.random() * bucketSize); + } else { + double rnd = Math.random() * bucketWeightSum; + selectedPosition = bisect(totals, rnd); + } + + SRV choosenSrvRecord = bucket.remove(selectedPosition); + sortedSrvRecords.add(choosenSrvRecord); + } + } + + return sortedSrvRecords; + } + + // TODO This is not yet really bisection just a stupid linear search. + private static int bisect(int[] array, double value) { + int pos = 0; + for (int element : array) { + if (value < element) + break; + pos++; + } + return pos; + } + +} diff --git a/src/main/java/org/minidns/util/SuccessCallback.java b/src/main/java/org/minidns/util/SuccessCallback.java new file mode 100644 index 000000000..6b3d4d165 --- /dev/null +++ b/src/main/java/org/minidns/util/SuccessCallback.java @@ -0,0 +1,17 @@ +/* + * Copyright 2015-2024 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.util; + +public interface SuccessCallback { + + void onSuccess(T result); + +} diff --git a/src/main/resources/.keep-minidns-dnssec-main-resources b/src/main/resources/.keep-minidns-dnssec-main-resources new file mode 100644 index 000000000..e69de29bb diff --git a/src/main/resources/de.measite.minidns/.keep b/src/main/resources/de.measite.minidns/.keep new file mode 100644 index 000000000..e69de29bb