Repository: MiniDNS/minidns Branch: master Commit: 46a3e8c6c107 Files: 231 Total size: 820.3 KB Directory structure: gitextract_xohl602z/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── LICENCE ├── LICENCE_APACHE ├── LICENCE_LGPL2.1 ├── LICENCE_WTFPL ├── Makefile ├── README.md ├── build-logic/ │ ├── build.gradle │ ├── settings.gradle │ └── src/ │ └── main/ │ └── groovy/ │ ├── org.minidns.android-boot-classpath-conventions.gradle │ ├── org.minidns.android-conventions.gradle │ ├── org.minidns.application-conventions.gradle │ ├── org.minidns.common-conventions.gradle │ ├── org.minidns.java-conventions.gradle │ └── org.minidns.javadoc-conventions.gradle ├── build.gradle ├── config/ │ ├── checkstyle/ │ │ ├── checkstyle.xml │ │ ├── header.txt │ │ └── suppressions.xml │ └── scalaStyle.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties.example ├── gradlew ├── gradlew.bat ├── minidns-android23/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── dnsserverlookup/ │ │ └── android21/ │ │ └── AndroidUsingLinkProperties.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── dnsserverlookup/ │ └── android21/ │ └── AndroidUsingLinkPropertiesTest.java ├── minidns-async/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── source/ │ │ └── async/ │ │ ├── AsyncDnsRequest.java │ │ ├── AsyncNetworkDataSource.java │ │ └── ChannelSelectedHandler.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── source/ │ └── async/ │ └── AsyncNetworkDataSourceTest.java ├── minidns-client/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── minidns/ │ │ │ ├── AbstractDnsClient.java │ │ │ ├── DnsCache.java │ │ │ ├── DnsClient.java │ │ │ ├── MiniDnsConfiguration.java │ │ │ ├── MiniDnsException.java │ │ │ ├── MiniDnsFuture.java │ │ │ ├── MiniDnsInitialization.java │ │ │ ├── RrSet.java │ │ │ ├── cache/ │ │ │ │ ├── ExtendedLruCache.java │ │ │ │ ├── FullLruCache.java │ │ │ │ ├── LruCache.java │ │ │ │ └── MiniDnsCacheFactory.java │ │ │ ├── dnsqueryresult/ │ │ │ │ ├── CachedDnsQueryResult.java │ │ │ │ ├── DirectCachedDnsQueryResult.java │ │ │ │ ├── DnsQueryResult.java │ │ │ │ ├── StandardDnsQueryResult.java │ │ │ │ └── SynthesizedCachedDnsQueryResult.java │ │ │ ├── dnsserverlookup/ │ │ │ │ ├── AbstractDnsServerLookupMechanism.java │ │ │ │ ├── AndroidUsingExec.java │ │ │ │ ├── AndroidUsingReflection.java │ │ │ │ ├── DnsServerLookupMechanism.java │ │ │ │ └── UnixUsingEtcResolvConf.java │ │ │ └── source/ │ │ │ ├── AbstractDnsDataSource.java │ │ │ ├── DnsDataSource.java │ │ │ ├── NetworkDataSource.java │ │ │ └── NetworkDataSourceWithAccounting.java │ │ └── resources/ │ │ └── de.measite.minidns/ │ │ └── .keep │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ ├── DnsClientTest.java │ ├── DnsWorld.java │ ├── LruCacheTest.java │ ├── dnsqueryresult/ │ │ └── TestWorldDnsQueryResult.java │ ├── dnsserverlookup/ │ │ └── AndroidUsingExecTest.java │ └── source/ │ └── NetworkDataSourceTest.java ├── minidns-core/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ ├── constants/ │ │ │ ├── DnsRootServer.java │ │ │ └── DnssecConstants.java │ │ ├── dnslabel/ │ │ │ ├── ALabel.java │ │ │ ├── DnsLabel.java │ │ │ ├── FakeALabel.java │ │ │ ├── LdhLabel.java │ │ │ ├── LeadingOrTrailingHyphenLabel.java │ │ │ ├── NonLdhLabel.java │ │ │ ├── NonReservedLdhLabel.java │ │ │ ├── OtherNonLdhLabel.java │ │ │ ├── ReservedLdhLabel.java │ │ │ ├── UnderscoreLabel.java │ │ │ └── XnLabel.java │ │ ├── dnsmessage/ │ │ │ ├── DnsMessage.java │ │ │ └── Question.java │ │ ├── dnsname/ │ │ │ ├── DnsName.java │ │ │ └── InvalidDnsNameException.java │ │ ├── edns/ │ │ │ ├── Edns.java │ │ │ ├── EdnsOption.java │ │ │ ├── Nsid.java │ │ │ └── UnknownEdnsOption.java │ │ ├── idna/ │ │ │ ├── DefaultIdnaTransformator.java │ │ │ ├── IdnaTransformator.java │ │ │ └── MiniDnsIdna.java │ │ ├── record/ │ │ │ ├── A.java │ │ │ ├── AAAA.java │ │ │ ├── CNAME.java │ │ │ ├── DLV.java │ │ │ ├── DNAME.java │ │ │ ├── DNSKEY.java │ │ │ ├── DS.java │ │ │ ├── Data.java │ │ │ ├── DelegatingDnssecRR.java │ │ │ ├── InternetAddressRR.java │ │ │ ├── MX.java │ │ │ ├── NS.java │ │ │ ├── NSEC.java │ │ │ ├── NSEC3.java │ │ │ ├── NSEC3PARAM.java │ │ │ ├── OPENPGPKEY.java │ │ │ ├── OPT.java │ │ │ ├── PTR.java │ │ │ ├── RRSIG.java │ │ │ ├── RRWithTarget.java │ │ │ ├── Record.java │ │ │ ├── SOA.java │ │ │ ├── SRV.java │ │ │ ├── TLSA.java │ │ │ ├── TXT.java │ │ │ └── UNKNOWN.java │ │ └── util/ │ │ ├── Base32.java │ │ ├── Base64.java │ │ ├── CallbackRecipient.java │ │ ├── CollectionsUtil.java │ │ ├── ExceptionCallback.java │ │ ├── Hex.java │ │ ├── InetAddressUtil.java │ │ ├── MultipleIoException.java │ │ ├── NameUtil.java │ │ ├── PlatformDetection.java │ │ ├── SafeCharSequence.java │ │ ├── SrvUtil.java │ │ └── SuccessCallback.java │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── minidns/ │ │ ├── Assert.java │ │ ├── dnslabel/ │ │ │ └── DnsLabelTest.java │ │ ├── dnsmessage/ │ │ │ └── DnsMessageTest.java │ │ ├── dnsname/ │ │ │ └── DnsNameTest.java │ │ ├── record/ │ │ │ ├── RecordsTest.java │ │ │ └── TLSATest.java │ │ └── util/ │ │ ├── Base32Test.java │ │ ├── Base64Test.java │ │ ├── InetAddressUtilTest.java │ │ ├── NameUtilTest.java │ │ └── SrvUtilTest.java │ └── resources/ │ └── org/ │ └── minidns/ │ └── dnsmessage/ │ ├── codinghorror-txt │ ├── com-ds-rrsig │ ├── com-ns │ ├── com-nsec3 │ ├── example-nsec │ ├── gmail-domainkey-txt │ ├── gmail-mx │ ├── google-aaaa │ ├── gpn-srv │ ├── oracle-soa │ ├── root-dnskey │ └── sun-a ├── minidns-dane/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── dane/ │ │ └── java7/ │ │ └── DaneExtendedTrustManager.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── dane/ │ └── java7/ │ └── DaneJava7Test.java ├── minidns-dnssec/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── minidns/ │ │ │ ├── dane/ │ │ │ │ ├── DaneCertificateException.java │ │ │ │ ├── DaneVerifier.java │ │ │ │ ├── ExpectingTrustManager.java │ │ │ │ └── X509TrustManagerUtil.java │ │ │ └── dnssec/ │ │ │ ├── DigestCalculator.java │ │ │ ├── DnssecClient.java │ │ │ ├── DnssecQueryResult.java │ │ │ ├── DnssecResultNotAuthenticException.java │ │ │ ├── DnssecUnverifiedReason.java │ │ │ ├── DnssecValidationFailedException.java │ │ │ ├── DnssecValidatorInitializationException.java │ │ │ ├── SignatureVerifier.java │ │ │ ├── Verifier.java │ │ │ └── algorithms/ │ │ │ ├── AlgorithmMap.java │ │ │ ├── DsaSignatureVerifier.java │ │ │ ├── EcdsaSignatureVerifier.java │ │ │ ├── EcgostSignatureVerifier.java │ │ │ ├── JavaSecDigestCalculator.java │ │ │ ├── JavaSecSignatureVerifier.java │ │ │ └── RsaSignatureVerifier.java │ │ └── resources/ │ │ └── .keep-minidns-dnssec-main-resources │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── dnssec/ │ │ ├── DnssecClientTest.java │ │ ├── DnssecWorld.java │ │ ├── VerifierTest.java │ │ └── algorithms/ │ │ ├── AlgorithmTest.java │ │ ├── DigestTest.java │ │ ├── DsaSingatureVerifierTest.java │ │ ├── RsaSignatureVerifierTest.java │ │ └── SignatureVerifierTest.java │ └── resources/ │ └── .keep-minidns-dnssec-test-resources ├── minidns-hla/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── hla/ │ │ ├── DnssecResolverApi.java │ │ ├── ResolutionUnsuccessfulException.java │ │ ├── ResolverApi.java │ │ ├── ResolverResult.java │ │ ├── SrvResolverResult.java │ │ └── srv/ │ │ ├── SrvProto.java │ │ ├── SrvService.java │ │ ├── SrvServiceProto.java │ │ └── SrvType.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── hla/ │ └── MiniDnsHlaTest.java ├── minidns-integration-test/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ ├── integrationtest/ │ │ │ ├── AsyncApiTest.java │ │ │ ├── CoreTest.java │ │ │ ├── DaneTest.java │ │ │ ├── DnssecTest.java │ │ │ ├── HlaTest.java │ │ │ ├── IntegrationTest.java │ │ │ ├── IntegrationTestHelper.java │ │ │ ├── IntegrationTestTools.java │ │ │ ├── IterativeDnssecTest.java │ │ │ └── NsidTest.java │ │ └── jul/ │ │ └── MiniDnsJul.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── integrationtest/ │ └── IntegrationTestTest.java ├── minidns-iterative-resolver/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── iterative/ │ │ ├── IterativeClientException.java │ │ ├── IterativeDnsClient.java │ │ ├── ReliableDnsClient.java │ │ └── ResolutionState.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── iterative/ │ └── IterativeDnsClientTest.java ├── minidns-repl/ │ ├── build.gradle │ ├── scala.repl │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── minidns/ │ │ └── minidnsrepl/ │ │ ├── DnssecStats.java │ │ ├── MiniDnsRepl.java │ │ └── MiniDnsStats.java │ └── test/ │ └── java/ │ └── org/ │ └── minidns/ │ └── minidnsrepl/ │ └── ReplTest.java ├── misc/ │ ├── resolve.pl │ └── sbt/ │ ├── .gitignore │ └── build.sbt ├── repl ├── settings.gradle └── version ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push, pull_request] jobs: build: name: Build MiniDNS runs-on: ubuntu-24.04 strategy: matrix: java: - 17 - 21 env: PRIMARY_JAVA_VERSION: 21 steps: - name: Checkout uses: actions/checkout@v2 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v1 with: java-version: ${{ matrix.java }} # Caches - name: Cache Maven uses: actions/cache@v2 with: path: ~/.m2/repository key: maven-${{ hashFiles('**/build.gradle') }} restore-keys: | maven- - name: Cache Gradle uses: actions/cache@v2 with: path: ~/.gradle/caches key: gradle-caches-${{ hashFiles('**/build.gradle') }} restore-keys: gradle-caches - name: Cache Gradle Binary uses: actions/cache@v2 with: path: | ~/gradle-bin-${GRADLE_VERSION}/ key: gradle-bin-${GRADLE_VERSION} - name: Cache Android SDK uses: actions/cache@v2 with: path: | ~/.android/sdk key: android-${{ hashFiles('build.gradle') }} restore-keys: | android- # Pre-reqs - name: Install Android SDK Manager uses: android-actions/setup-android@v2 - name: Install Android SDK run: | sdkmanager \ "platforms;android-19" \ "platforms;android-23" # Testing - name: Gradle Check run: ./gradlew check --stacktrace # Test local publish - name: Gradle publish run: ./gradlew publishToMavenLocal --stacktrace # Javadoc - name: Javadoc if: ${{ matrix.java == env.PRIMARY_JAVA_VERSION }} run: ./gradlew javadocAll --stacktrace # Test Coverage Report - name: Jacoco Test Coverage run: ./gradlew minidns-hla:testCodeCoverageReport # Coveralls - name: Report coverage stats to Coveralls if: ${{ matrix.java == env.PRIMARY_JAVA_VERSION }} uses: coverallsapp/github-action@v2 with: format: jacoco file: minidns-hla/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml # Upload build artifacts - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: smack-java-${{ matrix.java }} path: | minidns-*/build/libs/*.jar !**/*-test-fixtures.jar !**/*-tests.jar ================================================ FILE: .gitignore ================================================ # From https://github.com/github/gitignore # # # # # # # # # # # # # Android gitignore # # # # # # # # # # # # # # Built application files *.apk *.ap_ # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties gradle.properties # Proguard folder generated by Eclipse proguard/ # # # # # # # # # VIM / Linux # # # # # # # # # [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ .directory # # # # # # # Eclipse # # # # # # # *.pydevproject .metadata .gradle bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .settings/ .loadpath .classpath .project # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # CDT-specific .cproject # PDT-specific .buildpath # sbteclipse plugin .target # TeXlipse plugin .texlipse # # # # # # # # # # IntelliJ IDEA # # # # # # # # # # .idea/ *.iml # # # # # # OS X # # # # # # .DS_Store .AppleDouble .LSOverride # Icon must ends with two \r. Icon # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Auto generated version file /minidns-core/src/main/resources/org.minidns/version ================================================ FILE: LICENCE ================================================ This software may be used under the terms of (at your choice) - LGPL version 2 (or later) (see LICENCE_LGPL2.1 for details) - Apache Software licence (see LICENCE_APACHE for details) - WTFPL (see LICENCE_WTFPL for details) ================================================ FILE: LICENCE_APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: LICENCE_LGPL2.1 ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ================================================ FILE: LICENCE_WTFPL ================================================ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2014 Rene Treffer Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: Makefile ================================================ GRADLE ?= ./gradlew .PHONY: all all: check codecov eclipse javadocAll inttest .PHONY: codecov codecov: $(GRADLE) minidns-hla:testCodeCoverageReport echo "Code coverage report available at $(PWD)/minidns-hla/build/reports/jacoco/testCodeCoverageReport/html/index.html" .PHONY: check check: $(GRADLE) $@ .PHONY: eclipse eclipse: $(GRADLE) $@ .PHONY: inttest inttest: $(GRADLE) $@ .PHONY: javadocAll javadocAll: $(GRADLE) $@ echo "javadoc available at $(PWD)/build/javadoc/index.html" ================================================ FILE: README.md ================================================ MiniDNS - A DNSSEC enabled DNS library ====================================== [![Build Status](https://github.com/MiniDNS/minidns/workflows/CI/badge.svg)](https://github.com/MiniDNS/minidns/actions?query=workflow%3A%22CI%22) [![Coverage Status](https://coveralls.io/repos/MiniDNS/minidns/badge.svg)](https://coveralls.io/r/MiniDNS/minidns) MiniDNS ("**M**odular **I**nternet **N**ame **I**nformer for **DNS**") is a DNS library for Android and Java SE. It can parse resource records (A, AAAA, NS, SRV, …) and is easy to use and extend. MiniDNS aims to be secure, modular, efficient and as simple as possible. It also provides support for **DNSSEC** and **DANE**, and is thus the ideal resolver if you want to bring DNSSEC close to your application. It comes with a pluggable cache mechanism, a pre-configured cache and an easy to use high-level API (`minidns-hla`) for those who just want to perform a reliable lookup of a domain name. **Notice:** DNSSEC/DANE support has not yet undergo a security audit. If you find the project useful and if you are able to provide the resources for a security audit, then please contact us. If you are looking for a DNSSEC-enabled resolver in C (and/or Lua) then hava a look at the [Knot Resolver](https://www.knot-resolver.cz/). Also this library is not intended to be used as a DNS server. You might want to look into [dnsjava](http://dnsjava.org/) for such functionality. **MiniDNS release resources** (javadoc, …) an be found at https://minidns.org/releases Quickstart ---------- The easiest way to use MiniDNS is by its high-level API provided by the minidns-hla Maven artifact. Simply add the artifact to your projects dependencies. For example with gradle ```groovy compile "org.minidns:minidns-hla:$minidnsVersion" ``` Then you can use the `ResolverApi` or `DnssecResolverApi` class to perform DNS lookups and check if the result was authenticated via DNSSEC. The following example shows a lookup of A records of 'verteiltesysteme.net'. ```java 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 provides full support for SRV resource records and their handling. ```java 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. … } } ``` REPL ---- MiniDNS comes with a REPL which can be used to perform DNS lookups and to test the library. Simple use `./repl` to start the REPL. The loaded REPL comes with some predefined variables that you can use to perform lookups. For example `c` is a simple DNS client. See `minidns-repl/scala.repl` for more. ```text minidns $ ./repl ... scala> c query ("measite.de", TYPE.A) res0: dnsqueryresult.DnsQueryResult = DnsMessage@54653(QUERY NO_ERROR qr rd ra) { \ [Q: measite.de. IN A] \ [A: measite.de. 3599 IN A 85.10.226.249] \ [X: EDNS: version: 0, flags:; udp: 512] } ``` ================================================ FILE: build-logic/build.gradle ================================================ plugins { id 'groovy-gradle-plugin' } repositories { gradlePluginPortal() } dependencies { implementation "biz.aQute.bnd:biz.aQute.bnd.gradle:7.0.0" implementation "net.ltgt.gradle:gradle-errorprone-plugin:4.0.1" implementation "ru.vyarus:gradle-animalsniffer-plugin:1.7.1" } ================================================ FILE: build-logic/settings.gradle ================================================ rootProject.name = 'minidns-build-logic' ================================================ FILE: build-logic/src/main/groovy/org.minidns.android-boot-classpath-conventions.gradle ================================================ compileJava { options.bootstrapClasspath = files(androidBootClasspath) } javadoc { classpath += files(androidBootClasspath) } ================================================ FILE: build-logic/src/main/groovy/org.minidns.android-conventions.gradle ================================================ 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] } ================================================ FILE: build-logic/src/main/groovy/org.minidns.application-conventions.gradle ================================================ plugins { id 'application' } application { applicationDefaultJvmArgs = ["-enableassertions"] } run { // Pass all system properties down to the "application" run systemProperties System.getProperties() } ================================================ FILE: build-logic/src/main/groovy/org.minidns.common-conventions.gradle ================================================ 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 } ================================================ FILE: build-logic/src/main/groovy/org.minidns.java-conventions.gradle ================================================ 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() } ================================================ FILE: build-logic/src/main/groovy/org.minidns.javadoc-conventions.gradle ================================================ 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" } ================================================ FILE: build.gradle ================================================ plugins { // The scalastyle plugin of smack-repl wants the root project to // have a ideaProject task, so let's add one. id 'idea' id 'org.minidns.javadoc-conventions' } ext { javadocAllDir = new File(buildDir, 'javadoc') nonJavadocAllProjects = [ ':minidns-integration-test', ':minidns-repl', ].collect{ project(it) } javadocAllProjects = subprojects - nonJavadocAllProjects } evaluationDependsOnChildren() task javadocAll(type: Javadoc) { source javadocAllProjects.collect {project -> project.sourceSets.main.allJava.findAll { // Filter out symbolic links to avoid // "warning: a package-info.java file has already been seen for package" // javadoc warnings. !java.nio.file.Files.isSymbolicLink(it.toPath()) } } destinationDir = javadocAllDir // Might need a classpath classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) classpath += files(androidBootClasspath) options { linkSource = true use = true links = [ "https://docs.oracle.com/en/java/javase/${javaMajor}/docs/api/", ] as String[] overview = "$projectDir/resources/javadoc-overview.html" } // Finally copy the javadoc doc-files from the subprojects, which // are potentially generated, to the javadocAll directory. Note // that we use a copy *method* and not a *task* because the inputs // of copy tasks is determined within the configuration phase. And // since some of the inputs are generated, they will not get // picked up if we used a copy method. See also // https://stackoverflow.com/a/40518516/194894 doLast { copy { javadocAllProjects.each { from ("${it.projectDir}/src/javadoc") { include '**/doc-files/*.*' } } into javadocAllDir } } } task inttest { description 'Verify correct functionality by running some integration tests.' dependsOn project(':minidns-integration-test').tasks.run } ================================================ FILE: config/checkstyle/checkstyle.xml ================================================ ================================================ FILE: config/checkstyle/header.txt ================================================ /* * 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. */ ================================================ FILE: config/checkstyle/suppressions.xml ================================================ ================================================ FILE: config/scalaStyle.xml ================================================ Scalastyle standard configuration true ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties.example ================================================ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # GPG settings # # gpg key id #signing.keyId=DEADBEEF # the gpg key passphrase #signing.password=correcthorsebatterystaple # gpg keyring (this is the default gnupg keyring containing private keys) #signing.secretKeyRingFile=/home/ubuntu/.gnupg/secring.gpg # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # nexus settings # # the nexus username used for log in #nexusUsername=ubuntu # the nexus password #nexusPassword=correcthorsebatterystaple ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s ' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: minidns-android23/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } ext { myAndroidSdkApi = 23 androidBootClasspath = getAndroidRuntimeJar(myAndroidSdkApi) } description = "MiniDNS code that requires an Android SDK API of ${myAndroidSdkApi} or higher" dependencies { api project(':minidns-client') testImplementation project(path: ":minidns-client", configuration: "testRuntime") // Add the Android jar to the Eclipse .classpath. implementation files(androidBootClasspath) // For AnimalSniffer signature "net.sf.androidscents.signature:android-api-level-${myAndroidSdkApi}:6.0_r3@signature" } ================================================ FILE: minidns-android23/src/main/java/org/minidns/dnsserverlookup/android21/AndroidUsingLinkProperties.java ================================================ /* * 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; } } ================================================ FILE: minidns-android23/src/test/java/org/minidns/dnsserverlookup/android21/AndroidUsingLinkPropertiesTest.java ================================================ /* * 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 org.junit.jupiter.api.Test; public class AndroidUsingLinkPropertiesTest { /** * Dummy test to make jacocoRootReport happy. */ @Test public void nopTest() { } } ================================================ FILE: minidns-async/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } description = "Asynchronous DNS lookups using Java NIO" dependencies { api project(':minidns-client') testImplementation project(path: ":minidns-client", configuration: "testRuntime") } ================================================ FILE: minidns-async/src/main/java/org/minidns/source/async/AsyncDnsRequest.java ================================================ /* * 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); } } } ================================================ FILE: minidns-async/src/main/java/org/minidns/source/async/AsyncNetworkDataSource.java ================================================ /* * 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(); } } } } ================================================ FILE: minidns-async/src/main/java/org/minidns/source/async/ChannelSelectedHandler.java ================================================ /* * 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); } ================================================ FILE: minidns-async/src/test/java/org/minidns/source/async/AsyncNetworkDataSourceTest.java ================================================ /* * 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 org.junit.jupiter.api.Test; public class AsyncNetworkDataSourceTest { /** * Dummy test to make jacocoRootReport happy. */ @Test public void nopTest() { } } ================================================ FILE: minidns-client/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } description = "A small non-recursing DNS client" dependencies { api project(':minidns-core') testImplementation project(path: ":minidns-core", configuration: "testRuntime") } jar { bundle { bnd( // minidns-client invokes Class.forName("android.os.…") // which causes OSGi to import android.os, because OSGi's // bnd scans for `Class.forName` usages per default. // See also https://github.com/openhab/openhab-addons/pull/11670#issuecomment-982539016 '-noclassforname': 'true', ) } } ================================================ FILE: minidns-client/src/main/java/org/minidns/AbstractDnsClient.java ================================================ /* * 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); } } ================================================ FILE: minidns-client/src/main/java/org/minidns/DnsCache.java ================================================ /* * 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); } ================================================ FILE: minidns-client/src/main/java/org/minidns/DnsClient.java ================================================ /* * 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 googleV4Dns = InetAddressUtil.ipv4From("8.8.8.8"); STATIC_IPV4_DNS_SERVERS.add(googleV4Dns); } catch (IllegalArgumentException e) { LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e); } try { Inet6Address googleV6Dns = InetAddressUtil.ipv6From("[2001:4860:4860::8888]"); STATIC_IPV6_DNS_SERVERS.add(googleV6Dns); } 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"); } } } ================================================ FILE: minidns-client/src/main/java/org/minidns/MiniDnsConfiguration.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/MiniDnsException.java ================================================ /* * 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; } } } ================================================ FILE: minidns-client/src/main/java/org/minidns/MiniDnsFuture.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/MiniDnsInitialization.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/RrSet.java ================================================ /* * 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); } } } ================================================ FILE: minidns-client/src/main/java/org/minidns/cache/ExtendedLruCache.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/cache/FullLruCache.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/cache/LruCache.java ================================================ /* * 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) { } } ================================================ FILE: minidns-client/src/main/java/org/minidns/cache/MiniDnsCacheFactory.java ================================================ /* * 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(); } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsqueryresult/CachedDnsQueryResult.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsqueryresult/DirectCachedDnsQueryResult.java ================================================ /* * 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); } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsqueryresult/StandardDnsQueryResult.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsqueryresult/SynthesizedCachedDnsQueryResult.java ================================================ /* * 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); } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsserverlookup/AbstractDnsServerLookupMechanism.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsserverlookup/AndroidUsingReflection.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsserverlookup/DnsServerLookupMechanism.java ================================================ /* * 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(); } ================================================ FILE: minidns-client/src/main/java/org/minidns/dnsserverlookup/UnixUsingEtcResolvConf.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/source/AbstractDnsDataSource.java ================================================ /* * 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; } } ================================================ FILE: minidns-client/src/main/java/org/minidns/source/DnsDataSource.java ================================================ /* * 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); } } ================================================ FILE: minidns-client/src/main/java/org/minidns/source/NetworkDataSource.java ================================================ /* * 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(); } } ================================================ FILE: minidns-client/src/main/java/org/minidns/source/NetworkDataSourceWithAccounting.java ================================================ /* * 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); } } } ================================================ FILE: minidns-client/src/main/resources/de.measite.minidns/.keep ================================================ ================================================ FILE: minidns-client/src/test/java/org/minidns/DnsClientTest.java ================================================ /* * 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.cache.LruCache; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; import org.minidns.dnsqueryresult.DnsQueryResult; import org.minidns.dnsqueryresult.TestWorldDnsQueryResult; import org.minidns.dnsserverlookup.AbstractDnsServerLookupMechanism; import org.minidns.dnsserverlookup.AndroidUsingExec; import org.minidns.dnsserverlookup.AndroidUsingReflection; import org.minidns.dnsserverlookup.DnsServerLookupMechanism; import org.minidns.record.A; import org.minidns.record.Record.TYPE; import org.minidns.source.AbstractDnsDataSource; import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; import static org.minidns.DnsWorld.a; import static org.minidns.DnsWorld.applyStubRecords; import static org.minidns.DnsWorld.record; import static org.junit.jupiter.api.Assertions.assertArrayEquals; 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 DnsClientTest { @Test public void testLookupMechanismOrder() { DnsClient.addDnsServerLookupMechanism(new TestDnsServerLookupMechanism(AndroidUsingExec.INSTANCE)); DnsClient.addDnsServerLookupMechanism(new TestDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE)); List expectedOrder = new ArrayList<>(); expectedOrder.add(0, AndroidUsingExec.INSTANCE); expectedOrder.add(1, AndroidUsingReflection.INSTANCE); for (DnsServerLookupMechanism mechanism : DnsClient.LOOKUP_MECHANISMS) { if (expectedOrder.isEmpty()) { break; } DnsServerLookupMechanism shouldBeRemovedNext = expectedOrder.get(0); if (mechanism.getName().equals(shouldBeRemovedNext.getName())) { expectedOrder.remove(0); } } assertTrue(expectedOrder.isEmpty()); } private static class TestDnsServerLookupMechanism extends AbstractDnsServerLookupMechanism { protected TestDnsServerLookupMechanism(DnsServerLookupMechanism lookupMechanism) { super(lookupMechanism.getName(), lookupMechanism.getPriority()); } @Override public boolean isAvailable() { return true; } @Override public List getDnsServerAddresses() { return null; } } @Test public void testSingleRecordQuery() throws IOException { DnsClient client = new DnsClient(new LruCache(0)); applyStubRecords(client, record("www.example.com", a("127.0.0.1"))); DnsQueryResult result = client.query("www.example.com", TYPE.A); DnsMessage response = result.response; assertNotNull(response); assertEquals(1, response.answerSection.size()); assertEquals(TYPE.A, response.answerSection.get(0).type); assertArrayEquals(new byte[] {127, 0, 0, 1}, ((A) response.answerSection.get(0).payloadData).getIp()); result = client.query("www2.example.com", TYPE.A); assertEquals(RESPONSE_CODE.NX_DOMAIN, result.response.responseCode); result = client.query("www.example.com", TYPE.CNAME); assertEquals(RESPONSE_CODE.NX_DOMAIN, result.response.responseCode); } @Test public void testReturnNullSource() throws IOException { class NullSource extends AbstractDnsDataSource { boolean queried = false; @Override public DnsQueryResult query(DnsMessage message, InetAddress address, int port) { queried = true; DnsMessage response = message.getResponseBuilder(RESPONSE_CODE.NO_ERROR) .setRecursionAvailable(true) .build(); return new TestWorldDnsQueryResult(message, response); } } DnsClient client = new DnsClient(new LruCache(0)); NullSource source = new NullSource(); client.setDataSource(source); DnsQueryResult message = client.query("www.example.com", TYPE.A); assertTrue(source.queried); assertNotNull(message); } } ================================================ FILE: minidns-client/src/test/java/org/minidns/DnsWorld.java ================================================ /* * 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.constants.DnsRootServer; import org.minidns.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; import org.minidns.dnsmessage.Question; import org.minidns.dnsname.DnsName; import org.minidns.dnsqueryresult.DnsQueryResult; import org.minidns.dnsqueryresult.TestWorldDnsQueryResult; import org.minidns.record.A; import org.minidns.record.AAAA; import org.minidns.record.CNAME; import org.minidns.record.DLV; import org.minidns.record.DNSKEY; import org.minidns.record.DS; import org.minidns.record.Data; import org.minidns.record.MX; import org.minidns.record.NS; import org.minidns.record.NSEC; import org.minidns.record.NSEC3; import org.minidns.record.RRSIG; import org.minidns.record.Record; import org.minidns.record.SOA; import org.minidns.record.SRV; import org.minidns.record.Record.CLASS; import org.minidns.record.Record.TYPE; import org.minidns.source.AbstractDnsDataSource; import org.minidns.util.InetAddressUtil; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; public class DnsWorld extends AbstractDnsDataSource { private List answers = new ArrayList<>(); private final Map> worldData = new HashMap<>(); @Override public DnsQueryResult query(DnsMessage message, InetAddress address, int port) { assertNotNull(message); assertNotNull(address); assertEquals(53, port); for (PreparedResponse answer : answers) { if (answer.isResponse(message, address)) { DnsMessage.Builder response = answer.getResponse().asBuilder(); response.setId(message.id); response.setQuestions(message.questions); return new TestWorldDnsQueryResult(message, response.build(), answer); } } DnsMessage nxDomainResponse = message .getResponseBuilder(RESPONSE_CODE.NX_DOMAIN) // TODO: This RA is faked and eventually causes problems. .setRecursionAvailable(true) .setAuthoritativeAnswer(true) .build(); return new TestWorldDnsQueryResult(message, nxDomainResponse); } public void addPreparedResponse(PreparedResponse answer) { answers.add(answer); } public interface PreparedResponse { boolean isResponse(DnsMessage request, InetAddress address); DnsMessage getResponse(); } public static class AnswerResponse implements PreparedResponse { final DnsMessage request; final DnsMessage response; public AnswerResponse(DnsMessage request, DnsMessage response) { this.request = request; this.response = response; } @Override public boolean isResponse(DnsMessage request, InetAddress address) { List questions = this.request.copyQuestions(); for (Question q : request.questions) { if (!hasQuestion(questions, q)) { return false; } } return questions.isEmpty(); } @Override public DnsMessage getResponse() { return response; } private static boolean hasQuestion(Collection questions, Question q) { for (Iterator iterator = questions.iterator(); iterator.hasNext(); ) { if (iterator.next().equals(q)) { iterator.remove(); return true; } } return false; } @Override public String toString() { return "req: " + request + '\n' + "res: " + response + '\n'; } } public static class RootAnswerResponse extends AnswerResponse { public RootAnswerResponse(DnsMessage request, DnsMessage response) { super(request, response); } @Override public boolean isResponse(DnsMessage request, InetAddress address) { return address.getHostName().endsWith(".root-servers.net") && super.isResponse(request, address); } @Override public String toString() { return getClass().getSimpleName() + '\n' + super.toString(); } } public static class AddressedAnswerResponse extends AnswerResponse { final InetAddress address; public AddressedAnswerResponse(InetAddress address, DnsMessage request, DnsMessage response) { super(request, response); this.address = address; } @Override public boolean isResponse(DnsMessage request, InetAddress address) { return address.equals(this.address) && super.isResponse(request, address); } @Override public String toString() { return getClass().getSimpleName() + ": " + address + '\n' + super.toString(); } } public abstract static class HintsResponse implements PreparedResponse { final DnsName ending; final DnsMessage response; public HintsResponse(DnsName ending, DnsMessage response) { this.ending = ending; this.response = response; } boolean questionHintable(DnsMessage request) { for (Question question : request.questions) { if (question.name.isChildOf(ending) || question.name.equals(ending)) { return true; } } return false; } @Override public DnsMessage getResponse() { return response; } @Override public String toString() { return getClass().getSimpleName() + ": " + ending + '\n' + response; } } public static class RootHintsResponse extends HintsResponse { public RootHintsResponse(DnsName ending, DnsMessage response) { super(ending, response); } @Override public boolean isResponse(DnsMessage request, InetAddress address) { // TODO: It appears that we shouldn't hint down to the nameserver if the query is about a 'DS' RR. Because // they have to get answered at the parental part of the zone cut. if (request.getQuestion().type == TYPE.DS) { return false; } return address.getHostName().endsWith(".root-servers.net") && questionHintable(request); } } public static class AddressedHintsResponse extends HintsResponse { final InetAddress address; public AddressedHintsResponse(InetAddress address, DnsName ending, DnsMessage response) { super(ending, response); this.address = address; } @Override public boolean isResponse(DnsMessage request, InetAddress address) { return address.equals(this.address) && questionHintable(request); } @Override public String toString() { return getClass().getSimpleName() + ": " + address + '\n' + response; } } public static class Zone { // TODO: Change type of zoneName to DnsName and make fields final. String zoneName; InetAddress address; List> records; public Zone(String zoneName, InetAddress address, List> records) { this.zoneName = zoneName; this.address = address; this.records = records; } public List getRRSets() { List rrSetBuilders = new ArrayList<>(); outerloop: for (Record record : records) { for (RrSet.Builder builder : rrSetBuilders) { if (builder.addIfPossible(record)) { continue outerloop; } } rrSetBuilders.add(RrSet.builder().addRecord(record)); } List rrSets = new ArrayList<>(rrSetBuilders.size()); for (RrSet.Builder builder : rrSetBuilders) { rrSets.add(builder.build()); } return rrSets; } boolean isRootZone() { return (zoneName == null || zoneName.isEmpty()) && address == null; } } public static DnsWorld applyZones(AbstractDnsClient client, Zone... zones) { DnsWorld world = new DnsWorld(); client.setDataSource(world); for (Zone zone : zones) { for (RrSet rrSet : zone.getRRSets()) { // A zone may have glue RR sets, so we need use rrSet.name as zoneName here. DnsName zoneName = rrSet.name; Map zoneData = world.worldData.get(zoneName); if (zoneData == null) { zoneData = new HashMap<>(); world.worldData.put(zoneName, zoneData); } // TODO: Shouldn't we try to merge with a previously existing rrSet of the same type instead of // overriding it? Or does this not happen by construction? zoneData.put(rrSet.type, rrSet); DnsMessage.Builder req = client.buildMessage(new Question(rrSet.name, rrSet.type, rrSet.clazz)); DnsMessage.Builder resp = DnsMessage.builder(); resp.setAnswers(rrSet.records); resp.setAuthoritativeAnswer(true); attachGlues(resp, rrSet.records, zone.records); attachSignatures(resp, zone.records); DnsMessage request = req.build(); DnsMessage response = resp.build(); if (zone.isRootZone()) { world.addPreparedResponse(new RootAnswerResponse(request, response)); } else { world.addPreparedResponse(new AddressedAnswerResponse(zone.address, request, response)); } if (rrSet.type == TYPE.NS) { DnsMessage.Builder hintsResp = DnsMessage.builder(); hintsResp.setNameserverRecords(rrSet.records); hintsResp.setAdditionalResourceRecords(response.additionalSection); DnsMessage hintsResponse = hintsResp.build(); if (zone.isRootZone()) { world.addPreparedResponse(new RootHintsResponse(rrSet.name, hintsResponse)); } else { world.addPreparedResponse(new AddressedHintsResponse(zone.address, rrSet.name, hintsResponse)); } } } } return world; } static void attachSignatures(DnsMessage.Builder response, List> records) { List> recordList = new ArrayList<>(records.size()); for (Record record : response.getAnswers()) { for (Record r : records) { if (r.name.equals(record.name) && r.type == TYPE.RRSIG && ((RRSIG) r.payloadData).typeCovered == record.type) { recordList.add(r); } } } response.addAnswers(recordList); recordList.clear(); for (Record record : response.getAdditionalResourceRecords()) { for (Record r : records) { if (r.name.equals(record.name) && r.type == TYPE.RRSIG && ((RRSIG) r.payloadData).typeCovered == record.type) { recordList.add(r); } } } response.addAdditionalResourceRecords(recordList); } static void attachGlues(DnsMessage.Builder response, Collection> answers, List> records) { List> glues = new ArrayList<>(); for (Record record : answers) { if (record.type == TYPE.CNAME) { glues.addAll(findGlues(((CNAME) record.payloadData).target, records)); } else if (record.type == TYPE.NS) { glues.addAll(findGlues(((NS) record.payloadData).target, records)); } else if (record.type == TYPE.SRV) { glues.addAll(findGlues(((SRV) record.payloadData).target, records)); } } if (!glues.isEmpty()) { response.setAdditionalResourceRecords(glues); } } private static List> findGlues(DnsName name, List> records) { List> glues = new ArrayList<>(); for (Record record : records) { if (record.name.equals(name)) { if (record.type == TYPE.CNAME) { glues.addAll(findGlues(((CNAME) record.payloadData).target, records)); } else if (record.type == TYPE.A || record.type == TYPE.AAAA) { glues.add(record); } } } return glues; } @SafeVarargs public static DnsWorld applyStubRecords(AbstractDnsClient client, Record... records) { DnsWorld world = new DnsWorld(); client.setDataSource(world); for (Record record : records) { DnsMessage.Builder request = client.buildMessage(new Question(record.name, record.type, record.clazz, record.unicastQuery)); request.setRecursionDesired(true); DnsMessage.Builder response = DnsMessage.builder(); response.addAnswer(record); response.setRecursionAvailable(true); world.addPreparedResponse(new AnswerResponse(request.build(), response.build())); } return world; } @SafeVarargs public static Zone rootZone(Record... records) { List> listOfRecords = new ArrayList<>(records.length); for (Record record : records) { listOfRecords.add(record); } return rootZone(listOfRecords); } public static Zone rootZone(List> records) { return new Zone("", null, records); } @SafeVarargs public static Zone zone(String zoneName, String nsName, String nsIp, Record... records) { List> listOfRecords = new ArrayList<>(records.length); for (Record record : records) { listOfRecords.add(record); } return zone(zoneName, nsName, nsIp, listOfRecords); } public static Zone zone(String zoneName, String nsName, String nsIp, List> records) { Inet4Address inet4Address = InetAddressUtil.ipv4From(nsIp); try { return zone(zoneName, InetAddress.getByAddress(nsName, inet4Address.getAddress()), records); } catch (UnknownHostException e) { // This will never happen, as we already ensured the validity of the IP address by using parseIpV4() throw new RuntimeException(e); } } public static Zone zone(String zoneName, InetAddress address, List> records) { return new Zone(zoneName, address, records); } public static Record record(String name, long ttl, D data) { return new Record(name, data.getType(), CLASS.IN, ttl, data, false); } public static Record record(DnsName name, long ttl, D data) { return new Record(name, data.getType(), CLASS.IN, ttl, data, false); } public static Record record(String name, D data) { return record(name, 3600, data); } public static Record record(DnsName name, D data) { return record(name, 3600, data); } public static A a(byte[] ip) { return new A(ip); } public static A a(CharSequence ipCharSequence) { return new A(ipCharSequence); } public static AAAA aaaa(byte[] ip) { return new AAAA(ip); } public static AAAA CharSequence(CharSequence ipCharSequence) { return new AAAA(ipCharSequence); } public static CNAME cname(String name) { return cname(DnsName.from(name)); } public static CNAME cname(DnsName name) { return new CNAME(name); } public static DNSKEY dnskey(int flags, int protocol, SignatureAlgorithm algorithm, byte[] key) { return new DNSKEY((short) flags, (byte) protocol, algorithm, key); } public static DNSKEY dnskey(int flags, SignatureAlgorithm algorithm, byte[] key) { return dnskey(flags, DNSKEY.PROTOCOL_RFC4034, algorithm, key); } public static DS ds(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) { return new DS(keyTag, algorithm, digestType, digest); } public static DS ds(int keyTag, SignatureAlgorithm algorithm, byte digestType, byte[] digest) { return new DS(keyTag, algorithm, digestType, digest); } public static DLV dlv(int keyTag, SignatureAlgorithm algorithm, DigestAlgorithm digestType, byte[] digest) { return new DLV(keyTag, algorithm, digestType, digest); } public static MX mx(int priority, String name) { return mx(priority, DnsName.from(name)); } public static MX mx(int priority, DnsName name) { return new MX(priority, name); } public static MX mx(String name) { return mx(10, name); } public static NS ns(String name) { return ns(DnsName.from(name)); } public static NS ns(DnsName name) { return new NS(name); } public static NSEC nsec(String next, TYPE... types) { return nsec(DnsName.from(next), types); } public static NSEC nsec(DnsName next, TYPE... types) { List typesList = Arrays.asList(types); return new NSEC(next, typesList); } public static NSEC3 nsec3(byte hashAlgorithm, byte flags, int iterations, byte[] salt, byte[] nextHashed, TYPE... types) { List typesList = Arrays.asList(types); return new NSEC3(hashAlgorithm, flags, iterations, salt, nextHashed, typesList); } public static RRSIG rrsig(TYPE typeCovered, SignatureAlgorithm algorithm, int labels, long originalTtl, Date signatureExpiration, Date signatureInception, int keyTag, String signerName, byte[] signature) { return rrsig(typeCovered, algorithm, (byte) labels, originalTtl, signatureExpiration, signatureInception, keyTag, DnsName.from(signerName), signature); } public static RRSIG rrsig(TYPE typeCovered, SignatureAlgorithm algorithm, int labels, long originalTtl, Date signatureExpiration, Date signatureInception, int keyTag, DnsName signerName, byte[] signature) { return new RRSIG(typeCovered, algorithm, (byte) labels, originalTtl, signatureExpiration, signatureInception, keyTag, signerName, signature); } public static RRSIG rrsig(TYPE typeCovered, int algorithm, int labels, long originalTtl, Date signatureExpiration, Date signatureInception, int keyTag, String signerName, byte[] signature) { return rrsig(typeCovered, algorithm, (byte) labels, originalTtl, signatureExpiration, signatureInception, keyTag, DnsName.from(signerName), signature); } public static RRSIG rrsig(TYPE typeCovered, int algorithm, int labels, long originalTtl, Date signatureExpiration, Date signatureInception, int keyTag, DnsName signerName, byte[] signature) { return new RRSIG(typeCovered, algorithm, (byte) labels, originalTtl, signatureExpiration, signatureInception, keyTag, signerName, signature); } public static SOA soa(String mname, String rname, long serial, int refresh, int retry, int expire, long minimum) { return soa(DnsName.from(mname), DnsName.from(rname), serial, refresh, retry, expire, minimum); } public static SOA soa(DnsName mname, DnsName rname, long serial, int refresh, int retry, int expire, long minimum) { return new SOA(mname, rname, serial, refresh, retry, expire, minimum); } public static SRV srv(int priority, int weight, int port, String name) { return srv(priority, weight, port, DnsName.from(name)); } public static SRV srv(int priority, int weight, int port, DnsName name) { return new SRV(priority, weight, port, name); } public static SRV srv(int port, String name) { return srv(10, 10, port, name); } public RrSet lookupRrSetFor(DnsName name, TYPE type) { Map zoneData = worldData.get(name); if (zoneData == null) { return null; } return zoneData.get(type); } public InetAddress lookupSingleAuthoritativeNameserverForZone(DnsName zone) { if (zone.isRootLabel()) { return DnsRootServer.getIpv4RootServerById('a'); } RrSet nsRrSet = lookupRrSetFor(zone, TYPE.NS); if (nsRrSet == null) { throw new IllegalStateException(); } @SuppressWarnings("unchecked") Record nsRecord = (Record) nsRrSet.records.iterator().next(); RrSet aRrSet = lookupRrSetFor(nsRecord.name, TYPE.A); if (aRrSet == null) { throw new IllegalStateException(); } @SuppressWarnings("unchecked") Record
aRecord = (Record) aRrSet.records.iterator().next(); try { return InetAddress.getByAddress(nsRecord.name.toString(), aRecord.payloadData.getIp()); } catch (UnknownHostException e) { throw new AssertionError(e); } } } ================================================ FILE: minidns-client/src/test/java/org/minidns/LruCacheTest.java ================================================ /* * 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.junit.jupiter.api.Test; import org.minidns.cache.LruCache; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsmessage.Question; import org.minidns.dnsqueryresult.TestWorldDnsQueryResult; import org.minidns.record.Record; import static org.minidns.DnsWorld.a; import static org.minidns.DnsWorld.ns; import static org.minidns.DnsWorld.record; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; public class LruCacheTest { @Test public void testOutdatedCacheEntry() { LruCache lruCache = new LruCache(5); Question q = new Question("", Record.TYPE.A); TestWorldDnsQueryResult result = createSampleMessage(q, 1); DnsMessage question = q.asQueryMessage(); lruCache.put(question, result); assertNull(lruCache.get(question)); assertNull(lruCache.get(question)); assertEquals(1, lruCache.getExpireCount()); assertEquals(2, lruCache.getMissCount()); } @Test public void testOverfilledCache() { LruCache lruCache = new LruCache(5); Question firstQuestion = new Question("", Record.TYPE.A); lruCache.put(firstQuestion.asQueryMessage(), createSampleMessage(firstQuestion)); assertNotNull(lruCache.get(firstQuestion.asQueryMessage())); Question question; question = new Question("1", Record.TYPE.A); lruCache.put(question.asQueryMessage(), createSampleMessage(question)); question = new Question("2", Record.TYPE.A); lruCache.put(question.asQueryMessage(), createSampleMessage(question)); question = new Question("3", Record.TYPE.A); lruCache.put(question.asQueryMessage(), createSampleMessage(question)); question = new Question("4", Record.TYPE.A); lruCache.put(question.asQueryMessage(), createSampleMessage(question)); question = new Question("5", Record.TYPE.A); lruCache.put(question.asQueryMessage(), createSampleMessage(question)); assertNull(lruCache.get(firstQuestion.asQueryMessage())); assertEquals(0, lruCache.getExpireCount()); assertEquals(1, lruCache.getMissCount()); assertEquals(1, lruCache.getHitCount()); } private static TestWorldDnsQueryResult createSampleMessage(Question question) { return createSampleMessage(question, System.currentTimeMillis()); } private static TestWorldDnsQueryResult createSampleMessage(Question question, long receiveTimestamp) { DnsMessage.Builder message = DnsMessage.builder(); message.setReceiveTimestamp(receiveTimestamp); message.addAnswer(record("", ns("a.root-servers.net"))); message.addAdditionalResourceRecord(record("a.root-servers.net", a("127.0.0.1"))); DnsMessage responseMessage = message.build(); DnsMessage query = question.asQueryMessage(); return new TestWorldDnsQueryResult(query, responseMessage); } } ================================================ FILE: minidns-client/src/test/java/org/minidns/dnsqueryresult/TestWorldDnsQueryResult.java ================================================ /* * 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.DnsWorld.PreparedResponse; import org.minidns.dnsmessage.DnsMessage; public class TestWorldDnsQueryResult extends DnsQueryResult { public final PreparedResponse preparedResponse; public TestWorldDnsQueryResult(DnsMessage query, DnsMessage response) { this(query, response, null); } public TestWorldDnsQueryResult(DnsMessage query, DnsMessage response, PreparedResponse preparedResponse) { super(QueryMethod.testWorld, query, response); this.preparedResponse = preparedResponse; } } ================================================ FILE: minidns-client/src/test/java/org/minidns/dnsserverlookup/AndroidUsingExecTest.java ================================================ /* * 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 static org.junit.jupiter.api.Assertions.assertTrue; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.net.UnknownHostException; import java.util.Set; import org.junit.jupiter.api.Test; public class AndroidUsingExecTest { private static final String PROPS_WITH_NEWLINE = "[property.name]: [\n" + "]\n"; @Test public void parsePropsWithNewlineTest() throws UnknownHostException, IOException { Reader reader = new StringReader(PROPS_WITH_NEWLINE); BufferedReader bufferedReader = new BufferedReader(reader); Set servers = AndroidUsingExec.parseProps(bufferedReader, false); assertTrue(servers.isEmpty()); } } ================================================ FILE: minidns-client/src/test/java/org/minidns/source/NetworkDataSourceTest.java ================================================ /* * 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.junit.jupiter.api.Test; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsqueryresult.DnsQueryResult; import java.io.IOException; import java.net.InetAddress; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class NetworkDataSourceTest { @Test public void udpTruncatedTcpFallbackTest() throws IOException { final int tcpResponseId = 42; class TestNetworkDataSource extends NetworkDataSource { boolean lastQueryUdp = false; @Override protected DnsMessage queryUdp(DnsMessage message, InetAddress address, int port) throws IOException { assertFalse(lastQueryUdp); lastQueryUdp = true; DnsMessage.Builder msg = DnsMessage.builder(); msg.setTruncated(true); return msg.build(); } @Override protected DnsMessage queryTcp(DnsMessage message, InetAddress address, int port) throws IOException { assertTrue(lastQueryUdp); lastQueryUdp = false; return DnsMessage.builder().setId(tcpResponseId).build(); } } TestNetworkDataSource world = new TestNetworkDataSource(); DnsQueryResult result = world.query(DnsMessage.builder().build(), null, 53); assertEquals(tcpResponseId, result.response.id); assertFalse(world.lastQueryUdp); } } ================================================ FILE: minidns-core/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } description = "MiniDNS' core classes and functionality" class CreateFileTask extends DefaultTask { @Input String fileContent @OutputFile File outputFile @TaskAction def createFile() { outputFile.text = fileContent } } task createVersionResource(type: CreateFileTask) { fileContent = version + ' (' + gitCommit + ')' outputFile = new File(projectDir, 'src/main/resources/org.minidns/version') } processResources.dependsOn(createVersionResource) ================================================ FILE: minidns-core/src/main/java/org/minidns/constants/DnsRootServer.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/constants/DnssecConstants.java ================================================ /* * 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); } } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/ALabel.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/DnsLabel.java ================================================ /* * 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; } } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/FakeALabel.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/LdhLabel.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/LeadingOrTrailingHyphenLabel.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/NonLdhLabel.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java ================================================ /* * 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) == '-'; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/UnderscoreLabel.java ================================================ /* * 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) == '_'; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnslabel/XnLabel.java ================================================ /* * 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"); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnsmessage/DnsMessage.java ================================================ /* * 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(); } } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnsmessage/Question.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnsname/DnsName.java ================================================ /* * 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("."); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/dnsname/InvalidDnsNameException.java ================================================ /* * 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."; } } } ================================================ FILE: minidns-core/src/main/java/org/minidns/edns/Edns.java ================================================ /* * 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); } } } ================================================ FILE: minidns-core/src/main/java/org/minidns/edns/EdnsOption.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/edns/Nsid.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/edns/UnknownEdnsOption.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/idna/DefaultIdnaTransformator.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/idna/IdnaTransformator.java ================================================ /* * 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); } ================================================ FILE: minidns-core/src/main/java/org/minidns/idna/MiniDnsIdna.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/A.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/AAAA.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/CNAME.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/DLV.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/DNAME.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/DNSKEY.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/DS.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/Data.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/DelegatingDnssecRR.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/InternetAddressRR.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/MX.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/NS.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/NSEC.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/NSEC3.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/NSEC3PARAM.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/OPENPGPKEY.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/OPT.java ================================================ /* * 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); } } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/PTR.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/RRSIG.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/RRWithTarget.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/Record.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/SOA.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/SRV.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/TLSA.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/TXT.java ================================================ /* * 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() + "\""; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/record/UNKNOWN.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/Base32.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/Base64.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/CallbackRecipient.java ================================================ /* * 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); } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/CollectionsUtil.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/ExceptionCallback.java ================================================ /* * 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); } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/Hex.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/InetAddressUtil.java ================================================ /* * 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); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/MultipleIoException.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/NameUtil.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/PlatformDetection.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/SafeCharSequence.java ================================================ /* * 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(); } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/SrvUtil.java ================================================ /* * 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; } } ================================================ FILE: minidns-core/src/main/java/org/minidns/util/SuccessCallback.java ================================================ /* * 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); } ================================================ FILE: minidns-core/src/test/java/org/minidns/Assert.java ================================================ /* * 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 static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; public class Assert { public static void assertCsEquals(CharSequence expected, CharSequence actual) { assertCsEquals(null, expected, actual); } public static void assertCsEquals(String message, CharSequence expected, CharSequence actual) { if (expected != null && actual != null) { assertEquals(expected.toString(), actual.toString(), message); } else { assertEquals(expected, actual, message); } } public static void assertArrayContentEquals(T[] expect, Collection value) { assertEquals(expect.length, value.size()); List list = new ArrayList<>(Arrays.asList(expect)); for (Object type : value) { assertTrue(list.remove(type)); } assertTrue(list.isEmpty()); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/dnslabel/DnsLabelTest.java ================================================ /* * 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 static org.minidns.Assert.assertCsEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.minidns.idna.MiniDnsIdna; public class DnsLabelTest { @Test public void simpleNonReservedLdhLabelTest() { final String nonReservedLdhLabelString = "test"; final DnsLabel label = DnsLabel.from(nonReservedLdhLabelString); assertEquals(nonReservedLdhLabelString, label.label); assertTrue(label instanceof NonReservedLdhLabel); assertEquals("NonReservedLdhLabel", label.getLabelType()); } @Test public void aLabelTest() { final String uLabelString = "müller"; final String aLabelString = MiniDnsIdna.toASCII(uLabelString); final DnsLabel label = DnsLabel.from(aLabelString); assertEquals(aLabelString, label.label); assertTrue(label instanceof ALabel); assertEquals(uLabelString, label.getInternationalizedRepresentation()); assertEquals("ALabel", label.getLabelType()); } @Test public void fakeALabelTest() { final String fakeALabelString = "xn--mller-va"; final DnsLabel label = DnsLabel.from(fakeALabelString); assertEquals(fakeALabelString, label.label); assertTrue(label instanceof FakeALabel); assertEquals("FakeALabel", label.getLabelType()); } @Test public void underscoreLabelTest() { final String underscoreLabelString = "_tcp"; final DnsLabel label = DnsLabel.from(underscoreLabelString); assertEquals(underscoreLabelString, label.label); assertTrue(label instanceof UnderscoreLabel); assertEquals("UnderscoreLabel", label.getLabelType()); } @Test public void leadingHyphenLabelTest() { final String leadingHyphenLabelString = "-foo"; final DnsLabel label = DnsLabel.from(leadingHyphenLabelString); assertEquals(leadingHyphenLabelString, label.label); assertTrue(label instanceof LeadingOrTrailingHyphenLabel); assertEquals("LeadingOrTrailingHyphenLabel", label.getLabelType()); } @Test public void trailingHyphenLabelTest() { final String trailingHyphenLabelString = "bar-"; final DnsLabel label = DnsLabel.from(trailingHyphenLabelString); assertEquals(trailingHyphenLabelString, label.label); assertTrue(label instanceof LeadingOrTrailingHyphenLabel); assertEquals("LeadingOrTrailingHyphenLabel", label.getLabelType()); } @Test public void otherNonLdhLabelTest() { final String otherNonLdhLabelString = "w@$abi"; final DnsLabel label = DnsLabel.from(otherNonLdhLabelString); assertEquals(otherNonLdhLabelString, label.label); assertTrue(label instanceof OtherNonLdhLabel); assertEquals("OtherNonLdhLabel", label.getLabelType()); } @Test public void dnsLabelWildcardStringTest() { assertEquals("*", DnsLabel.WILDCARD_LABEL.toString()); } @Test public void escapeUnsafeCharactersTest() { assertCsEquals("foo●bar", DnsLabel.from("foo.bar")); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/dnsmessage/DnsMessageTest.java ================================================ /* * 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.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; import org.minidns.dnsname.DnsName; import org.minidns.edns.Edns; import org.minidns.record.A; import org.minidns.record.AAAA; import org.minidns.record.RRWithTarget; import org.minidns.record.Record; import org.minidns.record.DNSKEY; import org.minidns.record.DS; import org.minidns.record.Data; import org.minidns.record.MX; import org.minidns.record.NS; import org.minidns.record.NSEC; import org.minidns.record.NSEC3; import org.minidns.record.NSEC3.HashAlgorithm; import org.minidns.record.Record.CLASS; import org.minidns.record.Record.TYPE; import org.minidns.record.OPT; import org.minidns.record.RRSIG; import org.minidns.record.SOA; import org.minidns.record.SRV; import org.minidns.record.TXT; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; import org.junit.jupiter.api.Test; import static org.minidns.Assert.assertArrayContentEquals; import static org.minidns.Assert.assertCsEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class DnsMessageTest { DnsMessage getMessageFromResource(final String resourceFileName) throws IOException { DnsMessage result; try (InputStream inputStream = getClass().getResourceAsStream(resourceFileName); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { // TODO: There should be a more efficient way to read the resource file as reading byte per byte. for (int readBytes = inputStream.read(); readBytes >= 0; readBytes = inputStream.read()) outputStream.write(readBytes); result = new DnsMessage(outputStream.toByteArray()); } assertNotNull(result); return result; } @Test public void testALookup() throws Exception { DnsMessage m = getMessageFromResource("sun-a"); assertFalse(m.authoritativeAnswer); List> answers = m.answerSection; assertEquals(2, answers.size()); Record cname = answers.get(0); Record a = answers.get(1); assertTrue(cname.getPayload() instanceof RRWithTarget); assertEquals(TYPE.CNAME, cname.getPayload().getType()); assertCsEquals("legacy-sun.oraclegha.com", ((RRWithTarget) cname.getPayload()).target); assertCsEquals("legacy-sun.oraclegha.com", a.name); assertTrue(a.getPayload() instanceof A); assertEquals(TYPE.A, a.getPayload().getType()); assertCsEquals("156.151.59.35", a.getPayload().toString()); } @Test public void testAAAALookup() throws Exception { DnsMessage m = getMessageFromResource("google-aaaa"); assertFalse(m.authoritativeAnswer); List> answers = m.answerSection; assertEquals(1, answers.size()); Record answer = answers.get(0); assertCsEquals("google.com", answer.name); assertTrue(answer.getPayload() instanceof AAAA); assertEquals(TYPE.AAAA, answer.getPayload().getType()); assertCsEquals("2a00:1450:400c:c02:0:0:0:8a", answer.getPayload().toString()); } @Test public void testMXLookup() throws Exception { DnsMessage m = getMessageFromResource("gmail-mx"); assertFalse(m.authoritativeAnswer); List> answers = m.answerSection; assertEquals(5, answers.size()); Map mxes = new TreeMap<>(); for (Record r : answers) { assertCsEquals("gmail.com", r.name); Data d = r.getPayload(); assertTrue(d instanceof MX); assertEquals(TYPE.MX, d.getType()); mxes.put(((MX) d).priority, ((MX) d).target); } assertCsEquals("gmail-smtp-in.l.google.com", mxes.get(5)); assertCsEquals("alt1.gmail-smtp-in.l.google.com", mxes.get(10)); assertCsEquals("alt2.gmail-smtp-in.l.google.com", mxes.get(20)); assertCsEquals("alt3.gmail-smtp-in.l.google.com", mxes.get(30)); assertCsEquals("alt4.gmail-smtp-in.l.google.com", mxes.get(40)); } @Test public void testSRVLookup() throws Exception { DnsMessage m = getMessageFromResource("gpn-srv"); assertFalse(m.authoritativeAnswer); List> answers = m.answerSection; assertEquals(1, answers.size()); Record answer = answers.get(0); assertTrue(answer.getPayload() instanceof SRV); assertEquals(TYPE.SRV, answer.getPayload().getType()); SRV r = (SRV) answer.getPayload(); assertCsEquals("raven.toroid.org", r.target); assertEquals(5222, r.port); assertEquals(0, r.priority); } @Test public void testTXTLookup() throws Exception { DnsMessage m = getMessageFromResource("codinghorror-txt"); HashSet txtToBeFound = new HashSet<>(); txtToBeFound.add("google-site-verification=2oV3cW79A6icpGf-JbLGY4rP4_omL4FOKTqRxb-Dyl4"); txtToBeFound.add("keybase-site-verification=dKxf6T30x5EbNIUpeJcbWxUABJEnVWzQ3Z3hCumnk10"); txtToBeFound.add("v=spf1 include:spf.mandrillapp.com ~all"); List> answers = m.answerSection; for (Record r : answers) { assertCsEquals("codinghorror.com", r.name); Data d = r.getPayload(); assertTrue(d instanceof TXT); assertEquals(TYPE.TXT, d.getType()); TXT txt = (TXT) d; assertTrue(txtToBeFound.contains(txt.getText())); txtToBeFound.remove(txt.getText()); } assertEquals(txtToBeFound.size(), 0); } @Test public void testTXTMultiCharacterStringLookup() throws IOException { DnsMessage dnsMessage = getMessageFromResource("gmail-domainkey-txt"); assertEquals(1, dnsMessage.answerSection.size()); Record answerRecord = dnsMessage.answerSection.get(0); assertEquals(TYPE.TXT, answerRecord.type); Record txtRecord = answerRecord.as(TXT.class); List characterStrings = txtRecord.payloadData.getCharacterStrings(); assertEquals(2, characterStrings.size()); assertEquals( "k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAviPGBk4ZB64UfSqWyAicdR7lodhytae+EYRQVtKDhM+1mXjEqRtP/pDT3sBhazkmA48n2k5NJUyMEoO8nc2r6sUA+/Dom5jRBZp6qDKJOwjJ5R/OpHamlRG+YRJQqR", characterStrings.get(0)); assertEquals( "tqEgSiJWG7h7efGYWmh4URhFM9k9+rmG/CwCgwx7Et+c8OMlngaLl04/bPmfpjdEyLWyNimk761CX6KymzYiRDNz1MOJOJ7OzFaS4PFbVLn0m5mf0HVNtBpPwWuCNvaFVflUYxEyblbB6h/oWOPGbzoSgtRA47SHV53SwZjIsVpbq4LxUW9IxAEwYzGcSgZ4n5Q8X8TndowsDUzoccPFGhdwIDAQAB", characterStrings.get(1)); String text = txtRecord.payloadData.getText(); assertEquals( "k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAviPGBk4ZB64UfSqWyAicdR7lodhytae+EYRQVtKDhM+1mXjEqRtP/pDT3sBhazkmA48n2k5NJUyMEoO8nc2r6sUA+/Dom5jRBZp6qDKJOwjJ5R/OpHamlRG+YRJQqR / tqEgSiJWG7h7efGYWmh4URhFM9k9+rmG/CwCgwx7Et+c8OMlngaLl04/bPmfpjdEyLWyNimk761CX6KymzYiRDNz1MOJOJ7OzFaS4PFbVLn0m5mf0HVNtBpPwWuCNvaFVflUYxEyblbB6h/oWOPGbzoSgtRA47SHV53SwZjIsVpbq4LxUW9IxAEwYzGcSgZ4n5Q8X8TndowsDUzoccPFGhdwIDAQAB", text); } @Test public void testSoaLookup() throws Exception { DnsMessage m = getMessageFromResource("oracle-soa"); assertFalse(m.authoritativeAnswer); List> answers = m.answerSection; assertEquals(1, answers.size()); Record answer = answers.get(0); assertTrue(answer.getPayload() instanceof SOA); assertEquals(TYPE.SOA, answer.getPayload().getType()); SOA soa = (SOA) answer.getPayload(); assertCsEquals("orcldns1.ultradns.com", soa.mname); assertCsEquals("hostmaster⧷@oracle.com", soa.rname); assertEquals(2015032404L, soa.serial); assertEquals(10800, soa.refresh); assertEquals(3600, soa.retry); assertEquals(1209600, soa.expire); assertEquals(900L, soa.minimum); } @Test public void testComNsLookup() throws Exception { DnsMessage m = getMessageFromResource("com-ns"); assertFalse(m.authoritativeAnswer); assertFalse(m.authenticData); assertTrue(m.recursionDesired); assertTrue(m.recursionAvailable); assertTrue(m.qr); List> answers = m.answerSection; assertEquals(13, answers.size()); for (Record answer : answers) { assertCsEquals("com", answer.name); assertEquals(Record.CLASS.IN, answer.clazz); assertEquals(TYPE.NS, answer.type); assertEquals(112028, answer.ttl); assertTrue(((NS) answer.payloadData).target.ace.endsWith(".gtld-servers.net")); } List> arr = m.additionalSection; assertEquals(1, arr.size()); Edns edns = Edns.fromRecord(arr.get(0)); assertEquals(4096, edns.udpPayloadSize); assertEquals(0, edns.version); } @Test public void testRootDnskeyLookup() throws Exception { DnsMessage m = getMessageFromResource("root-dnskey"); assertFalse(m.authoritativeAnswer); assertTrue(m.recursionDesired); assertTrue(m.recursionAvailable); List> answers = m.answerSection; assertEquals(3, answers.size()); for (int i = 0; i < answers.size(); i++) { Record answer = answers.get(i); assertCsEquals(".", answer.name); assertEquals(19593, answer.getTtl()); assertEquals(TYPE.DNSKEY, answer.type); assertEquals(TYPE.DNSKEY, answer.getPayload().getType()); DNSKEY dnskey = (DNSKEY) answer.getPayload(); assertEquals(3, dnskey.protocol); assertEquals(SignatureAlgorithm.RSASHA256, dnskey.algorithm); assertTrue((dnskey.flags & DNSKEY.FLAG_ZONE) > 0); assertEquals(dnskey.getKeyTag(), dnskey.getKeyTag()); switch (i) { case 0: assertTrue((dnskey.flags & DNSKEY.FLAG_SECURE_ENTRY_POINT) > 0); assertEquals(260, dnskey.getKeyLength()); assertEquals(19036, dnskey.getKeyTag()); break; case 1: assertEquals(DNSKEY.FLAG_ZONE, dnskey.flags); assertEquals(132, dnskey.getKeyLength()); assertEquals(48613, dnskey.getKeyTag()); break; case 2: assertEquals(DNSKEY.FLAG_ZONE, dnskey.flags); assertEquals(132, dnskey.getKeyLength()); assertEquals(1518, dnskey.getKeyTag()); break; } } List> arr = m.additionalSection; assertEquals(1, arr.size()); Record opt = arr.get(0); Edns edns = Edns.fromRecord(opt); assertEquals(512, edns.udpPayloadSize); assertEquals(0, edns.version); } @Test public void testComDsAndRrsigLookup() throws Exception { DnsMessage m = getMessageFromResource("com-ds-rrsig"); assertFalse(m.authoritativeAnswer); assertTrue(m.recursionDesired); assertTrue(m.recursionAvailable); List> answers = m.answerSection; assertEquals(2, answers.size()); assertEquals(TYPE.DS, answers.get(0).type); assertEquals(TYPE.DS, answers.get(0).payloadData.getType()); DS ds = (DS) answers.get(0).payloadData; assertEquals(30909, ds.keyTag); assertEquals(SignatureAlgorithm.RSASHA256, ds.algorithm); assertEquals(DigestAlgorithm.SHA256, ds.digestType); assertCsEquals("E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CFC41A5766", ds.getDigestHex()); assertEquals(TYPE.RRSIG, answers.get(1).type); assertEquals(TYPE.RRSIG, answers.get(1).payloadData.getType()); RRSIG rrsig = (RRSIG) answers.get(1).payloadData; assertEquals(TYPE.DS, rrsig.typeCovered); assertEquals(SignatureAlgorithm.RSASHA256, rrsig.algorithm); assertEquals(1, rrsig.labels); assertEquals(86400, rrsig.originalTtl); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); assertCsEquals("20150629170000", dateFormat.format(rrsig.signatureExpiration)); assertCsEquals("20150619160000", dateFormat.format(rrsig.signatureInception)); assertEquals(48613, rrsig.keyTag); assertCsEquals(".", rrsig.signerName); assertEquals(128, rrsig.getSignatureLength()); List> arr = m.additionalSection; assertEquals(1, arr.size()); assertEquals(TYPE.OPT, arr.get(0).getPayload().getType()); Record opt = arr.get(0); Edns edns = Edns.fromRecord(opt); assertEquals(512, edns.udpPayloadSize); assertEquals(0, edns.version); assertTrue(edns.dnssecOk); } @Test public void testExampleNsecLookup() throws Exception { DnsMessage m = getMessageFromResource("example-nsec"); List> answers = m.answerSection; assertEquals(1, answers.size()); assertEquals(TYPE.NSEC, answers.get(0).type); assertEquals(TYPE.NSEC, answers.get(0).payloadData.getType()); NSEC nsec = (NSEC) answers.get(0).getPayload(); assertCsEquals("www.example.com", nsec.next); ArrayList types = new ArrayList<>(Arrays.asList( TYPE.A, TYPE.NS, TYPE.SOA, TYPE.TXT, TYPE.AAAA, TYPE.RRSIG, TYPE.NSEC, TYPE.DNSKEY)); for (TYPE type : nsec.types) { assertTrue(types.remove(type)); } assertTrue(types.isEmpty()); } @Test public void testComNsec3Lookup() throws Exception { DnsMessage m = getMessageFromResource("com-nsec3"); assertEquals(0, m.answerSection.size()); List> records = m.authoritySection; assertEquals(8, records.size()); for (Record record : records) { if (record.type == TYPE.NSEC3) { assertEquals(TYPE.NSEC3, record.getPayload().getType()); NSEC3 nsec3 = (NSEC3) record.payloadData; assertEquals(HashAlgorithm.SHA1, nsec3.hashAlgorithm); assertEquals(1, nsec3.flags); assertEquals(0, nsec3.iterations); assertEquals(0, nsec3.getSaltLength()); switch (record.name.ace) { case "CK0POJMG874LJREF7EFN8430QVIT8BSM.com": assertCsEquals("CK0QFMDQRCSRU0651QLVA1JQB21IF7UR", nsec3.getNextHashedBase32()); assertArrayContentEquals(new TYPE[] {TYPE.NS, TYPE.SOA, TYPE.RRSIG, TYPE.DNSKEY, TYPE.NSEC3PARAM}, nsec3.types); break; case "V2I33UBTHNVNSP9NS85CURCLSTFPTE24.com": assertCsEquals("V2I4KPUS7NGDML5EEJU3MVHO26GKB6PA", nsec3.getNextHashedBase32()); assertArrayContentEquals(new TYPE[] {TYPE.NS, TYPE.DS, TYPE.RRSIG}, nsec3.types); break; case "3RL20VCNK6KV8OT9TDIJPI0JU1SS6ONS.com": assertCsEquals("3RL3UFVFRUE94PV5888AIC2TPS0JA9V2", nsec3.getNextHashedBase32()); assertArrayContentEquals(new TYPE[] {TYPE.NS, TYPE.DS, TYPE.RRSIG}, nsec3.types); break; } } } } @Test public void testMessageSelfQuestionReconstruction() throws Exception { DnsMessage.Builder dmb = DnsMessage.builder(); dmb.setQuestion(new Question("www.example.com", TYPE.A)); dmb.setRecursionDesired(true); dmb.setId(42); dmb.setQrFlag(true); DnsMessage message = new DnsMessage(dmb.build().toArray()); assertEquals(1, message.questions.size()); assertEquals(0, message.answerSection.size()); assertEquals(0, message.additionalSection.size()); assertEquals(0, message.authoritySection.size()); assertTrue(message.recursionDesired); assertTrue(message.qr); assertEquals(42, message.id); assertCsEquals("www.example.com", message.questions.get(0).name); assertEquals(TYPE.A, message.questions.get(0).type); } @Test public void testMessageSelfEasyAnswersReconstruction() throws Exception { DnsMessage.Builder dmb = DnsMessage.builder(); dmb.addAnswer(record("www.example.com", a("127.0.0.1"))) .addAnswer(record("www.example.com", ns("example.com"))); dmb.setRecursionAvailable(true); dmb.setCheckingDisabled(true); dmb.setQrFlag(false); dmb.setId(43); DnsMessage message = new DnsMessage(dmb.build().toArray()); assertEquals(0, message.questions.size()); assertEquals(2, message.answerSection.size()); assertEquals(0, message.additionalSection.size()); assertEquals(0, message.authoritySection.size()); assertTrue(message.recursionAvailable); assertFalse(message.authenticData); assertTrue(message.checkingDisabled); assertFalse(message.qr); assertEquals(43, message.id); assertCsEquals("www.example.com", message.answerSection.get(0).name); assertEquals(TYPE.A, message.answerSection.get(0).type); assertCsEquals("127.0.0.1", message.answerSection.get(0).payloadData.toString()); assertCsEquals("www.example.com", message.answerSection.get(1).name); assertEquals(TYPE.NS, message.answerSection.get(1).type); assertCsEquals("example.com.", message.answerSection.get(1).payloadData.toString()); } @Test public void testMessageSelfComplexReconstruction() throws Exception { DnsMessage.Builder dmb = DnsMessage.builder(); dmb.addQuestion(new Question("www.example.com", TYPE.NS)); dmb.addAnswer(record("www.example.com", ns("ns.example.com"))); dmb.addAdditionalResourceRecord(record("ns.example.com", a("127.0.0.1"))); dmb.addNameserverRecords(record("ns.example.com", aaaa("2001::1"))); dmb.setOpcode(DnsMessage.OPCODE.QUERY); dmb.setResponseCode(DnsMessage.RESPONSE_CODE.NO_ERROR); dmb.setRecursionAvailable(false); dmb.setAuthoritativeAnswer(true); dmb.setAuthenticData(true); dmb.setQrFlag(false); dmb.setId(43); DnsMessage message = new DnsMessage(dmb.build().toArray()); assertEquals(1, message.questions.size()); assertEquals(1, message.answerSection.size()); assertEquals(1, message.additionalSection.size()); assertEquals(1, message.authoritySection.size()); assertFalse(message.recursionAvailable); assertTrue(message.authenticData); assertFalse(message.checkingDisabled); assertFalse(message.qr); assertTrue(message.authoritativeAnswer); assertEquals(43, message.id); assertEquals(DnsMessage.OPCODE.QUERY, message.opcode); assertEquals(DnsMessage.RESPONSE_CODE.NO_ERROR, message.responseCode); assertCsEquals("www.example.com", message.questions.get(0).name); assertEquals(TYPE.NS, message.questions.get(0).type); assertCsEquals("www.example.com", message.answerSection.get(0).name); assertEquals(TYPE.NS, message.answerSection.get(0).type); assertCsEquals("ns.example.com.", message.answerSection.get(0).payloadData.toString()); assertCsEquals("ns.example.com", message.additionalSection.get(0).name); assertEquals(TYPE.A, message.additionalSection.get(0).type); assertCsEquals("127.0.0.1", message.additionalSection.get(0).payloadData.toString()); assertCsEquals("ns.example.com", message.authoritySection.get(0).name); assertEquals(TYPE.AAAA, message.authoritySection.get(0).type); assertCsEquals("2001:0:0:0:0:0:0:1", message.authoritySection.get(0).payloadData.toString()); } @Test public void testMessageSelfTruncatedReconstruction() throws Exception { DnsMessage.Builder dmb = DnsMessage.builder(); dmb.setTruncated(true); dmb.setQrFlag(false); dmb.setId(44); DnsMessage message = new DnsMessage(dmb.build().toArray()); assertEquals(44, message.id); assertFalse(message.qr); assertTrue(message.truncated); } @SuppressWarnings("unchecked") @Test public void testMessageSelfOptRecordReconstructione() throws Exception { DnsMessage.Builder m = DnsMessage.builder(); m.addAdditionalResourceRecord(record("www.example.com", a("127.0.0.1"))); m.getEdnsBuilder().setUdpPayloadSize(512).setDnssecOk(); DnsMessage message = new DnsMessage(m.build().toArray()); assertEquals(2, message.additionalSection.size()); assertCsEquals("www.example.com", message.additionalSection.get(0).name); assertEquals(TYPE.A, message.additionalSection.get(0).type); assertCsEquals("127.0.0.1", message.additionalSection.get(0).payloadData.toString()); assertCsEquals("EDNS: version: 0, flags: do; udp: 512", new Edns((Record) message.additionalSection.get(1)).toString()); } @Test public void testEmptyMessageToString() throws Exception { // toString() should never throw an exception or be null DnsMessage message = DnsMessage.builder().build(); assertNotNull(message.toString()); } @Test public void testFilledMessageToString() throws Exception { // toString() should never throw an exception or be null DnsMessage.Builder message = DnsMessage.builder(); message.setOpcode(DnsMessage.OPCODE.QUERY); message.setResponseCode(DnsMessage.RESPONSE_CODE.NO_ERROR); message.setId(1337); message.setAuthoritativeAnswer(true); message.addQuestion(new Question("www.example.com", TYPE.A)); message.addAnswer(record("www.example.com", a("127.0.0.1"))); message.addNameserverRecords(record("example.com", ns("ns.example.com"))); message.addAdditionalResourceRecord(record("ns.example.com", a("127.0.0.1"))); message.getEdnsBuilder().setUdpPayloadSize(512); assertNotNull(message.build().toString()); } @Test public void testEmptyMessageTerminalOutput() throws Exception { // asTerminalOutput() follows a certain design, however it might change in the future. // Once asTerminalOutput() is changed, it might be required to update this test routine. DnsMessage.Builder message = DnsMessage.builder(); message.setOpcode(DnsMessage.OPCODE.QUERY); message.setResponseCode(DnsMessage.RESPONSE_CODE.NO_ERROR); message.setId(1337); assertNotNull(message.build().asTerminalOutput()); } @Test public void testFilledMessageTerminalOutput() throws Exception { // asTerminalOutput() follows a certain design, however it might change in the future. // Once asTerminalOutput() is changed, it might be required to update this test routine. DnsMessage.Builder message = DnsMessage.builder(); message.setOpcode(DnsMessage.OPCODE.QUERY); message.setResponseCode(DnsMessage.RESPONSE_CODE.NO_ERROR); message.setId(1337); message.setAuthoritativeAnswer(true); message.addQuestion(new Question("www.example.com", TYPE.A)); message.addAnswer(record("www.example.com", a("127.0.0.1"))); message.addNameserverRecords(record("example.com", ns("ns.example.com"))); message.addAdditionalResourceRecord(record("ns.example.com", a("127.0.0.1"))); message.getEdnsBuilder().setUdpPayloadSize(512); assertNotNull(message.build().asTerminalOutput()); } public static Record record(String name, long ttl, Data data) { return new Record<>(name, data.getType(), CLASS.IN, ttl, data, false); } public static Record record(DnsName name, long ttl, Data data) { return new Record<>(name, data.getType(), CLASS.IN, ttl, data, false); } public static Record record(String name, Data data) { return record(name, 3600, data); } public static A a(CharSequence ipv4CharSequence) { return new A(ipv4CharSequence); } public static NS ns(String name) { return ns(DnsName.from(name)); } public static NS ns(DnsName name) { return new NS(name); } public static AAAA aaaa(CharSequence ipv6CharSequence) { return new AAAA(ipv6CharSequence); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/dnsname/DnsNameTest.java ================================================ /* * 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 static org.minidns.Assert.assertCsEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import org.junit.jupiter.api.Test; import org.minidns.dnslabel.DnsLabel; public class DnsNameTest { @Test public void sizeTest() { assertEquals(1, DnsName.from("").size()); assertEquals(13, DnsName.from("example.com").size()); assertEquals(16, DnsName.from("dömäin").size()); assertEquals(24, DnsName.from("dömäin.example").size()); } @Test public void toByteArrayTest() { assertArrayEquals(new byte[] {0}, DnsName.from("").getBytes()); assertArrayEquals(new byte[] {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0}, DnsName.from("example").getBytes()); assertArrayEquals(new byte[] {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}, DnsName.from("example.com").getBytes()); assertArrayEquals(new byte[] {14, 'x', 'n', '-', '-', 'd', 'm', 'i', 'n', '-', 'm', 'o', 'a', '0', 'i', 0}, DnsName.from("dömäin").getBytes()); } @Test public void parseTest() throws IOException { byte[] test = new byte[] {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0}; assertCsEquals("example", DnsName.parse(new DataInputStream(new ByteArrayInputStream(test)), test)); test = new byte[] {7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0}; assertCsEquals("example.com", DnsName.parse(new DataInputStream(new ByteArrayInputStream(test)), test)); } @Test public void parseLabelWillNullByteTest() throws IOException { byte[] test = new byte[] {6, 'v', 'i', 'c', 't', 'i', 'm', 4, 'o', 'r', 'g', 0, 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0}; DnsName dnsName = parse(test); assertCsEquals("victim.org␀.example.org", dnsName); DnsLabel orgWithoutNullByte = dnsName.getLabel(0); assertArrayEquals(new char[] { 'o', 'r', 'g'}, orgWithoutNullByte.getRawLabel().toCharArray()); DnsLabel orgWithNullByte = dnsName.getLabel(2); assertArrayEquals(new char[] { 'o', 'r', 'g', '\0'}, orgWithNullByte.getRawLabel().toCharArray()); assertArrayEquals(new char[] { 'o', 'r', 'g', '␀'}, orgWithNullByte.toString().toCharArray()); } private static DnsName parse(byte[] bytes) throws IOException { return DnsName.parse(new DataInputStream(new ByteArrayInputStream(bytes)), bytes); } @Test public void constructInvalid() throws IOException { DnsName.from("foo\\0nullbar"); } @Test public void equalsTest() { assertEquals(DnsName.from(""), DnsName.from(".")); } @Test public void testStripToParts() { assertCsEquals(DnsName.from("www.example.com"), DnsName.from("www.example.com").stripToLabels(3)); assertCsEquals(DnsName.from("example.com"), DnsName.from("www.example.com").stripToLabels(2)); assertCsEquals(DnsName.from("com"), DnsName.from("www.example.com").stripToLabels(1)); assertCsEquals(DnsName.from("."), DnsName.from("www.example.com").stripToLabels(0)); } @Test public void testStripToPartsIllegal() { assertThrows(IllegalArgumentException.class, () -> DnsName.from("").stripToLabels(1) ); } @Test public void testStripToPartsIllegalLong() { assertThrows(IllegalArgumentException.class, () -> DnsName.from("example.com").stripToLabels(3) ); } @Test public void testConcact() { String leftString = "foo.bar.de"; String rightString = "example.org"; DnsName left = DnsName.from(leftString); DnsName right = DnsName.from(rightString); DnsName concated = DnsName.from(left, right); DnsName expected = DnsName.from(leftString + '.' + rightString); assertEquals(expected, concated); } @Test public void testFromVarargs() { String leftString = "leftmost.left"; String middleString = "leftMiddle.middle.rightMiddle"; String rightString = "right.rightMost"; DnsName left = DnsName.from(leftString); DnsName middle = DnsName.from(middleString); DnsName right = DnsName.from(rightString); DnsName name = DnsName.from(left, middle, right); String completeString = leftString + '.' + middleString + '.' + rightString; assertEquals(name.getRawAce(), completeString); DnsName expected = DnsName.from(completeString); assertEquals(name, expected); } @Test public void caseInsenstiveCompare() { DnsName lowercase = DnsName.from("cs.fau.de"); DnsName uppercase = DnsName.from("CS.fau.de"); assertEquals(lowercase, uppercase); } @Test public void rawFieldsKeepCase() { String mixedCaseDnsName = "UP.low.UP.low.UP"; DnsName mixedCase = DnsName.from(mixedCaseDnsName); assertEquals(mixedCaseDnsName, mixedCase.getRawAce()); } @Test public void getLabelsTest() { final String tldLabelString = "tld"; final String secondLevelString = "second-level-domain"; final String thirdLevelString = "third-level-domain"; final String dnsNameString = thirdLevelString + '.' + secondLevelString + '.' + tldLabelString; final DnsName dnsName = DnsName.from(dnsNameString); DnsLabel[] labels = dnsName.getLabels(); assertEquals(tldLabelString, labels[0].label); assertEquals(secondLevelString, labels[1].label); assertEquals(thirdLevelString, labels[2].label); } @Test public void trailingDotDnsNameFromTest() { final String trailingDotDnsName = "foo.bar."; DnsName dnsName = DnsName.from(trailingDotDnsName); assertEquals("foo.bar", dnsName.ace); } @Test public void fromWithChild() { DnsName parent = DnsName.from("example.org"); String child = "foo"; DnsName dnsName = DnsName.from(child, parent); assertEquals("foo.example.org", dnsName.ace); } @Test public void fromWithChildAndGrandchild() { DnsName parent = DnsName.from("example.org"); DnsLabel grandChild = DnsLabel.from("foo"); DnsLabel child = DnsLabel.from("bar"); DnsName dnsName = DnsName.from(grandChild, child, parent); assertEquals("foo.bar.example.org", dnsName.ace); } @Test public void getHostpartLabel() { DnsName dnsName = DnsName.from("foo.example.org"); DnsLabel hostpart = dnsName.getHostpartLabel(); assertEquals("foo", hostpart.toString()); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/record/RecordsTest.java ================================================ /* * 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.NSEC3.HashAlgorithm; import org.minidns.record.Record.TYPE; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.List; import static org.minidns.Assert.assertCsEquals; import static org.minidns.Assert.assertArrayContentEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; /** * These are some tests for all records. * * The tests main purpose is to test if the output of toByteArray() is parsed into it's original value. * * Additionally, toString() is tested to be RFC compliant. */ public class RecordsTest { @Test public void testARecord() throws Exception { A a = new A(new byte[] {127, 0, 0, 1}); assertEquals("127.0.0.1", a.toString()); assertEquals(TYPE.A, a.getType()); byte[] ab = a.toByteArray(); a = A.parse(new DataInputStream(new ByteArrayInputStream(ab))); assertArrayEquals(new byte[] {127, 0, 0, 1}, a.getIp()); } @Test public void testARecordInvalidIp() throws Exception { assertThrows(IllegalArgumentException.class, () -> new A(new byte[42]) ); } @Test public void testAAAARecord() throws Exception { AAAA aaaa = new AAAA(new byte[] {0x20, 0x01, 0x0d, (byte) 0xb8, (byte) 0x85, (byte) 0xa3, 0x08, (byte) 0xd3, 0x13, 0x19, (byte) 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x44}); // Note: there are multiple valid representations of the IPv6 address due to optional reductions. assertEquals("2001:db8:85a3:8d3:1319:8a2e:370:7344", aaaa.toString()); assertEquals(TYPE.AAAA, aaaa.getType()); byte[] aaaab = aaaa.toByteArray(); aaaa = AAAA.parse(new DataInputStream(new ByteArrayInputStream(aaaab))); assertArrayEquals(new byte[] {0x20, 0x01, 0x0d, (byte) 0xb8, (byte) 0x85, (byte) 0xa3, 0x08, (byte) 0xd3, 0x13, 0x19, (byte) 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x44}, aaaa.getIp()); } @Test public void testAAAARecordInvalidIp() throws Exception { assertThrows(IllegalArgumentException.class, () -> new AAAA(new byte[42]) ); } @Test public void testCnameRecord() throws Exception { CNAME cname = new CNAME("www.example.com"); assertEquals("www.example.com.", cname.toString()); assertEquals(TYPE.CNAME, cname.getType()); byte[] cnameb = cname.toByteArray(); cname = CNAME.parse(new DataInputStream(new ByteArrayInputStream(cnameb)), cnameb); assertCsEquals("www.example.com", cname.target); } @Test public void testDlvRecord() throws Exception { DLV dlv = new DLV(42, (byte) 8, (byte) 2, new byte[] {0x13, 0x37}); assertEquals("42 RSASHA256 SHA256 1337", dlv.toString()); assertEquals(TYPE.DLV, dlv.getType()); byte[] dlvb = dlv.toByteArray(); dlv = DLV.parse(new DataInputStream(new ByteArrayInputStream(dlvb)), dlvb.length); assertEquals(42, dlv.keyTag); assertEquals(SignatureAlgorithm.RSASHA256, dlv.algorithm); assertEquals(DigestAlgorithm.SHA256, dlv.digestType); assertArrayEquals(new byte[] {0x13, 0x37}, dlv.digest); } @SuppressWarnings("deprecation") @Test public void testDnskeyRecord() throws Exception { DNSKEY dnskey = new DNSKEY(DNSKEY.FLAG_ZONE, (byte) 3, (byte) 1, new byte[] {42}); // TODO: Compare with real Base64 once done assertEquals("256 3 RSAMD5 " + dnskey.getKeyBase64(), dnskey.toString()); assertEquals(TYPE.DNSKEY, dnskey.getType()); byte[] dnskeyb = dnskey.toByteArray(); dnskey = DNSKEY.parse(new DataInputStream(new ByteArrayInputStream(dnskeyb)), dnskeyb.length); assertEquals(256, dnskey.flags); assertEquals(3, dnskey.protocol); assertEquals(SignatureAlgorithm.RSAMD5, dnskey.algorithm); assertArrayEquals(new byte[] {42}, dnskey.getKey()); } @Test public void testDnskeyRecordWithUnknownSignatureAlgorithm() throws Exception { byte unknownSignatureAlgorithm = (byte) 255; DNSKEY dnskey = new DNSKEY(DNSKEY.FLAG_ZONE, DNSKEY.PROTOCOL_RFC4034, unknownSignatureAlgorithm, new byte[] {42}); assertEquals(unknownSignatureAlgorithm, dnskey.algorithmByte); assertNull(dnskey.algorithm); byte[] dnskeyb = dnskey.toByteArray(); dnskey = DNSKEY.parse(new DataInputStream(new ByteArrayInputStream(dnskeyb)), dnskeyb.length); assertEquals(unknownSignatureAlgorithm, dnskey.algorithmByte); assertNull(dnskey.algorithm); } @Test public void testDsRecord() throws Exception { DS ds = new DS(42, (byte) 8, (byte) 2, new byte[] {0x13, 0x37}); assertEquals("42 RSASHA256 SHA256 1337", ds.toString()); assertEquals(TYPE.DS, ds.getType()); byte[] dsb = ds.toByteArray(); ds = DS.parse(new DataInputStream(new ByteArrayInputStream(dsb)), dsb.length); assertEquals(42, ds.keyTag); assertEquals(SignatureAlgorithm.RSASHA256, ds.algorithm); assertEquals(DigestAlgorithm.SHA256, ds.digestType); assertArrayEquals(new byte[] {0x13, 0x37}, ds.digest); } @Test public void testMxRecord() throws Exception { MX mx = new MX(10, "mx.example.com"); assertEquals("10 mx.example.com.", mx.toString()); assertEquals(TYPE.MX, mx.getType()); byte[] mxb = mx.toByteArray(); mx = MX.parse(new DataInputStream(new ByteArrayInputStream(mxb)), mxb); assertEquals(10, mx.priority); assertCsEquals("mx.example.com", mx.target); } @Test public void testNsecRecord() throws Exception { NSEC nsec = new NSEC("example.com", new TYPE[] {TYPE.A, TYPE.RRSIG, TYPE.DLV}); assertEquals("example.com. A RRSIG DLV", nsec.toString()); assertEquals(TYPE.NSEC, nsec.getType()); byte[] nsecb = nsec.toByteArray(); nsec = NSEC.parse(new DataInputStream(new ByteArrayInputStream(nsecb)), nsecb, nsecb.length); assertCsEquals("example.com", nsec.next); assertArrayContentEquals(new TYPE[] {TYPE.A, TYPE.RRSIG, TYPE.DLV}, nsec.types); } @Test public void testNsecTypeBitmapEmpty() throws IOException { List emptyTypes = Collections.emptyList(); assertEquals(0, NSEC.readTypeBitMap(NSEC.createTypeBitMap(emptyTypes)).size()); } @Test public void testNsec3Record() throws Exception { NSEC3 nsec3 = new NSEC3((byte) 1, (byte) 1, 1, new byte[] {0x13, 0x37}, new byte[] {0x42, 0x42, 0x42, 0x42, 0x42}, new TYPE[] {TYPE.A}); assertEquals("SHA1 1 1 1337 89144GI2 A", nsec3.toString()); assertEquals(TYPE.NSEC3, nsec3.getType()); byte[] nsec3b = nsec3.toByteArray(); nsec3 = NSEC3.parse(new DataInputStream(new ByteArrayInputStream(nsec3b)), nsec3b.length); assertEquals(HashAlgorithm.SHA1, nsec3.hashAlgorithm); assertEquals(1, nsec3.flags); assertEquals(1, nsec3.iterations); assertArrayEquals(new byte[] {0x13, 0x37}, nsec3.getSalt()); assertArrayEquals(new byte[] {0x42, 0x42, 0x42, 0x42, 0x42}, nsec3.getNextHashed()); assertArrayContentEquals(new TYPE[] {TYPE.A}, nsec3.types); assertEquals("SHA1 1 1 - ", new NSEC3((byte) 1, (byte) 1, 1, new byte[0], new byte[0], new TYPE[0]).toString()); } @Test public void testNsec3ParamRecord() throws Exception { NSEC3PARAM nsec3param = new NSEC3PARAM((byte) 1, (byte) 1, 1, new byte[0]); assertEquals("SHA1 1 1 -", nsec3param.toString()); assertEquals(TYPE.NSEC3PARAM, nsec3param.getType()); byte[] nsec3paramb = nsec3param.toByteArray(); nsec3param = NSEC3PARAM.parse(new DataInputStream(new ByteArrayInputStream(nsec3paramb))); assertEquals("SHA-1", nsec3param.hashAlgorithm.description); assertEquals(1, nsec3param.hashAlgorithmByte); assertEquals(1, nsec3param.flags); assertEquals(1, nsec3param.iterations); assertEquals(0, nsec3param.getSaltLength()); assertEquals("SHA1 1 1 1337", new NSEC3PARAM((byte) 1, (byte) 1, 1, new byte[] {0x13, 0x37}).toString()); } @Test public void testOpenpgpkeyRecord() throws Exception { OPENPGPKEY openpgpkey = new OPENPGPKEY(new byte[] {0x13, 0x37}); assertEquals("Ezc=", openpgpkey.toString()); assertEquals(TYPE.OPENPGPKEY, openpgpkey.getType()); byte[] openpgpkeyb = openpgpkey.toByteArray(); openpgpkey = OPENPGPKEY.parse(new DataInputStream(new ByteArrayInputStream(openpgpkeyb)), openpgpkeyb.length); assertArrayEquals(new byte[] {0x13, 0x37}, openpgpkey.getPublicKeyPacket()); } @Test public void testPtrRecord() throws Exception { PTR ptr = new PTR("ptr.example.com"); assertEquals("ptr.example.com.", ptr.toString()); assertEquals(TYPE.PTR, ptr.getType()); byte[] ptrb = ptr.toByteArray(); ptr = PTR.parse(new DataInputStream(new ByteArrayInputStream(ptrb)), ptrb); assertCsEquals("ptr.example.com", ptr.target); } @Test @SuppressWarnings("JavaUtilDate") public void testRrsigRecord() throws Exception { RRSIG rrsig = new RRSIG(TYPE.A, (byte) 8, (byte) 2, 3600, new Date(1000), new Date(0), 42, "example.com", new byte[] {42}); // TODO: Compare with real Base64 once done assertEquals("A RSASHA256 2 3600 19700101000001 19700101000000 42 example.com. " + rrsig.getSignatureBase64(), rrsig.toString()); assertEquals(TYPE.RRSIG, rrsig.getType()); byte[] rrsigb = rrsig.toByteArray(); rrsig = RRSIG.parse(new DataInputStream(new ByteArrayInputStream(rrsigb)), rrsigb, rrsigb.length); assertEquals(TYPE.A, rrsig.typeCovered); assertEquals(SignatureAlgorithm.RSASHA256, rrsig.algorithm); assertEquals(2, rrsig.labels); assertEquals(3600, rrsig.originalTtl); assertEquals(new Date(1000), rrsig.signatureExpiration); assertEquals(new Date(0), rrsig.signatureInception); assertEquals(42, rrsig.keyTag); assertCsEquals("example.com", rrsig.signerName); assertArrayEquals(new byte[] {42}, rrsig.getSignature()); } @Test public void testSoaRecord() throws Exception { SOA soa = new SOA("sns.dns.icann.org", "noc.dns.icann.org", 2015060341, 7200, 3600, 1209600, 3600); assertEquals("sns.dns.icann.org. noc.dns.icann.org. 2015060341 7200 3600 1209600 3600", soa.toString()); assertEquals(TYPE.SOA, soa.getType()); byte[] soab = soa.toByteArray(); soa = SOA.parse(new DataInputStream(new ByteArrayInputStream(soab)), soab); assertCsEquals("sns.dns.icann.org", soa.mname); assertCsEquals("noc.dns.icann.org", soa.rname); assertEquals(2015060341, soa.serial); assertEquals(7200, soa.refresh); assertEquals(3600, soa.retry); assertEquals(1209600, soa.expire); assertEquals(3600, soa.minimum); } @Test public void testSrvRecord() throws Exception { SRV srv = new SRV(30, 31, 5222, "hermes.jabber.org"); assertEquals("30 31 5222 hermes.jabber.org.", srv.toString()); assertEquals(TYPE.SRV, srv.getType()); byte[] srvb = srv.toByteArray(); srv = SRV.parse(new DataInputStream(new ByteArrayInputStream(srvb)), srvb); assertEquals(30, srv.priority); assertEquals(31, srv.weight); assertEquals(5222, srv.port); assertCsEquals("hermes.jabber.org", srv.target); } @Test public void testTlsaRecord() throws Exception { TLSA tlsa = new TLSA((byte) 1, (byte) 1, (byte) 1, new byte[] {0x13, 0x37}); assertEquals("1 1 1 1337", tlsa.toString()); assertEquals(TYPE.TLSA, tlsa.getType()); byte[] tlsab = tlsa.toByteArray(); tlsa = TLSA.parse(new DataInputStream(new ByteArrayInputStream(tlsab)), tlsab.length); assertEquals(1, tlsa.certUsageByte); assertEquals(1, tlsa.selectorByte); assertEquals(1, tlsa.matchingTypeByte); assertArrayEquals(new byte[] {0x13, 0x37}, tlsa.getCertificateAssociation()); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/record/TLSATest.java ================================================ /* * 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 static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.Test; public class TLSATest { @Test public void ensureTlsaLutsAreInitialized() { TLSA tlsa = new TLSA((byte) 3, (byte) 1, (byte) 2, new byte[] { 0x13, 0x37 }); assertEquals(3, tlsa.certUsageByte); assertNotNull(tlsa.certUsage); assertEquals(1, tlsa.selectorByte); assertNotNull(tlsa.selector); assertEquals(2, tlsa.matchingTypeByte); assertNotNull(tlsa.matchingType); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/util/Base32Test.java ================================================ /* * 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.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class Base32Test { @Test public void testEncodeToString() { assertEquals("", Base32.encodeToString(new byte[] {})); assertEquals("0410====", Base32.encodeToString(new byte[] {1, 2})); assertEquals("891K8HA6", Base32.encodeToString(new byte[] {0x42, 0x43, 0x44, 0x45, 0x46})); assertEquals("VS0FU07V03VG0===", Base32.encodeToString(new byte[] {-1, 0, -1, 0, -1, 0, -1, 0})); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/util/Base64Test.java ================================================ /* * 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.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class Base64Test { @Test public void testEncodeToString() { assertEquals("", Base64.encodeToString(new byte[] {})); assertEquals("Qg==", Base64.encodeToString(new byte[] {0x42})); assertEquals("AQID", Base64.encodeToString(new byte[] {1, 2, 3})); assertEquals("CAIGAP8B/wA=", Base64.encodeToString(new byte[] {8, 2, 6, 0, -1, 1, -1, 0})); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/util/InetAddressUtilTest.java ================================================ /* * 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 static org.junit.jupiter.api.Assertions.assertEquals; import java.net.Inet4Address; import java.net.Inet6Address; import org.junit.jupiter.api.Test; import org.minidns.dnsname.DnsName; public class InetAddressUtilTest { // @formatter:off private static final String[] VALID_IPV6 = new String[] { "2a03:4000:2:2f5::1", "2001:0db8:85a3:08d3:1319:8a2e:0370:7344" }; // @formatter:on // @formatter:off private static final String[] ZERO_COMPRESSED_IPV6 = new String[] { "2001:db8:0:0:0::1", "2001:db8:0:0::1", "2001:db8:0::1", "2001:db8::1" }; // @formatter:on // @formatter:off private static final String[] VALID_IPV4 = new String[] { "192.168.0.1", "127.0.0.1" }; // @formatter:on // @formatter:off private static final String[] INVALID_IP = new String[] { "2001:0db8:85a3:08d3:1319:8a2e:0370:7344:3212", "2001:db8:0:0:1", "0.0.1", "1.2.3.4.5", "foo.example", "foo.bar.baz.example", }; // @formatter:on @Test public void testValidIpv6() { assertAllValidIpv6(VALID_IPV6); assertAllValidIpv6(ZERO_COMPRESSED_IPV6); } @Test public void testInvalidIpv6() { assertAllInvalidIpv6(INVALID_IP); } @Test public void testValidIpv4() { assertAllValidIpv4(VALID_IPV4); } @Test public void testInvalidIpv4() { assertAllInvalidIpv4(INVALID_IP); } private static void assertAllValidIpv6(String... addresses) { for (String address : addresses) { if (!InetAddressUtil.isIpV6Address(address)) { throw new AssertionError(address + " is not a valid IPv6 Address"); } } } private static void assertAllValidIpv4(String... addresses) { for (String address : addresses) { if (!InetAddressUtil.isIpV4Address(address)) { throw new AssertionError(address + " is not a valid IPv4 Address"); } } } private static void assertAllInvalidIpv6(String... addresses) { for (String address : addresses) { if (InetAddressUtil.isIpV6Address(address)) { throw new AssertionError( address + " is believed to be valid IPv6 Address by isIpv6Address(), although it should not be one."); } } } private static void assertAllInvalidIpv4(String... addresses) { for (String address : addresses) { if (InetAddressUtil.isIpV4Address(address)) { throw new AssertionError( address + " is believed to be valid IPv4 Address by isIpv4Address(), although it should not be one"); } } } @Test public void testReverseInet6Address() { Inet6Address inet6Address = InetAddressUtil.ipv6From(VALID_IPV6[0]); DnsName reversedIpv6Address = InetAddressUtil.reverseIpAddressOf(inet6Address); assertEquals(DnsName.from("3.0.a.2.0.0.0.4.2.0.0.0.5.f.2.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0"), reversedIpv6Address); } @Test public void testReverseInet4Address() { Inet4Address inet4Address = InetAddressUtil.ipv4From(VALID_IPV4[0]); DnsName reversedIpv4Address = InetAddressUtil.reverseIpAddressOf(inet4Address); assertEquals(DnsName.from("1.0.168.192"), reversedIpv4Address); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/util/NameUtilTest.java ================================================ /* * 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.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class NameUtilTest { @Test public void idnEqualsTest() { assertTrue(NameUtil.idnEquals(null, null)); assertTrue(NameUtil.idnEquals("domain.example", "domain.example")); assertTrue(NameUtil.idnEquals("dömäin.example", "xn--dmin-moa0i.example")); assertTrue(NameUtil.idnEquals("موقع.وزارة-الاتصالات.مصر", "xn--4gbrim.xn----ymcbaaajlc6dj7bxne2c.xn--wgbh1c")); assertFalse(NameUtil.idnEquals("dömäin.example", null)); assertFalse(NameUtil.idnEquals(null, "domain.example")); assertFalse(NameUtil.idnEquals("dömäin.example", "domain.example")); assertFalse(NameUtil.idnEquals("", "domain.example")); } } ================================================ FILE: minidns-core/src/test/java/org/minidns/util/SrvUtilTest.java ================================================ /* * 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 static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import org.minidns.record.SRV; public class SrvUtilTest { @Test public void sortSRVlowestPrioFirstTest() { List sortedRecords = SrvUtil.sortSrvRecords(createSRVRecords()); assertTrue(sortedRecords.get(0).target.ace.equals("0.20.foo.bar")); } @Test public void sortSRVdistributeOverWeights() { int weight50 = 0, weight20one = 0, weight20two = 0, weight10 = 0; for (int i = 0; i < 1000; i++) { List sortedRecords = SrvUtil.sortSrvRecords(createSRVRecords()); String host = sortedRecords.get(1).target.ace; if (host.equals("5.20.one.foo.bar")) { weight20one++; } else if (host.equals("5.20.two.foo.bar")) { weight20two++; } else if (host.equals("5.10.foo.bar")) { weight10++; } else if (host.equals("5.50.foo.bar")) { weight50++; } else { fail("Wrong host after SRVRecord sorting"); } } assertTrue(weight50 > 400 && weight50 < 600); assertTrue(weight20one > 100 && weight20one < 300); assertTrue(weight20two > 100 && weight20two < 300); assertTrue(weight10 > 0 && weight10 < 200); } @Test public void sortSRVdistributeZeroWeights() { int weightZeroOne = 0, weightZeroTwo = 0; for (int i = 0; i < 1000; i++) { List sortedRecords = SrvUtil.sortSrvRecords(createSRVRecords()); // Remove the first 5 records with a lower priority for (int j = 0; j < 5; j++) { sortedRecords.remove(0); } String host = sortedRecords.remove(0).target.ace; if (host.equals("10.0.one.foo.bar")) { weightZeroOne++; } else if (host.endsWith("10.0.two.foo.bar")) { weightZeroTwo++; } else { fail("Wrong host after SRVRecord sorting"); } } assertTrue(weightZeroOne > 400 && weightZeroOne < 600); assertTrue(weightZeroTwo > 400 && weightZeroTwo < 600); } private static List createSRVRecords() { List records = new ArrayList<>(); // We create one record with priority 0 that should also be tried first // Then 4 records with priority 5 and different weights (50, 20, 20, 10) // Then 2 records with priority 10 and weight 0 which should be treated equal // These records are added in a 'random' way to the list records.add(new SRV(5, 20, 42, "5.20.one.foo.bar")); // Priority 5, Weight 20 records.add(new SRV(10, 0, 42, "10.0.one.foo.bar")); // Priority 10, Weight 0 records.add(new SRV(5, 10, 42, "5.10.foo.bar")); // Priority 5, Weight 10 records.add(new SRV(10, 0, 42, "10.0.two.foo.bar")); // Priority 10, Weight 0 records.add(new SRV(5, 20, 42, "5.20.two.foo.bar")); // Priority 5, Weight 20 records.add(new SRV(0, 20, 42, "0.20.foo.bar")); // Priority 0, Weight 20 records.add(new SRV(5, 50, 42, "5.50.foo.bar")); // Priority 5, Weight 50 return records; } } ================================================ FILE: minidns-dane/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' } description = "Support for DANE using APIs of Java 7 (or higher)" dependencies { api project(':minidns-dnssec') testImplementation project(path: ":minidns-client", configuration: "testRuntime") testImplementation project(path: ":minidns-dnssec", configuration: "testRuntime") } ================================================ FILE: minidns-dane/src/main/java/org/minidns/dane/java7/DaneExtendedTrustManager.java ================================================ /* * 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(); } } ================================================ FILE: minidns-dane/src/test/java/org/minidns/dane/java7/DaneJava7Test.java ================================================ /* * 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.junit.jupiter.api.Test; public class DaneJava7Test { /** * Just here to ensure jacoco is not complaining. */ @Test public void emptyTest() { } } ================================================ FILE: minidns-dnssec/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } description = "MiniDNS' client with support for DNSSEC" dependencies { api project(':minidns-client') api project(':minidns-iterative-resolver') testImplementation project(path: ":minidns-client", configuration: "testRuntime") } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dane/DaneCertificateException.java ================================================ /* * 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); } } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dane/DaneVerifier.java ================================================ /* * 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()]); } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dane/ExpectingTrustManager.java ================================================ /* * 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(); } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dane/X509TrustManagerUtil.java ================================================ /* * 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"); } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DigestCalculator.java ================================================ /* * 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); } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DnssecClient.java ================================================ /* * 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; } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DnssecQueryResult.java ================================================ /* * 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; } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java ================================================ /* * 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; } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java ================================================ /* * 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; } } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java ================================================ /* * 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); } } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java ================================================ /* * 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); } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/SignatureVerifier.java ================================================ /* * 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; } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/Verifier.java ================================================ /* * 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; } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java ================================================ /* * 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); } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java ================================================ /* * 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); } } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java ================================================ /* * 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"); } } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java ================================================ /* * 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; } } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java ================================================ /* * 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); } } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java ================================================ /* * 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; } ================================================ FILE: minidns-dnssec/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java ================================================ /* * 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(); } } ================================================ FILE: minidns-dnssec/src/main/resources/.keep-minidns-dnssec-main-resources ================================================ ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/DnssecClientTest.java ================================================ /* * 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.DnsWorld; import org.minidns.cache.LruCache; import org.minidns.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsname.DnsName; import org.minidns.dnssec.DnssecValidationFailedException.AuthorityDoesNotContainSoa; import org.minidns.dnssec.DnssecWorld.DnssecData; import org.minidns.iterative.ReliableDnsClient.Mode; import org.minidns.record.A; 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; import org.junit.jupiter.api.Test; import java.io.IOException; import java.lang.reflect.Field; import java.net.InetAddress; import java.security.PrivateKey; import java.util.Date; import java.util.List; import static org.minidns.DnsWorld.a; import static org.minidns.DnsWorld.applyZones; import static org.minidns.DnsWorld.dnskey; import static org.minidns.DnsWorld.ns; import static org.minidns.DnsWorld.nsec; import static org.minidns.DnsWorld.record; import static org.minidns.DnsWorld.rootZone; import static org.minidns.DnsWorld.rrsig; import static org.minidns.DnsWorld.soa; import static org.minidns.DnsWorld.zone; import static org.minidns.dnssec.DnssecWorld.addNsec; import static org.minidns.dnssec.DnssecWorld.dlv; import static org.minidns.dnssec.DnssecWorld.ds; import static org.minidns.dnssec.DnssecWorld.publicKey; import static org.minidns.dnssec.DnssecWorld.rrsigRecord; import static org.minidns.dnssec.DnssecWorld.selfSignDnskeyRrSet; import static org.minidns.dnssec.DnssecWorld.sign; import static org.minidns.dnssec.DnssecWorld.signedRootZone; import static org.minidns.dnssec.DnssecWorld.signedZone; 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; // TODO: Make selfSignDnskeyRrset() part of signedZone() and remove it from all tests public class DnssecClientTest { private static SignatureAlgorithm algorithm = SignatureAlgorithm.RSASHA256; private static DigestAlgorithm digestType = DigestAlgorithm.SHA1; private static PrivateKey rootPrivateKSK; private static DNSKEY rootKSK; private static PrivateKey rootPrivateZSK; private static DNSKEY rootZSK; private static DNSKEY comKSK; private static DNSKEY comZSK; private static PrivateKey comPrivateZSK; private static PrivateKey comPrivateKSK; static { DnssecData rootDnssecData = DnssecWorld.getDnssecDataFor(""); rootPrivateKSK = rootDnssecData.privateKsk; rootKSK = rootDnssecData.ksk; rootPrivateZSK = rootDnssecData.privateZsk; rootZSK = rootDnssecData.zsk; DnssecData comDnssecData = DnssecWorld.getDnssecDataFor("com"); comPrivateKSK = comDnssecData.privateKsk; comKSK = comDnssecData.ksk; comPrivateZSK = comDnssecData.privateZsk; comZSK = comDnssecData.zsk; } public static DnssecClient constructDnssecClient() { DnssecClient client = new DnssecClient(new LruCache(0)); client.addSecureEntryPoint(DnsName.ROOT, rootKSK.getKey()); client.setMode(Mode.iterativeOnly); return client; } void checkCorrectExampleMessage(DnsMessage message) { List> answers = message.answerSection; assertEquals(1, answers.size()); assertEquals(Record.TYPE.A, answers.get(0).type); assertArrayEquals(new byte[] {1, 1, 1, 2}, ((A) answers.get(0).payloadData).getIp()); } @Test public void testBasicValid() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertTrue(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testNoSEPAtKSK() throws IOException { DnssecClient client = constructDnssecClient(); DNSKEY comKSK = dnskey(DNSKEY.FLAG_ZONE, algorithm, publicKey(algorithm, comPrivateKSK)); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertTrue(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testSingleZSK() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK)), sign(comKSK, "com", comPrivateKSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertTrue(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testMissingDelegation() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); assertThrows(AuthorityDoesNotContainSoa.class, () -> client.queryDnssec("example.com", Record.TYPE.A) ); } @SuppressWarnings("unchecked") @Test public void testUnsignedRoot() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, rootZone( record("com", ds("com", digestType, comKSK)), record("com", ns("ns.com")), record("ns.com", a("1.1.1.1")) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testNoRootSecureEntryPoint() throws IOException { DnssecClient client = constructDnssecClient(); client.clearSecureEntryPoints(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); assertEquals(1, result.getUnverifiedReasons().size()); assertTrue(result.getUnverifiedReasons().iterator().next() instanceof DnssecUnverifiedReason.NoRootSecureEntryPointReason); } @SuppressWarnings("unchecked") @Test public void testUnsignedZone() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), zone("com", "ns.com", "1.1.1.1", record("example.com", a("1.1.1.2")) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.dnsQueryResult.response; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testInvalidDNSKEY() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); assertThrows(DnssecValidationFailedException.class, () -> client.query("example.com", Record.TYPE.A) ); } @SuppressWarnings("unchecked") @Test public void testNoDNSKEY() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); assertThrows(DnssecValidationFailedException.class, () -> client.query("example.com", Record.TYPE.A) ); } @SuppressWarnings("unchecked") @Test public void testInvalidRRSIG() throws IOException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { DnssecClient client = constructDnssecClient(); Record invalidRrSig = rrsigRecord(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))); RRSIG soonToBeInvalidRrSig = invalidRrSig.payloadData; Field signature = soonToBeInvalidRrSig.getClass().getDeclaredField("signature"); signature.setAccessible(true); byte[] signatureMod = (byte[]) signature.get(soonToBeInvalidRrSig); // Change the signature a little bit so that it becomes invalid. signatureMod[signatureMod.length / 2]++; applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), zone("com", "ns.com", "1.1.1.1", record("com", comKSK), record("com", comZSK), record("example.com", a("1.1.1.2")), invalidRrSig ) ); assertThrows(DnssecValidationFailedException.class, () -> client.query("example.com", Record.TYPE.A) ); } @SuppressWarnings({"unchecked", "JavaUtilDate"}) @Test public void testUnknownAlgorithm() throws IOException { DnssecClient client = constructDnssecClient(); Date signatureExpiration = new Date(System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000); Date signatureInception = new Date(System.currentTimeMillis() - 14 * 24 * 60 * 60 * 1000); RRSIG unknownRrsig = rrsig(Record.TYPE.A, 213, 2, 3600, signatureExpiration, signatureInception, comZSK.getKeyTag(), "com", new byte[0]); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), zone("com", "ns.com", "1.1.1.1", record("com", comKSK), record("com", comZSK), record("example.com", a("1.1.1.2")), record("example.com", unknownRrsig) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testInvalidDelegation() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds(comKSK.getKeyTag(), algorithm, digestType, new byte[0]))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); assertThrows(DnssecValidationFailedException.class, () -> client.query("example.com", Record.TYPE.A) ); } @SuppressWarnings("unchecked") @Test public void testUnknownDelegationDigestType() throws IOException { DnssecClient client = constructDnssecClient(); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds(comKSK.getKeyTag(), algorithm, (byte) 213, new byte[0]))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings({"unchecked", "JavaUtilDate"}) @Test public void testSignatureOutOfDate() throws IOException { DnssecClient client = constructDnssecClient(); Date signatureExpiration = new Date(System.currentTimeMillis() - 14 * 24 * 60 * 60 * 1000); Date signatureInception = new Date(System.currentTimeMillis() - 28L * 24L * 60L * 60L * 1000L); RRSIG outOfDateSig = rrsig(Record.TYPE.A, algorithm, 2, 3600, signatureExpiration, signatureInception, comZSK.getKeyTag(), "com", new byte[0]); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comPrivateZSK, outOfDateSig, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings({"unchecked", "JavaUtilDate"}) @Test public void testSignatureInFuture() throws IOException { DnssecClient client = constructDnssecClient(); Date signatureExpiration = new Date(System.currentTimeMillis() + 28L * 24L * 60L * 60L * 1000L); Date signatureInception = new Date(System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000); RRSIG outOfDateSig = rrsig(Record.TYPE.A, algorithm, 2, 3600, signatureExpiration, signatureInception, comZSK.getKeyTag(), "com", new byte[0]); applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comPrivateZSK, outOfDateSig, record("example.com", a("1.1.1.2"))) ) ); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); } @SuppressWarnings("unchecked") @Test public void testValidNSEC() throws Exception { DnssecClient client = constructDnssecClient(); DnsWorld world = applyZones(client, signedRootZone( sign(rootKSK, "", rootPrivateKSK, algorithm, record("", rootKSK), record("", rootZSK)), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ds("com", digestType, comKSK))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("com", ns("ns.com"))), sign(rootZSK, "", rootPrivateZSK, algorithm, record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", sign(comKSK, "com", comPrivateKSK, algorithm, record("com", comKSK), record("com", comZSK)), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", a("1.1.1.2"))) ) ); DnsMessage.Builder nsecMessage = DnsMessage.builder(); List> records = DnssecWorld.merge( sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", nsec("www.example.com", Record.TYPE.A))), sign(comZSK, "com", comPrivateZSK, algorithm, record("example.com", soa("sns.dns.icann.org", "noc.dns.icann.org", 2015081265, 7200, 3600, 1209600, 3600)))); nsecMessage.setNameserverRecords(records); nsecMessage.setAuthoritativeAnswer(true); world.addPreparedResponse(new DnssecWorld.AddressedNsecResponse(InetAddress.getByAddress("ns.com", new byte[] {1, 1, 1, 1}), nsecMessage.build())); DnssecQueryResult result = client.queryDnssec("nsec.example.com", Record.TYPE.A); // TODO: The setSripSignatureRecords() call could probably be removed. It does not appear to server any purpose here. client.setStripSignatureRecords(false); DnsMessage message = result.synthesizedResponse; assertEquals(0, message.answerSection.size()); assertTrue(message.authenticData); } /** * Zone 'com.' has no DS in the root zone. Hence, in order to verify the results of RRs under 'com.' a DLV has to * been used. * * @throws IOException in case of an I/O error. */ @Test public void testValidDLV() throws IOException { DnssecClient client = constructDnssecClient(); DnsWorld dnsWorld = applyZones(client, signedRootZone( selfSignDnskeyRrSet(""), sign("", ds("dlv")), sign("", record("dlv", ns("ns.com"))), sign("", record("com", ns("ns.com"))), sign("", record("ns.com", a("1.1.1.1"))) ), signedZone("com", "ns.com", "1.1.1.1", selfSignDnskeyRrSet("com"), sign("com", record("example.com", a("1.1.1.2"))) ), signedZone("dlv", "ns.com", "1.1.1.1", selfSignDnskeyRrSet("dlv"), sign("dlv", record("com.dlv", dlv("com", digestType, comKSK))) ) ); // Add NSEC which proves that there is no DS record for 'com.'. Note that the prove comes from the parental zone // nameserver in case of DS RRs. addNsec(dnsWorld, "", "a.root-servers.net", "com", "dlv", TYPE.NS); client.configureLookasideValidation(DnsName.from("dlv")); DnssecQueryResult result = client.queryDnssec("example.com", Record.TYPE.A); assertTrue(result.isAuthenticData()); DnsMessage message = result.synthesizedResponse; checkCorrectExampleMessage(message); client.disableLookasideValidation(); result = client.queryDnssec("example.com", Record.TYPE.A); assertFalse(result.isAuthenticData()); message = result.synthesizedResponse; checkCorrectExampleMessage(message); } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/DnssecWorld.java ================================================ /* * 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.DnsWorld; import org.minidns.constants.DnssecConstants.DigestAlgorithm; import org.minidns.constants.DnssecConstants.SignatureAlgorithm; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsname.DnsName; import org.minidns.dnssec.algorithms.AlgorithmMap; import org.minidns.record.DLV; import org.minidns.record.DNSKEY; import org.minidns.record.DS; import org.minidns.record.Data; import org.minidns.record.NSEC; import org.minidns.record.RRSIG; import org.minidns.record.Record; import org.minidns.record.Record.TYPE; import org.minidns.util.InetAddressUtil; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.math.BigInteger; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; import java.security.SignatureException; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.RSAKeyGenParameterSpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; public class DnssecWorld extends DnsWorld { public static final SignatureAlgorithm DEFAULT_DNSSEC_ALGORITHM = SignatureAlgorithm.RSASHA256; public static final DigestAlgorithm DEFAULT_DIGEST_ALGORITHM = DigestAlgorithm.SHA1; private static final Map DNSSEC_DATA = new HashMap<>(); public static final class DnssecData { public final DnsName zone; public final DNSKEY ksk; public final PrivateKey privateKsk; public final DNSKEY zsk; public final PrivateKey privateZsk; public final SignatureAlgorithm signatureAlgorithm; private DnssecData(DnsName zone, DNSKEY ksk, PrivateKey privateKsk, DNSKEY zsk, PrivateKey privateZsk, SignatureAlgorithm signatureAlgorithm) { this.zone = zone; this.ksk = ksk; this.privateKsk = privateKsk; this.zsk = zsk; this.privateZsk = privateZsk; this.signatureAlgorithm = signatureAlgorithm; } } public static DnssecData getDnssecDataFor(CharSequence zone) { return getDnssecDataFor(DnsName.from(zone)); } public static DnssecData getDnssecDataFor(DnsName zone) { DnssecData dnssecData = DNSSEC_DATA.get(zone); if (dnssecData != null) { return dnssecData; } SignatureAlgorithm algorithm = DEFAULT_DNSSEC_ALGORITHM; PrivateKey privateKsk = generatePrivateKey(algorithm, 2048); DNSKEY ksk = dnskey(DNSKEY.FLAG_ZONE | DNSKEY.FLAG_SECURE_ENTRY_POINT, algorithm, publicKey(algorithm, privateKsk)); PrivateKey privateZsk = generatePrivateKey(algorithm, 1024); DNSKEY zsk = dnskey(DNSKEY.FLAG_ZONE, algorithm, publicKey(algorithm, privateZsk)); dnssecData = new DnssecData(zone, ksk, privateKsk, zsk, privateZsk, algorithm); DNSSEC_DATA.put(zone, dnssecData); return dnssecData; } public static Zone signedRootZone(SignedRRSet... rrSets) { return new Zone("", null, merge(rrSets)); } public static Zone signedZone(String zoneName, String nsName, String nsIp, SignedRRSet... records) { Inet4Address inet4Address = InetAddressUtil.ipv4From(nsIp); try { return signedZone(zoneName, InetAddress.getByAddress(nsName, inet4Address.getAddress()), records); } catch (UnknownHostException e) { // This will never happen, as we already ensured the validity of the IP address by using parseIpV4() throw new RuntimeException(e); } } public static Zone signedZone(String zoneName, InetAddress address, SignedRRSet... rrSets) { return new Zone(zoneName, address, merge(rrSets)); } public static List> merge(SignedRRSet... rrSets) { List> recordList = new ArrayList<>(); for (SignedRRSet rrSet : rrSets) { recordList.add(rrSet.signature); recordList.addAll(Arrays.asList(rrSet.records)); } return recordList; } @SuppressWarnings("varargs") @SafeVarargs public static SignedRRSet sign(DNSKEY key, String signerName, PrivateKey privateKey, SignatureAlgorithm algorithm, Record... records) { return new SignedRRSet(records, rrsigRecord(key, signerName, privateKey, algorithm, records)); } @SuppressWarnings("varargs") @SafeVarargs public static SignedRRSet sign(DNSKEY key, DnsName signerName, PrivateKey privateKey, SignatureAlgorithm algorithm, Record... records) { return new SignedRRSet(records, rrsigRecord(key, signerName, privateKey, algorithm, records)); } @SuppressWarnings("varargs") @SafeVarargs public static SignedRRSet sign(PrivateKey privateKey, RRSIG rrsig, Record... records) { return new SignedRRSet(records, rrsigRecord(privateKey, rrsig, records)); } @SafeVarargs public static SignedRRSet sign(CharSequence signerName, Record... records) { return sign(DnsName.from(signerName), records); } @SuppressWarnings("varargs") @SafeVarargs public static SignedRRSet sign(DnsName signerName, Record... records) { DnssecData dnssecData = getDnssecDataFor(signerName); DNSKEY dnskey; PrivateKey privateKey; final TYPE typeToSign = records[0].type; switch (typeToSign) { case DNSKEY: dnskey = dnssecData.ksk; privateKey = dnssecData.privateKsk; break; default: dnskey = dnssecData.zsk; privateKey = dnssecData.privateZsk; break; } // TODO: Check if all records are of type 'typeToSign'. return new SignedRRSet(records, rrsigRecord(dnskey, signerName, privateKey, dnssecData.signatureAlgorithm, records)); } public static SignedRRSet selfSignDnskeyRrSet(CharSequence zone) { return selfSignDnskeyRrSet(DnsName.from(zone)); } public static SignedRRSet selfSignDnskeyRrSet(DnsName zone) { DnssecData dnssecData = getDnssecDataFor(zone); return sign(zone, record(zone, dnssecData.ksk), record(zone, dnssecData.zsk)); } public static class SignedRRSet { Record[] records; Record signature; public SignedRRSet(Record[] records, Record signature) { this.records = records; this.signature = signature; } } @SafeVarargs public static Record rrsigRecord(DNSKEY key, String signerName, PrivateKey privateKey, SignatureAlgorithm algorithm, Record... records) { return rrsigRecord(key, DnsName.from(signerName), privateKey, algorithm, records); } @SuppressWarnings({"unchecked", "JavaUtilDate"}) public static Record rrsigRecord(DNSKEY key, DnsName signerName, PrivateKey privateKey, SignatureAlgorithm algorithm, Record... records) { Record.TYPE typeCovered = records[0].type; int labels = records[0].name.getLabelCount(); long originalTtl = records[0].ttl; Date signatureExpiration = new Date(System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000); Date signatureInception = new Date(System.currentTimeMillis() - 14 * 24 * 60 * 60 * 1000); RRSIG rrsig = rrsig(typeCovered, algorithm, labels, originalTtl, signatureExpiration, signatureInception, key.getKeyTag(), signerName, new byte[0]); return rrsigRecord(privateKey, rrsig, records); } @SuppressWarnings("unchecked") public static Record rrsigRecord(PrivateKey privateKey, RRSIG rrsig, Record... records) { byte[] bytes = Verifier.combine(rrsig, Arrays.asList(records)); return record(records[0].name, rrsig.originalTtl, rrsig(rrsig.typeCovered, rrsig.algorithm, rrsig.labels, rrsig.originalTtl, rrsig.signatureExpiration, rrsig.signatureInception, rrsig.keyTag, rrsig.signerName, sign(privateKey, rrsig.algorithm, bytes))).as(RRSIG.class); } public static Record ds(CharSequence zone) { return ds(DnsName.from(zone)); } public static Record ds(DnsName zone) { DnssecData dnssecData = getDnssecDataFor(zone); return record(zone, ds(zone, DEFAULT_DIGEST_ALGORITHM, dnssecData.ksk)); } public static DS ds(String name, DigestAlgorithm digestType, DNSKEY dnskey) { return ds(DnsName.from(name), digestType, dnskey); } public static DS ds(DnsName name, DigestAlgorithm digestType, DNSKEY dnskey) { return ds(dnskey.getKeyTag(), dnskey.algorithm, digestType, calculateDsDigest(name, digestType, dnskey)); } public static DLV dlv(String name, DigestAlgorithm digestType, DNSKEY dnskey) { return dlv(DnsName.from(name), digestType, dnskey); } public static DLV dlv(DnsName name, DigestAlgorithm digestType, DNSKEY dnskey) { return dlv(dnskey.getKeyTag(), dnskey.algorithm, digestType, calculateDsDigest(name, digestType, dnskey)); } public static byte[] calculateDsDigest(DnsName name, DigestAlgorithm digestType, DNSKEY dnskey) { DigestCalculator digestCalculator = AlgorithmMap.INSTANCE.getDsDigestCalculator(digestType); byte[] dnskeyData = dnskey.toByteArray(); byte[] dnskeyOwner = 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); return digestCalculator.digest(combined); } @SuppressWarnings("deprecation") public static byte[] sign(PrivateKey privateKey, SignatureAlgorithm algorithm, byte[] content) { try { Signature signature; switch (algorithm) { case RSAMD5: signature = Signature.getInstance("MD5withRSA"); break; case RSASHA1: case RSASHA1_NSEC3_SHA1: signature = Signature.getInstance("SHA1withRSA"); break; case RSASHA256: signature = Signature.getInstance("SHA256withRSA"); break; case RSASHA512: signature = Signature.getInstance("SHA512withRSA"); break; case DSA: case DSA_NSEC3_SHA1: signature = Signature.getInstance("SHA1withDSA"); break; default: throw new RuntimeException(algorithm + " algorithm not yet supported by DNSSECWorld"); } signature.initSign(privateKey); signature.update(content); byte[] bytes = signature.sign(); switch (algorithm) { case DSA: case DSA_NSEC3_SHA1: return convertAsn1ToRFC((DSAPrivateKey) privateKey, bytes); case RSAMD5: case RSASHA1: case RSASHA1_NSEC3_SHA1: case RSASHA256: case RSASHA512: default: return bytes; } } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException | IOException e) { throw new RuntimeException(e); } } /** * Convert ASN.1 to RFC 2536. * * @param privateKey the private key. * @param bytes the bytes. * @return the RFC 2536 bytes. * @throws IOException if an IO error occurs. */ public static byte[] convertAsn1ToRFC(DSAPrivateKey privateKey, byte[] bytes) throws IOException { DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes)); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(privateKey.getParams().getP().bitLength() / 64 - 8); dis.skipBytes(2); streamAsn1Int(dis, dos, 20); streamAsn1Int(dis, dos, 20); return bos.toByteArray(); } public static void streamAsn1Int(DataInputStream dis, DataOutputStream dos, int targetLength) throws IOException { dis.skipBytes(1); byte s_pad = (byte) (dis.readByte() - targetLength); if (s_pad >= 0) { dis.skipBytes(s_pad); s_pad = 0; } else { for (int i = 0; i < (1 - s_pad); i++) { dos.writeByte(0); } } byte[] buf = new byte[targetLength + s_pad]; int bytesRead = dis.read(buf); if (bytesRead != buf.length) throw new IOException(); dos.write(buf); } @SuppressWarnings("deprecation") public static PrivateKey generatePrivateKey(SignatureAlgorithm algorithm, int length) { switch (algorithm) { case RSAMD5: case RSASHA1: case RSASHA1_NSEC3_SHA1: case RSASHA256: case RSASHA512: return generateRSAPrivateKey(length, RSAKeyGenParameterSpec.F4); case DSA: case DSA_NSEC3_SHA1: return generateDSAPrivateKey(length); default: throw new RuntimeException(algorithm + " algorithm not yet supported by DNSSECWorld"); } } public static PrivateKey generateRSAPrivateKey(int length, BigInteger publicExponent) { try { KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); rsa.initialize(new RSAKeyGenParameterSpec(length, publicExponent)); KeyPair keyPair = rsa.generateKeyPair(); return keyPair.getPrivate(); } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static PrivateKey generateDSAPrivateKey(int length) { try { KeyPairGenerator dsa = KeyPairGenerator.getInstance("DSA"); dsa.initialize(length); KeyPair keyPair = dsa.generateKeyPair(); return keyPair.getPrivate(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @SuppressWarnings("deprecation") public static byte[] publicKey(SignatureAlgorithm algorithm, PrivateKey privateKey) { switch (algorithm) { case RSAMD5: case RSASHA1: case RSASHA1_NSEC3_SHA1: case RSASHA256: case RSASHA512: return getRSAPublicKey((RSAPrivateCrtKey) privateKey); case DSA: case DSA_NSEC3_SHA1: return getDSAPublicKey((DSAPrivateKey) privateKey); default: throw new RuntimeException(algorithm + " algorithm not yet supported by DNSSECWorld"); } } private static byte[] getDSAPublicKey(DSAPrivateKey privateKey) { final DSAParams params = privateKey.getParams(); final BigInteger g = params.getG(); final BigInteger p = params.getP(); final BigInteger q = params.getQ(); final BigInteger x = privateKey.getX(); final BigInteger y = g.modPow(x, p); final int t = p.bitLength() / 64 - 8; final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final DataOutputStream dos = new DataOutputStream(baos); try { dos.writeByte(t); dos.write(toUnsignedByteArray(q, 20)); dos.write(toUnsignedByteArray(p, t * 8 + 64)); dos.write(toUnsignedByteArray(g, t * 8 + 64)); dos.write(toUnsignedByteArray(y, t * 8 + 64)); } catch (IOException e) { throw new RuntimeException(e); } return baos.toByteArray(); } public static byte[] getRSAPublicKey(RSAPrivateCrtKey privateKey) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); byte[] exponent = toUnsignedByteArray(privateKey.getPublicExponent()); if (exponent.length > 255) { dos.writeByte(0); dos.writeShort(exponent.length); } else { dos.writeByte(exponent.length); } dos.write(exponent); dos.write(toUnsignedByteArray(privateKey.getModulus())); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } private static byte[] toUnsignedByteArray(BigInteger bigInteger) { byte[] array = bigInteger.toByteArray(); if (array[0] == 0) { byte[] tmp = new byte[array.length - 1]; System.arraycopy(array, 1, tmp, 0, tmp.length); array = tmp; } return array; } private static byte[] toUnsignedByteArray(BigInteger bigInteger, int length) { byte[] array = bigInteger.toByteArray(); if (array.length != length) { if (array.length == length + 1 && array[0] == 0) { byte[] tmp = new byte[array.length - 1]; System.arraycopy(array, 1, tmp, 0, tmp.length); array = tmp; } else if (array.length < length) { byte[] tmp = new byte[length]; System.arraycopy(array, 0, tmp, length - array.length, array.length); array = tmp; } } return array; } public static class AddressedNsecResponse implements PreparedResponse { final InetAddress address; final DnsMessage nsecMessage; final boolean isRootNameserver; // We currently do not use the whole list of NSEC records, but in the future we eventually will be. final List> nsecRecords; public AddressedNsecResponse(InetAddress address, DnsMessage nsecMessage) { this.address = address; this.nsecMessage = nsecMessage; this.isRootNameserver = address.getHostName().endsWith(".root-servers.net"); this.nsecRecords = nsecMessage.filterAuthoritySectionBy(NSEC.class); } @Override public boolean isResponse(DnsMessage request, InetAddress address) { boolean nameserverMatches; if (isRootNameserver) { nameserverMatches = address.getHostName().endsWith(".root-servers.net"); } else { nameserverMatches = address.equals(this.address); } Record nsecRecord = nsecRecords.get(0); return nameserverMatches && Verifier.nsecMatches(request.getQuestion().name, nsecRecord.name, nsecRecord.payloadData.next); } @Override public DnsMessage getResponse() { return nsecMessage; } @Override public String toString() { return getClass().getSimpleName() + ": " + address + '\n' + nsecMessage; } } public static void addNsec(DnsWorld dnsWorld, CharSequence zone, CharSequence zoneSoaNameserver, CharSequence owner, String nextSecure, Record.TYPE... typesCovered) { addNsec(dnsWorld, DnsName.from(zone), DnsName.from(zoneSoaNameserver), DnsName.from(owner), DnsName.from(nextSecure), typesCovered); } public static void addNsec(DnsWorld dnsWorld, DnsName zone, DnsName zoneSoaNameserver, DnsName owner, DnsName nextSecure, Record.TYPE... typesCovered) { DnssecData dnssecData = getDnssecDataFor(zone); PrivateKey privateKey = dnssecData.privateZsk; DNSKEY key = dnssecData.zsk; SignatureAlgorithm signatureAlgorithm = dnssecData.signatureAlgorithm; DnsMessage.Builder nsecAnswerBuilder = DnsMessage.builder(); List> records = DnssecWorld.merge( sign(key, zone, privateKey, signatureAlgorithm, record(owner, nsec(nextSecure, typesCovered))), sign(key, zone, privateKey, signatureAlgorithm, record(owner, soa(zoneSoaNameserver, DnsName.from("mailbox.of.responsible.person"), 2015081265, 7200, 3600, 1209600, 3600)))); nsecAnswerBuilder.setNameserverRecords(records); nsecAnswerBuilder.setAuthoritativeAnswer(true); DnsMessage nsecAnswer = nsecAnswerBuilder.build(); // Get the authoritative nameserver IP address from dns world. InetAddress authoritativeNameserver = dnsWorld.lookupSingleAuthoritativeNameserverForZone(zone); PreparedResponse preparedNsecResponse = new DnssecWorld.AddressedNsecResponse(authoritativeNameserver, nsecAnswer); dnsWorld.addPreparedResponse(preparedNsecResponse); } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/VerifierTest.java ================================================ /* * 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.Question; import org.minidns.dnsname.DnsName; import org.minidns.dnssec.algorithms.JavaSecDigestCalculator; import org.minidns.record.NSEC; import org.minidns.record.NSEC3; import org.minidns.record.Record; import org.minidns.record.Record.TYPE; import org.junit.jupiter.api.Test; import java.math.BigInteger; import static org.minidns.DnsWorld.nsec; import static org.minidns.DnsWorld.nsec3; import static org.minidns.DnsWorld.record; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class VerifierTest { @Test public void testNsecMatches() { assertTrue(Verifier.nsecMatches("example.com", "com", "com")); assertTrue(Verifier.nsecMatches("example.com", "e.com", "f.com")); assertTrue(Verifier.nsecMatches("example.com", "be", "de")); assertTrue(Verifier.nsecMatches("nsec.example.com", "example.com", "www.example.com")); assertFalse(Verifier.nsecMatches("example.com", "a.com", "example.com")); assertFalse(Verifier.nsecMatches("example.com", "example1.com", "example2.com")); assertFalse(Verifier.nsecMatches("example.com", "test.com", "xxx.com")); assertFalse(Verifier.nsecMatches("example.com", "xxx.com", "test.com")); assertFalse(Verifier.nsecMatches("example.com", "aaa.com", "bbb.com")); assertFalse(Verifier.nsecMatches("www.example.com", "example2.com", "example3.com")); assertFalse(Verifier.nsecMatches("test.nsec.example.com", "nsec.example.com", "a.nsec.example.com")); assertFalse(Verifier.nsecMatches("test.nsec.example.com", "test.nsec.example.com", "a.example.com")); assertFalse(Verifier.nsecMatches("www.example.com", "example.com", "nsec.example.com")); assertFalse(Verifier.nsecMatches("example.com", "nsec.example.com", "www.example.com")); } @Test public void testVerifyNsec() { Record nsecRecord = record("example.com", nsec("www.example.com", TYPE.A, TYPE.NS, TYPE.SOA, TYPE.TXT, TYPE.AAAA, TYPE.RRSIG, TYPE.NSEC, TYPE.DNSKEY)).as(NSEC.class); assertNull(Verifier.verifyNsec(nsecRecord, new Question("nsec.example.com", TYPE.A))); assertNull(Verifier.verifyNsec(nsecRecord, new Question("example.com", TYPE.PTR))); assertNotNull(Verifier.verifyNsec(nsecRecord, new Question("www.example.com", TYPE.A))); assertNotNull(Verifier.verifyNsec(nsecRecord, new Question("example.com", TYPE.NS))); } @Test public void testVerifyNsec3() { byte[] bytes = new byte[] {0x3f, (byte) 0xb1, (byte) 0xd0, (byte) 0xaa, 0x27, (byte) 0xe2, 0x5f, (byte) 0xda, 0x40, 0x75, (byte) 0x92, (byte) 0x95, 0x5a, 0x1c, 0x7f, (byte) 0x98, (byte) 0xdb, 0x5b, 0x79, (byte) 0x91}; Record nsec3Record = record("7UO4LIHALHHLNGLJAFT7TBIQ6H1SL1CN.net", nsec3((byte) 1, (byte) 1, 0, new byte[0], bytes, TYPE.NS, TYPE.SOA, TYPE.RRSIG, TYPE.DNSKEY, TYPE.NSEC3PARAM)).as(NSEC3.class); DnsName zone = DnsName.from("net"); assertNull(Verifier.verifyNsec3(zone, nsec3Record, new Question("x.net", TYPE.A))); assertNotNull(Verifier.verifyNsec3(zone, nsec3Record, new Question("example.net", TYPE.A))); } @Test public void testNsec3hash() throws Exception { JavaSecDigestCalculator digestCalculator = new JavaSecDigestCalculator("SHA-1"); assertEquals("6e8777855bcd60d7b45fc51893776dde75bf6cd4", new BigInteger(1, Verifier.nsec3hash(digestCalculator, new byte[] {42}, new byte[] {88}, 5)).toString(16)); } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/algorithms/AlgorithmTest.java ================================================ /* * 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; public class AlgorithmTest { protected static AlgorithmMap algorithmMap; static { algorithmMap = AlgorithmMap.INSTANCE; } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/algorithms/DigestTest.java ================================================ /* * 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.dnssec.DigestCalculator; import org.minidns.record.NSEC3.HashAlgorithm; import org.junit.jupiter.api.Test; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; public class DigestTest extends AlgorithmTest { @Test public void testSha1DsDigest() { DigestCalculator dsDigestCalculator = algorithmMap.getDsDigestCalculator(DigestAlgorithm.SHA1); assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", digestHexString(dsDigestCalculator, "")); assertEquals("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", digestHexString(dsDigestCalculator, "test")); assertEquals("640ab2bae07bedc4c163f679a746f7ab7fb5d1fa", digestHexString(dsDigestCalculator, "Test")); } @Test public void testSha256DsDigest() { DigestCalculator dsDigestCalculator = algorithmMap.getDsDigestCalculator(DigestAlgorithm.SHA256); assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", digestHexString(dsDigestCalculator, "")); assertEquals("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", digestHexString(dsDigestCalculator, "test")); assertEquals("532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25", digestHexString(dsDigestCalculator, "Test")); } @Test public void testSha1nsec3Digest() { DigestCalculator nsecDigestCalculator = algorithmMap.getNsecDigestCalculator(HashAlgorithm.SHA1); assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", digestHexString(nsecDigestCalculator, "")); assertEquals("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", digestHexString(nsecDigestCalculator, "test")); assertEquals("640ab2bae07bedc4c163f679a746f7ab7fb5d1fa", digestHexString(nsecDigestCalculator, "Test")); } private static String digestHexString(DigestCalculator digestCalculator, String in) { return new BigInteger(1, digestCalculator.digest(in.getBytes(StandardCharsets.UTF_8))).toString(16); } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/algorithms/DsaSingatureVerifierTest.java ================================================ /* * 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.SignatureAlgorithm; import org.minidns.dnssec.DnssecValidationFailedException; import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.minidns.dnssec.DnssecWorld.generatePrivateKey; import static org.minidns.dnssec.DnssecWorld.publicKey; import static org.minidns.dnssec.DnssecWorld.sign; public class DsaSingatureVerifierTest extends SignatureVerifierTest { private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.DSA; @Test public void testDSA1024Valid() throws DnssecValidationFailedException { verifierTest(1024, ALGORITHM); } @Test public void testDSA512Valid() throws DnssecValidationFailedException { verifierTest(512, ALGORITHM); } @Test public void testDSAIllegalSignature() { byte[] sample = new byte[] { 0x0 }; assertThrows(DataMalformedException.class, () -> assertSignatureValid(publicKey(ALGORITHM, generatePrivateKey(ALGORITHM, 1024)), ALGORITHM, sample, sample) ); } @Test public void testDSAIllegalPublicKey() { byte[] sample = getRandomBytes(); assertThrows(DataMalformedException.class, () -> assertSignatureValid(new byte[] {0x0}, ALGORITHM, sign(generatePrivateKey(ALGORITHM, 1024), ALGORITHM, sample), sample) ); } @Test public void testDSAWrongSignature() throws DnssecValidationFailedException { byte[] sample = getRandomBytes(); assertSignatureInvalid(publicKey(ALGORITHM, generatePrivateKey(ALGORITHM, 1024)), ALGORITHM, sign(generatePrivateKey(ALGORITHM, 1024), ALGORITHM, sample), sample); } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/algorithms/RsaSignatureVerifierTest.java ================================================ /* * 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.SignatureAlgorithm; import org.minidns.dnssec.DnssecValidationFailedException; import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; import org.junit.jupiter.api.Test; import java.math.BigInteger; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.minidns.dnssec.DnssecWorld.generatePrivateKey; import static org.minidns.dnssec.DnssecWorld.generateRSAPrivateKey; import static org.minidns.dnssec.DnssecWorld.publicKey; import static org.minidns.dnssec.DnssecWorld.sign; public class RsaSignatureVerifierTest extends SignatureVerifierTest { @Test public void testShortExponentSHA1RSAValid() throws DnssecValidationFailedException { verifierTest(generateRSAPrivateKey(1024, BigInteger.valueOf(17)), SignatureAlgorithm.RSASHA1); } @Test public void testLongExponentSHA1RSAValid() throws DnssecValidationFailedException { verifierTest(generateRSAPrivateKey(3072, BigInteger.valueOf(256).pow(256).add(BigInteger.ONE)), SignatureAlgorithm.RSASHA1); } @Test public void testSHA1RSAIllegalSignature() throws DnssecValidationFailedException { byte[] sample = new byte[] { 0x0 }; assertThrows(DnssecValidationFailedException.class, () -> assertSignatureValid( publicKey(SignatureAlgorithm.RSASHA1, generatePrivateKey(SignatureAlgorithm.RSASHA1, 1024)), SignatureAlgorithm.RSASHA1, sample, sample) ); } @Test public void testSHA1RSAIllegalPublicKey() throws DnssecValidationFailedException { byte[] sample = getRandomBytes(); assertThrows(DataMalformedException.class, () -> assertSignatureValid(new byte[] { 0x0 }, SignatureAlgorithm.RSASHA1, sign(generatePrivateKey(SignatureAlgorithm.RSASHA1, 1024), SignatureAlgorithm.RSASHA1, sample), sample) ); } @Test public void testSHA1RSAWrongSignature() throws DnssecValidationFailedException { byte[] sample = getRandomBytes(); assertSignatureInvalid( publicKey(SignatureAlgorithm.RSASHA1, generatePrivateKey(SignatureAlgorithm.RSASHA1, 1024)), SignatureAlgorithm.RSASHA1, sign(generatePrivateKey(SignatureAlgorithm.RSASHA1, 1024), SignatureAlgorithm.RSASHA1, sample), sample); } @SuppressWarnings("deprecation") @Test public void testMD5RSAValid() throws DnssecValidationFailedException { verifierTest(1024, SignatureAlgorithm.RSAMD5); } @Test public void testSHA256RSAValid() throws DnssecValidationFailedException { verifierTest(1024, SignatureAlgorithm.RSASHA256); } @Test public void testSHA512RSAValid() throws DnssecValidationFailedException { verifierTest(1024, SignatureAlgorithm.RSASHA512); } } ================================================ FILE: minidns-dnssec/src/test/java/org/minidns/dnssec/algorithms/SignatureVerifierTest.java ================================================ /* * 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.SignatureAlgorithm; import org.minidns.dnsname.DnsName; import org.minidns.dnssec.DnssecValidationFailedException; import org.minidns.record.DNSKEY; import org.minidns.record.RRSIG; import java.security.PrivateKey; import java.util.concurrent.ThreadLocalRandom; import static org.minidns.dnssec.DnssecWorld.generatePrivateKey; import static org.minidns.dnssec.DnssecWorld.publicKey; import static org.minidns.dnssec.DnssecWorld.sign; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class SignatureVerifierTest extends AlgorithmTest { protected void verifierTest(int length, SignatureAlgorithm algorithm) throws DnssecValidationFailedException { verifierTest(generatePrivateKey(algorithm, length), algorithm); } protected void verifierTest(PrivateKey privateKey, SignatureAlgorithm algorithm) throws DnssecValidationFailedException { byte[] sample = getRandomBytes(); assertSignatureValid(publicKey(algorithm, privateKey), algorithm, sign(privateKey, algorithm, sample), sample); } protected static void assertSignatureValid(byte[] publicKey, SignatureAlgorithm algorithm, byte[] signature, byte[] signedBytes) throws DnssecValidationFailedException { assertTrue(verify(publicKey, algorithm, signature, signedBytes)); } protected static void assertSignatureInvalid(byte[] publicKey, SignatureAlgorithm algorithm, byte[] signature, byte[] signedBytes) throws DnssecValidationFailedException { assertFalse(verify(publicKey, algorithm, signature, signedBytes)); } private static boolean verify(byte[] publicKey, SignatureAlgorithm algorithm, byte[] signature, byte[] signedBytes) throws DnssecValidationFailedException { DNSKEY key = new DNSKEY((short) 0, (byte) 0, algorithm, publicKey); RRSIG rrsig = new RRSIG(null, algorithm, (byte) 0, (long) 0, null, null, 0, DnsName.ROOT, signature); boolean res = algorithmMap.getSignatureVerifier(algorithm).verify(signedBytes, rrsig, key); return res; } protected static byte[] getRandomBytes() { byte[] randomBytes = new byte[1024]; ThreadLocalRandom.current().nextBytes(randomBytes); return randomBytes; } } ================================================ FILE: minidns-dnssec/src/test/resources/.keep-minidns-dnssec-test-resources ================================================ ================================================ FILE: minidns-hla/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } description = "An easy to use high-level API (HLA) of MiniDNS' client" dependencies { api project(':minidns-dnssec') testImplementation project(path: ":minidns-client", configuration: "testRuntime") testImplementation project(path: ":minidns-dnssec", configuration: "testRuntime") } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/DnssecResolverApi.java ================================================ /* * 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); } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/ResolutionUnsuccessfulException.java ================================================ /* * 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; } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/ResolverApi.java ================================================ /* * 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; } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/ResolverResult.java ================================================ /* * 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); } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/SrvResolverResult.java ================================================ /* * 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; } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/srv/SrvProto.java ================================================ /* * 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()); } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/srv/SrvService.java ================================================ /* * 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); } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/srv/SrvServiceProto.java ================================================ /* * 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; } } ================================================ FILE: minidns-hla/src/main/java/org/minidns/hla/srv/SrvType.java ================================================ /* * 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; } } ================================================ FILE: minidns-hla/src/test/java/org/minidns/hla/MiniDnsHlaTest.java ================================================ /* * 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.junit.jupiter.api.Test; public class MiniDnsHlaTest { /** * Dummy test to make jacocoRootReport happy. */ @Test public void nopTest() { } } ================================================ FILE: minidns-integration-test/build.gradle ================================================ /* * Copyright 2015 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. */ plugins { id 'org.minidns.java-conventions' id 'org.minidns.application-conventions' } description = "MiniDNS' integration test suite" mainClassName = 'org.minidns.integrationtest.IntegrationTestHelper' applicationDefaultJvmArgs = ["-enableassertions"] dependencies { api project(':minidns-client') api project(':minidns-async') api project(':minidns-iterative-resolver') api project(':minidns-dnssec') api project(':minidns-hla') implementation "org.junit.vintage:junit-vintage-engine:$junitVersion" implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation project(path: ":minidns-client", configuration: "testRuntime") } run { // Pass all system properties down to the "application" run. // Used e.g. for integration test configuration via properties. systemProperties System.getProperties() } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/AsyncApiTest.java ================================================ /* * 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); } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/CoreTest.java ================================================ /* * 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); } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/DaneTest.java ================================================ /* * 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()); } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/DnssecTest.java ================================================ /* * 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()); } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/HlaTest.java ================================================ /* * 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()); } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/IntegrationTest.java ================================================ /* * 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; } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/IntegrationTestHelper.java ================================================ /* * 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; } } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/IntegrationTestTools.java ================================================ /* * 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; } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/IterativeDnssecTest.java ================================================ /* * 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; } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/integrationtest/NsidTest.java ================================================ /* * 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; } } ================================================ FILE: minidns-integration-test/src/main/java/org/minidns/jul/MiniDnsJul.java ================================================ /* * 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); } } ================================================ FILE: minidns-integration-test/src/test/java/org/minidns/integrationtest/IntegrationTestTest.java ================================================ /* * 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.junit.jupiter.api.Test; public class IntegrationTestTest { /** * Just here to ensure jacoco is not complaining. */ @Test public void emptyTest() { } } ================================================ FILE: minidns-iterative-resolver/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id 'org.minidns.android-conventions' } description = "A DNS client that can iteratively resolve resource records" dependencies { api project(':minidns-client') testImplementation project(path: ":minidns-client", configuration: "testRuntime") } ================================================ FILE: minidns-iterative-resolver/src/main/java/org/minidns/iterative/IterativeClientException.java ================================================ /* * 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; } } } ================================================ FILE: minidns-iterative-resolver/src/main/java/org/minidns/iterative/IterativeDnsClient.java ================================================ /* * 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; } } } ================================================ FILE: minidns-iterative-resolver/src/main/java/org/minidns/iterative/ReliableDnsClient.java ================================================ /* * 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); } } ================================================ FILE: minidns-iterative-resolver/src/main/java/org/minidns/iterative/ResolutionState.java ================================================ /* * 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--; } } ================================================ FILE: minidns-iterative-resolver/src/test/java/org/minidns/iterative/IterativeDnsClientTest.java ================================================ /* * 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.List; import org.minidns.cache.LruCache; import org.minidns.dnsmessage.DnsMessage; import org.minidns.dnsqueryresult.DnsQueryResult; import org.minidns.record.A; import org.minidns.record.Data; import org.minidns.record.Record; import org.minidns.record.Record.TYPE; import org.junit.jupiter.api.Test; import static org.minidns.DnsWorld.a; import static org.minidns.DnsWorld.applyZones; import static org.minidns.DnsWorld.ns; import static org.minidns.DnsWorld.record; import static org.minidns.DnsWorld.rootZone; import static org.minidns.DnsWorld.zone; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class IterativeDnsClientTest { @Test public void basicIterativeTest() throws IOException { IterativeDnsClient client = new IterativeDnsClient(new LruCache(0)); applyZones(client, rootZone( record("com", ns("ns.com")), record("ns.com", a("1.1.1.1")) ), zone("com", "ns.com", "1.1.1.1", record("example.com", ns("ns.example.com")), record("ns.example.com", a("1.1.1.2")) ), zone("example.com", "ns.example.com", "1.1.1.2", record("www.example.com", a("1.1.1.3")) ) ); DnsQueryResult result = client.query("www.example.com", TYPE.A); DnsMessage message = result.response; List> answers = message.answerSection; assertEquals(1, answers.size()); assertEquals(TYPE.A, answers.get(0).type); assertArrayEquals(new byte[] {1, 1, 1, 3}, ((A) answers.get(0).payloadData).getIp()); } @Test public void loopIterativeTest() throws IOException { IterativeDnsClient client = new IterativeDnsClient(new LruCache(0)); applyZones(client, rootZone( record("a", ns("a.ns")), record("b", ns("b.ns")), record("a.ns", a("1.1.1.1")), record("b.ns", a("1.1.1.2")) ), zone("a", "a.ns", "1.1.1.1", record("test.a", ns("a.test.b")) ), zone("b", "b.ns", "1.1.1.2", record("test.b", ns("b.test.a")) ) ); assertThrows(IterativeClientException.LoopDetected.class, () -> client.query("www.test.a", TYPE.A) ); } @Test public void notGluedNsTest() throws IOException { IterativeDnsClient client = new IterativeDnsClient(new LruCache(0)); applyZones(client, rootZone( record("com", ns("ns.com")), record("net", ns("ns.net")), record("ns.com", a("1.1.1.1")), record("ns.net", a("1.1.2.1")) ), zone("com", "ns.com", "1.1.1.1", record("example.com", ns("example.ns.net")) ), zone("net", "ns.net", "1.1.2.1", record("example.ns.net", a("1.1.2.2")) ), zone("example.com", "example.ns.net", "1.1.2.2", record("www.example.com", a("1.1.1.3")) ) ); DnsQueryResult result = client.query("www.example.com", TYPE.A); DnsMessage message = result.response; List> answers = message.answerSection; assertEquals(1, answers.size()); assertEquals(TYPE.A, answers.get(0).type); assertArrayEquals(new byte[] {1, 1, 1, 3}, ((A) answers.get(0).payloadData).getIp()); } } ================================================ FILE: minidns-repl/build.gradle ================================================ plugins { id 'org.minidns.java-conventions' id "com.github.alisiikh.scalastyle" version "3.5.0" } description = "A read-eval-print loop (REPL) for MiniDNS" apply plugin: 'scala' apply plugin: 'com.github.alisiikh.scalastyle' ext { scalaVersion = '2.13.13' } dependencies { api project(':minidns-client') api project(':minidns-iterative-resolver') api project(':minidns-dnssec') api project(':minidns-integration-test') api project(':minidns-hla') implementation "com.lihaoyi:ammonite_$scalaVersion:3.0.0-M1" testImplementation project(path: ":minidns-client", configuration: "testRuntime") } scalastyle { config = new File(rootConfigDir, 'scalaStyle.xml') verbose = true failOnWarning = true } check.dependsOn(scalastyleCheck) task printClasspath(dependsOn: assemble) { doLast { println sourceSets.main.runtimeClasspath.asPath } } ================================================ FILE: minidns-repl/scala.repl ================================================ org.minidns.minidnsrepl.MiniDnsRepl.init() import org.minidns._ import org.minidns.dnsmessage._ import org.minidns.record.Record.TYPE import org.minidns.record._ import org.minidns.dnssec.DnssecClient import org.minidns.minidnsrepl.MiniDnsRepl.clearCache import org.minidns.minidnsrepl.MiniDnsRepl.writeToFile import org.minidns.minidnsrepl.MiniDnsStats._ import org.minidns.jul.MiniDnsJul._ import java.net.InetAddress import java.util.logging._ def debugLog() = { val miniDnsLogger = Logger.getLogger("org.minidns") miniDnsLogger.setLevel(Level.FINE) val consoleHandler = new ConsoleHandler() consoleHandler.setLevel(Level.FINE) miniDnsLogger.addHandler(consoleHandler) } // Some standard values Predef.println("Set value 'c' to DNSClient") val c = org.minidns.minidnsrepl.MiniDnsRepl.DNSCLIENT Predef.println("Set value 'ic' to IterativeDnsClient") val ic = org.minidns.minidnsrepl.MiniDnsRepl.ITERATIVEDNSCLIENT Predef.println("Set value 'dc' to DnssecClient") val dc = org.minidns.minidnsrepl.MiniDnsRepl.DNSSECCLIENT // A normal resolver Predef.println("Set value 'r' to ResolverApi") val r = org.minidns.hla.ResolverApi.INSTANCE // A DNSSEC resolver Predef.println("Set value 'dr' to DnssecResolverApi") val dr = org.minidns.hla.DnssecResolverApi.INSTANCE Predef.println("Enjoy MiniDNS. Go ahead and try a query. For example:") Predef.println("c query (\"geekplace.eu\", TYPE.A)") Predef.println("dr resolveDnssecReliable (\"verteiltesysteme.net\", classOf[A])") Predef.println("NOTE: You can enable debug log output by calling 'debugLog'") ================================================ FILE: minidns-repl/src/main/java/org/minidns/minidnsrepl/DnssecStats.java ================================================ /* * 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 } } ================================================ FILE: minidns-repl/src/main/java/org/minidns/minidnsrepl/MiniDnsRepl.java ================================================ /* * 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); } } } ================================================ FILE: minidns-repl/src/main/java/org/minidns/minidnsrepl/MiniDnsStats.java ================================================ /* * 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; } } ================================================ FILE: minidns-repl/src/test/java/org/minidns/minidnsrepl/ReplTest.java ================================================ /* * 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 org.junit.jupiter.api.Test; public class ReplTest { /** * Just here to ensure jacoco is not complaining. */ @Test public void emptyTest() { } } ================================================ FILE: misc/resolve.pl ================================================ use Net::DNS; my $r = Net::DNS::Resolver->new(); # This script is provided as a reference. It's not really part of the # minidns source code. Rather, the files it downloaded when _I_ ran it # today form part of the source, and the junit tests verify that # minidns interprets those exact packets the same way that I do. # If you rerun the script, you may get other results than I did in # January 2015. Several of them likely depend on geolocation and load # balancing. # In short: If you update the packets you'll have to interpret them # correctly and update the interpretation tests appropriately. # do one DNS lookup and write the query packet to a file sub lookup() { my ($domain, $type, $filename) = @_; my $p = $r->query($domain, $type); # print for visibility during test analysis print $p->string; open F, ">$filename" or die "$!\n"; print F $p->data; close F; } # SRV and MX are near and dear to me &lookup("gmail.com", "mx", "gmail-mx"); &lookup("_xmpp-client._tcp.gulbrandsen.priv.no", "srv", "gpn-srv"); # A matters to everyone # sunracle uses a CNAME now, which affords an opportunity to test for # an attack &lookup("www.sun.com", "a", "sun-a"); # we'll modify the answer and change one AD RR, and then check that a # direct lookup is not affected (ie. our cache isn't poisoned) &lookup("legacy-sun.oraclegha.com", "a", "sun-real-a"); # AAAA doubles in importance every few months &lookup("google.com", "aaaa", "google-aaaa"); &lookup("oracle.com", "soa", "oracle-soa"); ================================================ FILE: misc/sbt/.gitignore ================================================ /project/ /target/ ================================================ FILE: misc/sbt/build.sbt ================================================ name := "MiniDNS Playground for Scala" version := "1.0" resolvers += Resolver.sonatypeRepo("snapshots") resolvers += Resolver.mavenLocal libraryDependencies += "org.minidns" % "minidns-client" % "latest.integration" libraryDependencies += "org.minidns" % "minidns-dnssec" % "latest.integration" initialCommands in console += "import org.minidns._;" initialCommands in console += "import org.minidns.Record.TYPE;" initialCommands in console += "val client = new DnsClient(new java.util.HashMap[Question,DnsMessage]())" ================================================ FILE: repl ================================================ #!/usr/bin/env bash set -euo pipefail JDWP=false JDWP_PORT=8000 while getopts djp: OPTION "$@"; do case $OPTION in d) set -x ;; j) JDWP=true ;; p) JDWP_PORT=$OPTARG ;; esac done EXTRA_JAVA_ARGS=() if $JDWP; then EXTRA_JAVA_ARGS+=("-Xdebug") EXTRA_JAVA_ARGS+=("-Xrunjdwp:server=y,transport=dt_socket,address=${JDWP_PORT},suspend=n") fi PROJECT_ROOT=$(dirname "${BASH_SOURCE[0]}") cd "${PROJECT_ROOT}" echo "Compiling and computing classpath (May take a while)" # Sadly even with the --quiet option Gradle (or some component of) # will print the number of warnings/errors to stdout if there are # any. So the result could look like # 52 warnings\n1 warning\n12 warnings\n # /smack/smack-repl/build/classes/main:/smack/smack-repl/build/ # resources/main:/smack/smack-tcp/build/libs/smack-tcp-4.2.0-alpha4-SNAPSHOT.jar # So perform a "tail -n1" on the output of gradle GRADLE_CLASSPATH="$(${GRADLE_BIN:-./gradlew} :minidns-repl:printClasspath --quiet |\ tail -n1)" echo "Finished, starting REPL" exec java \ "${EXTRA_JAVA_ARGS[@]}" \ -Dscala.usejavacp=true \ -classpath "${GRADLE_CLASSPATH}" \ ammonite.Main \ --predef minidns-repl/scala.repl ================================================ FILE: settings.gradle ================================================ pluginManagement { includeBuild('build-logic') } // The name of the root project. // If we would not set the name, then gradle would use the directory // name of the root directory rootProject.name = 'MiniDNS' include 'minidns-core' include 'minidns-client' include 'minidns-async' include 'minidns-iterative-resolver' include 'minidns-dnssec' include 'minidns-dane' include 'minidns-integration-test' include 'minidns-repl' include 'minidns-hla' include 'minidns-android23' ================================================ FILE: version ================================================ 1.1.2-SNAPSHOT