Integrate MiniDNS and change static DNS server
This commit is contained in:
parent
028559ffc1
commit
7d3fb6c4b9
151 changed files with 16799 additions and 171 deletions
|
@ -42,6 +42,8 @@ configurations {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.junit.jupiter:junit-jupiter:5.8.1'
|
||||
implementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
|
||||
androidTestImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
|
@ -82,7 +84,6 @@ dependencies {
|
|||
|
||||
implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation 'org.minidns:minidns-hla:1.1.1'
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
|
||||
implementation "com.wefika:flowlayout:0.4.1"
|
||||
|
@ -307,7 +308,7 @@ android {
|
|||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF']
|
||||
excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF', 'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md']
|
||||
}
|
||||
}
|
||||
lint {
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
package de.monocles.chat.test;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.lang.Thread;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
|
||||
import tools.fastlane.screengrab.Screengrab;
|
||||
import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar;
|
||||
import tools.fastlane.screengrab.locale.LocaleTestRule;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.entities.TransferablePlaceholder;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.test.R;
|
||||
import eu.siacs.conversations.ui.ConversationsActivity;
|
||||
import eu.siacs.conversations.ui.StartConversationActivity;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ScreenshotTest {
|
||||
|
||||
static String pkg = InstrumentationRegistry.getInstrumentation().getContext().getPackageName();
|
||||
static XmppConnectionService xmppConnectionService;
|
||||
static Account account;
|
||||
|
||||
@ClassRule
|
||||
public static final LocaleTestRule localeTestRule = new LocaleTestRule();
|
||||
|
||||
@ClassRule
|
||||
public static final ServiceTestRule xmppServiceRule = new ServiceTestRule();
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws TimeoutException {
|
||||
CleanStatusBar.enableWithDefaults();
|
||||
|
||||
Intent intent = new Intent(ApplicationProvider.getApplicationContext(), XmppConnectionService.class);
|
||||
intent.setAction("ui");
|
||||
xmppConnectionService = ((XmppConnectionBinder) xmppServiceRule.bindService(intent)).getService();
|
||||
account = xmppConnectionService.findAccountByJid(Jid.of("carrot@chaosah.hereva"));
|
||||
if (account == null) {
|
||||
account = new Account(
|
||||
Jid.of("carrot@chaosah.hereva"),
|
||||
"orangeandfurry"
|
||||
);
|
||||
xmppConnectionService.createAccount(account);
|
||||
}
|
||||
|
||||
Uri avatarUri = Uri.parse("android.resource://" + pkg + "/" + String.valueOf(R.drawable.carrot));
|
||||
final Avatar avatar = xmppConnectionService.getFileBackend().getPepAvatar(avatarUri, 192, Bitmap.CompressFormat.WEBP);
|
||||
xmppConnectionService.getFileBackend().save(avatar);
|
||||
account.setAvatar(avatar.getFilename());
|
||||
|
||||
Contact cheogram = account.getRoster().getContact(Jid.of("cheogram.com"));
|
||||
cheogram.setOption(Contact.Options.IN_ROSTER);
|
||||
cheogram.setPhotoUri("android.resource://" + pkg + "/" + String.valueOf(R.drawable.cheogram));
|
||||
Presence cheogramPresence = Presence.parse(null, null, "");
|
||||
IqPacket discoPacket = new IqPacket(IqPacket.TYPE.RESULT);
|
||||
Element query = discoPacket.addChild("query", "http://jabber.org/protocol/disco#info");
|
||||
Element identity = query.addChild("identity");
|
||||
identity.setAttribute("category", "gateway");
|
||||
identity.setAttribute("type", "pstn");
|
||||
cheogramPresence.setServiceDiscoveryResult(new ServiceDiscoveryResult(discoPacket));
|
||||
cheogram.updatePresence("gw", cheogramPresence);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardown() {
|
||||
CleanStatusBar.disable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConversation() throws FileBackend.FileCopyException, InterruptedException {
|
||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(account, Jid.of("+15550737737@cheogram.com"), false, false);
|
||||
conversation.getContact().setOption(Contact.Options.IN_ROSTER);
|
||||
conversation.getContact().setSystemName("Pepper");
|
||||
conversation.getContact().setPhotoUri("android.resource://" + pkg + "/" + String.valueOf(R.drawable.pepper));
|
||||
|
||||
Message voicemail = new Message(conversation, "", 0, Message.STATUS_RECEIVED);
|
||||
voicemail.setOob("https://example.com/thing.mp3");
|
||||
voicemail.setFileParams(new Message.FileParams("https://example.com/thing.mp3|5000|0|0|10000"));
|
||||
voicemail.setType(Message.TYPE_FILE);
|
||||
voicemail.setSubject("Voicemail Recording");
|
||||
|
||||
Message transcript = new Message(conversation, "Where are you?", 0, Message.STATUS_RECEIVED);
|
||||
transcript.setSubject("Voicemail Transcription");
|
||||
|
||||
Message picture = new Message(conversation, "", 0, Message.STATUS_SEND_RECEIVED);
|
||||
picture.setOob("https://example.com/thing.webp");
|
||||
picture.setType(Message.TYPE_FILE);
|
||||
xmppConnectionService.getFileBackend().copyFileToPrivateStorage(
|
||||
picture,
|
||||
Uri.parse("android.resource://" + pkg + "/" + String.valueOf(R.drawable.komona)),
|
||||
"image/webp"
|
||||
);
|
||||
xmppConnectionService.getFileBackend().updateFileParams(picture);
|
||||
|
||||
conversation.addAll(0, Arrays.asList(
|
||||
voicemail,
|
||||
transcript,
|
||||
new Message(conversation, "Meow", 0, Message.STATUS_SEND_RECEIVED),
|
||||
picture,
|
||||
new Message(conversation, "👍", 0, Message.STATUS_RECEIVED)
|
||||
));
|
||||
|
||||
ActivityScenario scenario = ActivityScenario.launch(ConversationsActivity.class);
|
||||
scenario.onActivity((Activity activity) -> {
|
||||
((ConversationsActivity) activity).switchToConversation(conversation);
|
||||
});
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
Thread.sleep(100); // ImageView not paited yet after waitForIdleSync
|
||||
Screengrab.screenshot("conversation");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartConversation() throws InterruptedException {
|
||||
ActivityScenario scenario = ActivityScenario.launch(StartConversationActivity.class);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
Thread.sleep(100); // ImageView not paited yet after waitForIdleSync
|
||||
Screengrab.screenshot("startConversation");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddContact() throws InterruptedException {
|
||||
ActivityScenario scenario = ActivityScenario.launch(StartConversationActivity.class);
|
||||
onView(withId(eu.siacs.conversations.R.id.speed_dial)).perform(click());
|
||||
Screengrab.screenshot("startConversationOptions");
|
||||
|
||||
// Not actually online, so can't screenshot the gateway selector yet
|
||||
/*onView(withId(eu.siacs.conversations.R.id.create_contact)).perform(click());
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
Thread.sleep(10000); // ImageView not paited yet after waitForIdleSync
|
||||
Screengrab.screenshot("addContact");*/
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
compileJava {
|
||||
options.bootstrapClasspath = files(androidBootClasspath)
|
||||
}
|
||||
javadoc {
|
||||
classpath += files(androidBootClasspath)
|
||||
}
|
10
src/main/groovy/org.minidns.android-conventions.gradle
Normal file
10
src/main/groovy/org.minidns.android-conventions.gradle
Normal file
|
@ -0,0 +1,10 @@
|
|||
plugins {
|
||||
id 'ru.vyarus.animalsniffer'
|
||||
id 'org.minidns.common-conventions'
|
||||
}
|
||||
dependencies {
|
||||
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:4.4.2_r4@signature"
|
||||
}
|
||||
animalsniffer {
|
||||
sourceSets = [sourceSets.main]
|
||||
}
|
12
src/main/groovy/org.minidns.application-conventions.gradle
Normal file
12
src/main/groovy/org.minidns.application-conventions.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
plugins {
|
||||
id 'application'
|
||||
}
|
||||
|
||||
application {
|
||||
applicationDefaultJvmArgs = ["-enableassertions"]
|
||||
}
|
||||
|
||||
run {
|
||||
// Pass all system properties down to the "application" run
|
||||
systemProperties System.getProperties()
|
||||
}
|
36
src/main/groovy/org.minidns.common-conventions.gradle
Normal file
36
src/main/groovy/org.minidns.common-conventions.gradle
Normal file
|
@ -0,0 +1,36 @@
|
|||
ext {
|
||||
javaVersion = JavaVersion.VERSION_11
|
||||
javaMajor = javaVersion.getMajorVersion()
|
||||
minAndroidSdk = 19
|
||||
|
||||
androidBootClasspath = getAndroidRuntimeJar(minAndroidSdk)
|
||||
|
||||
// Export the function by turning it into a closure.
|
||||
// https://stackoverflow.com/a/23290820/194894
|
||||
getAndroidRuntimeJar = this.&getAndroidRuntimeJar
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
def getAndroidRuntimeJar(androidApiLevel) {
|
||||
def androidHome = getAndroidHome()
|
||||
def androidJar = new File("$androidHome/platforms/android-${androidApiLevel}/android.jar")
|
||||
if (androidJar.isFile()) {
|
||||
return androidJar
|
||||
} else {
|
||||
throw new Exception("Can't find android.jar for API level ${androidApiLevel}. Please install corresponding SDK platform package")
|
||||
}
|
||||
}
|
||||
|
||||
def getAndroidHome() {
|
||||
def androidHomeEnv = System.getenv("ANDROID_HOME")
|
||||
if (androidHomeEnv == null) {
|
||||
throw new Exception("ANDROID_HOME environment variable is not set")
|
||||
}
|
||||
def androidHome = new File(androidHomeEnv)
|
||||
if (!androidHome.isDirectory()) throw new Exception("Environment variable ANDROID_HOME is not pointing to a directory")
|
||||
return androidHome
|
||||
}
|
302
src/main/groovy/org.minidns.java-conventions.gradle
Normal file
302
src/main/groovy/org.minidns.java-conventions.gradle
Normal file
|
@ -0,0 +1,302 @@
|
|||
plugins {
|
||||
id 'biz.aQute.bnd.builder'
|
||||
id 'checkstyle'
|
||||
id 'eclipse'
|
||||
id 'idea'
|
||||
id 'jacoco'
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
id 'java-test-fixtures'
|
||||
id 'maven-publish'
|
||||
id 'net.ltgt.errorprone'
|
||||
id 'signing'
|
||||
|
||||
id 'jacoco-report-aggregation'
|
||||
id 'test-report-aggregation'
|
||||
|
||||
id 'org.minidns.common-conventions'
|
||||
id 'org.minidns.javadoc-conventions'
|
||||
}
|
||||
|
||||
version readVersionFile()
|
||||
|
||||
ext {
|
||||
isSnapshot = version.endsWith('-SNAPSHOT')
|
||||
gitCommit = getGitCommit()
|
||||
rootConfigDir = new File(rootDir, 'config')
|
||||
sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')
|
||||
isReleaseVersion = !isSnapshot
|
||||
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))
|
||||
signingRequired = !(isSnapshot || isContinuousIntegrationEnvironment)
|
||||
sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots'
|
||||
sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
|
||||
builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date())
|
||||
|
||||
junitVersion = '5.9.2'
|
||||
|
||||
if (project.hasProperty("useSonatype")) {
|
||||
useSonatype = project.getProperty("useSonatype").toBoolean()
|
||||
} else {
|
||||
// Default to true
|
||||
useSonatype = true
|
||||
}
|
||||
}
|
||||
|
||||
group = 'org.minidns'
|
||||
|
||||
java {
|
||||
sourceCompatibility = javaVersion
|
||||
targetCompatibility = sourceCompatibility
|
||||
}
|
||||
|
||||
eclipse {
|
||||
classpath {
|
||||
downloadJavadoc = true
|
||||
}
|
||||
}
|
||||
|
||||
// Make all project's 'test' target depend on javadoc, so that
|
||||
// javadoc is also linted.
|
||||
test.dependsOn javadoc
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
// Some systems may not have set their platform default
|
||||
// converter to 'utf8', but we use unicode in our source
|
||||
// files. Therefore ensure that javac uses unicode
|
||||
options.encoding = "utf8"
|
||||
options.compilerArgs = [
|
||||
'-Xlint:all',
|
||||
// Set '-options' because a non-java7 javac will emit a
|
||||
// warning if source/target is set to 1.7 and
|
||||
// bootclasspath is *not* set.
|
||||
'-Xlint:-options',
|
||||
// TODO: Enable xlint serial
|
||||
'-Xlint:-serial',
|
||||
'-Werror',
|
||||
]
|
||||
options.release = Integer.valueOf(javaMajor)
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.12"
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
reports {
|
||||
xml.required = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||
|
||||
testFixturesApi "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||
// https://stackoverflow.com/a/77274251/194894
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.0"
|
||||
|
||||
errorprone 'com.google.errorprone:error_prone_core:2.32.0'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
|
||||
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
|
||||
|
||||
// Enable full stacktraces of failed tests. Especially handy
|
||||
// for CI environments.
|
||||
testLogging {
|
||||
events "failed"
|
||||
exceptionFormat "full"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Implementation-Version': version,
|
||||
'Implementation-GitRevision': gitCommit,
|
||||
'Built-JDK': System.getProperty('java.version'),
|
||||
'Built-Gradle': gradle.gradleVersion,
|
||||
'Built-By': System.getProperty('user.name')
|
||||
)
|
||||
}
|
||||
|
||||
bundle {
|
||||
bnd(
|
||||
'-removeheaders': 'Tool, Bnd-*',
|
||||
'-exportcontents': 'org.minidns.*',
|
||||
'Import-Package': '!android,*'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '10.18.2'
|
||||
|
||||
configProperties.checkstyleLicenseHeader = "header"
|
||||
}
|
||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||
archiveClassifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
archiveClassifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
task testsJar(type: Jar) {
|
||||
archiveClassifier = 'tests'
|
||||
from sourceSets.test.output
|
||||
}
|
||||
configurations {
|
||||
testRuntime
|
||||
}
|
||||
artifacts {
|
||||
// Add a 'testRuntime' configuration including the tests so that
|
||||
// it can be consumed by other projects. See
|
||||
// http://stackoverflow.com/a/21946676/194894
|
||||
testRuntime testsJar
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
artifact testsJar
|
||||
pom {
|
||||
name = 'MiniDNS'
|
||||
packaging = 'jar'
|
||||
inceptionYear = '2014'
|
||||
url = 'https://github.com/minidns/minidns'
|
||||
afterEvaluate {
|
||||
description = project.description
|
||||
}
|
||||
|
||||
scm {
|
||||
url = 'https://github.com/minidns/minidns'
|
||||
connection = 'scm:https://github.com/minidns/minidns'
|
||||
developerConnection = 'scm:git://github.com/minidns/minidns.git'
|
||||
}
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution = 'repo'
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
id = 'rtreffer'
|
||||
name = 'Rene Treffer'
|
||||
email = 'treffer@measite.de'
|
||||
}
|
||||
developer {
|
||||
id = 'flow'
|
||||
name = 'Florian Schmaus'
|
||||
email = 'flow@geekplace.eu'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
if (sonatypeCredentialsAvailable && useSonatype) {
|
||||
maven {
|
||||
url isSnapshot ? sonatypeSnapshotUrl : sonatypeStagingUrl
|
||||
credentials {
|
||||
username = sonatypeUsername
|
||||
password = sonatypePassword
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use
|
||||
// gradle publish -P customRepoUrl=https://www.igniterealtime.org/archiva/repository/maven -P customRepoUsername=bamboo -P customRepoPassword=hidden -P useSonatype=false
|
||||
// to deploy to this repo.
|
||||
if (project.hasProperty("customRepoUrl")) {
|
||||
maven {
|
||||
name 'customRepo'
|
||||
url customRepoUrl
|
||||
if (project.hasProperty("customRepoUsername")) {
|
||||
credentials {
|
||||
username customRepoUsername
|
||||
password customRepoPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for gpg signatory not supporting the 'required' option
|
||||
// See https://github.com/gradle/gradle/issues/5064#issuecomment-381924984
|
||||
// Note what we use 'signing.gnupg.keyName' instead of 'signing.keyId'.
|
||||
tasks.withType(Sign) {
|
||||
onlyIf {
|
||||
project.hasProperty('signing.gnupg.keyName')
|
||||
}
|
||||
}
|
||||
signing {
|
||||
required { signingRequired }
|
||||
useGpgCmd()
|
||||
sign publishing.publications.mavenJava
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.errorprone {
|
||||
disableWarningsInGeneratedCode = true
|
||||
excludedPaths = ".*/jmh_generated/.*"
|
||||
error(
|
||||
"UnusedVariable",
|
||||
"UnusedMethod",
|
||||
"MethodCanBeStatic",
|
||||
)
|
||||
errorproneArgs = [
|
||||
// Disable MissingCasesInEnumSwitch error prone check
|
||||
// because this check is already done by javac as incomplete-switch.
|
||||
'-Xep:MissingCasesInEnumSwitch:OFF',
|
||||
'-Xep:StringSplitter:OFF',
|
||||
'-Xep:JavaTimeDefaultTimeZone:OFF',
|
||||
'-Xep:InlineMeSuggester:OFF',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Work around https://github.com/gradle/gradle/issues/4046
|
||||
task copyJavadocDocFiles(type: Copy) {
|
||||
from('src/javadoc')
|
||||
into 'build/docs/javadoc'
|
||||
include '**/doc-files/*.*'
|
||||
}
|
||||
javadoc.dependsOn copyJavadocDocFiles
|
||||
|
||||
def getGitCommit() {
|
||||
def projectDirFile = new File("$projectDir")
|
||||
|
||||
def cmd = 'git describe --always --tags --dirty=+'
|
||||
def proc = cmd.execute(null, projectDirFile)
|
||||
|
||||
def exitStatus = proc.waitFor()
|
||||
if (exitStatus != 0) return "non-git build"
|
||||
|
||||
def gitCommit = proc.text.trim()
|
||||
assert !gitCommit.isEmpty()
|
||||
gitCommit
|
||||
}
|
||||
|
||||
def readVersionFile() {
|
||||
def versionFile = new File(rootDir, 'version')
|
||||
if (!versionFile.isFile()) {
|
||||
throw new Exception("Could not find version file")
|
||||
}
|
||||
if (versionFile.text.isEmpty()) {
|
||||
throw new Exception("Version file does not contain a version")
|
||||
}
|
||||
versionFile.text.trim()
|
||||
}
|
24
src/main/groovy/org.minidns.javadoc-conventions.gradle
Normal file
24
src/main/groovy/org.minidns.javadoc-conventions.gradle
Normal file
|
@ -0,0 +1,24 @@
|
|||
plugins {
|
||||
// Javadoc linking requires repositories to bet configured. And
|
||||
// those are declared in common-conventions, hence we add it here.
|
||||
id 'org.minidns.common-conventions'
|
||||
}
|
||||
|
||||
|
||||
tasks.withType(Javadoc) {
|
||||
// The '-quiet' as second argument is actually a hack,
|
||||
// since the one parameter addStringOption doesn't seem to
|
||||
// work, we extra add '-quiet', which is added anyway by
|
||||
// gradle.
|
||||
// We disable 'missing' as we do most of javadoc checking via checkstyle.
|
||||
options.addStringOption('Xdoclint:all,-missing', '-quiet')
|
||||
// Abort on javadoc warnings.
|
||||
// See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363)
|
||||
// for information about the -Xwerror option.
|
||||
options.addStringOption('Xwerror', '-quiet')
|
||||
options.addStringOption('-release', javaMajor)
|
||||
}
|
||||
|
||||
tasks.withType(Javadoc) {
|
||||
options.charSet = "UTF-8"
|
||||
}
|
477
src/main/java/org/minidns/AbstractDnsClient.java
Normal file
477
src/main/java/org/minidns/AbstractDnsClient.java
Normal file
|
@ -0,0 +1,477 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import org.minidns.MiniDnsFuture.InternalMiniDnsFuture;
|
||||
import org.minidns.cache.LruCache;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.record.A;
|
||||
import org.minidns.record.AAAA;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.NS;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.Record.CLASS;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
import org.minidns.source.DnsDataSource;
|
||||
import org.minidns.source.NetworkDataSource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
|
||||
* This circumvents the missing javax.naming package on android.
|
||||
*/
|
||||
public abstract class AbstractDnsClient {
|
||||
|
||||
protected static final LruCache DEFAULT_CACHE = new LruCache();
|
||||
|
||||
protected static final Logger LOGGER = Logger.getLogger(AbstractDnsClient.class.getName());
|
||||
|
||||
/**
|
||||
* This callback is used by the synchronous query() method <b>and</b> 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<DnsQueryResult, IOException> queryAsync(CharSequence name, TYPE type) {
|
||||
Question q = new Question(name, type, CLASS.IN);
|
||||
return queryAsync(q);
|
||||
}
|
||||
|
||||
public final MiniDnsFuture<DnsQueryResult, IOException> queryAsync(Question q) {
|
||||
DnsMessage.Builder query = buildMessage(q);
|
||||
return queryAsync(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of an asynchronous DNS query which just wraps the synchronous case.
|
||||
* <p>
|
||||
* Subclasses override this method to support true asynchronous queries.
|
||||
* </p>
|
||||
*
|
||||
* @param query the query.
|
||||
* @return a future for this query.
|
||||
*/
|
||||
protected MiniDnsFuture<DnsQueryResult, IOException> queryAsync(DnsMessage.Builder query) {
|
||||
InternalMiniDnsFuture<DnsQueryResult, IOException> 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<DnsQueryResult, IOException> 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<? extends Data> 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<DnsQueryResult, IOException> 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 <code>null</code>.
|
||||
*/
|
||||
public DnsCache getCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
protected DnsMessage getQueryFor(Question q) {
|
||||
DnsMessage.Builder messageBuilder = buildMessage(q);
|
||||
DnsMessage query = messageBuilder.build();
|
||||
return query;
|
||||
}
|
||||
|
||||
private <D extends Data> Set<D> 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<NS> getCachedNameserverRecordsFor(DnsName dnsName) {
|
||||
return getCachedRecordsFor(dnsName, TYPE.NS);
|
||||
}
|
||||
|
||||
public Set<A> getCachedIPv4AddressesFor(DnsName dnsName) {
|
||||
return getCachedRecordsFor(dnsName, TYPE.A);
|
||||
}
|
||||
|
||||
public Set<AAAA> getCachedIPv6AddressesFor(DnsName dnsName) {
|
||||
return getCachedRecordsFor(dnsName, TYPE.AAAA);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <D extends Data> Set<D> getCachedIPNameserverAddressesFor(DnsName dnsName, TYPE type) {
|
||||
Set<NS> nsSet = getCachedNameserverRecordsFor(dnsName);
|
||||
if (nsSet.isEmpty())
|
||||
return Collections.emptySet();
|
||||
|
||||
Set<D> res = new HashSet<>(3 * nsSet.size());
|
||||
for (NS ns : nsSet) {
|
||||
Set<D> addresses;
|
||||
switch (type) {
|
||||
case A:
|
||||
addresses = (Set<D>) getCachedIPv4AddressesFor(ns.target);
|
||||
break;
|
||||
case AAAA:
|
||||
addresses = (Set<D>) getCachedIPv6AddressesFor(ns.target);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
res.addAll(addresses);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public Set<A> getCachedIPv4NameserverAddressesFor(DnsName dnsName) {
|
||||
return getCachedIPNameserverAddressesFor(dnsName, TYPE.A);
|
||||
}
|
||||
|
||||
public Set<AAAA> getCachedIPv6NameserverAddressesFor(DnsName dnsName) {
|
||||
return getCachedIPNameserverAddressesFor(dnsName, TYPE.AAAA);
|
||||
}
|
||||
}
|
50
src/main/java/org/minidns/DnsCache.java
Normal file
50
src/main/java/org/minidns/DnsCache.java
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.CachedDnsQueryResult;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
|
||||
/**
|
||||
* Cache for DNS Entries. Implementations must be thread safe.
|
||||
*/
|
||||
public abstract class DnsCache {
|
||||
|
||||
public static final int DEFAULT_CACHE_SIZE = 512;
|
||||
|
||||
/**
|
||||
* Add an an dns answer/response for a given dns question. Implementations
|
||||
* should honor the ttl / receive timestamp.
|
||||
* @param query The query message containing a question.
|
||||
* @param result The DNS query result.
|
||||
*/
|
||||
public final void put(DnsMessage query, DnsQueryResult result) {
|
||||
putNormalized(query.asNormalizedVersion(), result);
|
||||
}
|
||||
|
||||
protected abstract void putNormalized(DnsMessage normalizedQuery, DnsQueryResult result);
|
||||
|
||||
public abstract void offer(DnsMessage query, DnsQueryResult result, DnsName authoritativeZone);
|
||||
|
||||
/**
|
||||
* Request a cached dns response.
|
||||
* @param query The query message containing a question.
|
||||
* @return The dns message.
|
||||
*/
|
||||
public final CachedDnsQueryResult get(DnsMessage query) {
|
||||
return getNormalized(query.asNormalizedVersion());
|
||||
}
|
||||
|
||||
protected abstract CachedDnsQueryResult getNormalized(DnsMessage normalizedQuery);
|
||||
|
||||
}
|
492
src/main/java/org/minidns/DnsClient.java
Normal file
492
src/main/java/org/minidns/DnsClient.java
Normal file
|
@ -0,0 +1,492 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import org.minidns.MiniDnsException.ErrorResponseException;
|
||||
import org.minidns.MiniDnsException.NoQueryPossibleException;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.dnsserverlookup.AndroidUsingExec;
|
||||
import org.minidns.dnsserverlookup.AndroidUsingReflection;
|
||||
import org.minidns.dnsserverlookup.DnsServerLookupMechanism;
|
||||
import org.minidns.dnsserverlookup.UnixUsingEtcResolvConf;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
import org.minidns.util.CollectionsUtil;
|
||||
import org.minidns.util.InetAddressUtil;
|
||||
import org.minidns.util.MultipleIoException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
|
||||
* This circumvents the missing javax.naming package on android.
|
||||
*/
|
||||
public class DnsClient extends AbstractDnsClient {
|
||||
|
||||
static final List<DnsServerLookupMechanism> LOOKUP_MECHANISMS = new CopyOnWriteArrayList<>();
|
||||
|
||||
static final Set<Inet4Address> STATIC_IPV4_DNS_SERVERS = new CopyOnWriteArraySet<>();
|
||||
static final Set<Inet6Address> STATIC_IPV6_DNS_SERVERS = new CopyOnWriteArraySet<>();
|
||||
|
||||
static {
|
||||
addDnsServerLookupMechanism(AndroidUsingExec.INSTANCE);
|
||||
addDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE);
|
||||
addDnsServerLookupMechanism(UnixUsingEtcResolvConf.INSTANCE);
|
||||
|
||||
try {
|
||||
Inet4Address dnsforgeV4Dns = InetAddressUtil.ipv4From("176.9.93.198");
|
||||
STATIC_IPV4_DNS_SERVERS.add(dnsforgeV4Dns);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e);
|
||||
}
|
||||
|
||||
try {
|
||||
Inet6Address dnsforgeV6Dns = InetAddressUtil.ipv6From("[2a01:4f8:151:34aa::198]");
|
||||
STATIC_IPV6_DNS_SERVERS.add(dnsforgeV6Dns);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.log(Level.WARNING, "Could not add static IPv6 DNS Server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Set<String> blacklistedDnsServers = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(4));
|
||||
|
||||
private final Set<InetAddress> nonRaServers = Collections.newSetFromMap(new ConcurrentHashMap<InetAddress, Boolean>(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<InetAddress> getServerAddresses() {
|
||||
List<InetAddress> 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<InetAddress> dnsServerAddresses = getServerAddresses();
|
||||
|
||||
List<IOException> 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<DnsQueryResult, IOException> 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<InetAddress> dnsServerAddresses = getServerAddresses();
|
||||
|
||||
// Filter loop.
|
||||
Iterator<InetAddress> 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<MiniDnsFuture<DnsQueryResult, IOException>> futures = new ArrayList<>(dnsServerAddresses.size());
|
||||
// "Main" loop.
|
||||
for (InetAddress dns : dnsServerAddresses) {
|
||||
MiniDnsFuture<DnsQueryResult, IOException> 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).
|
||||
* <p>
|
||||
* The addresses are discovered by using one (or more) of the configured {@link DnsServerLookupMechanism}s.
|
||||
* </p>
|
||||
*
|
||||
* @return A list of DNS server IP addresses configured for this system.
|
||||
*/
|
||||
public static List<String> findDNS() {
|
||||
List<String> 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<String> 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<String> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @return A list of DNS server addresses.
|
||||
* @see #findDNS()
|
||||
*/
|
||||
public static List<InetAddress> findDnsAddresses() {
|
||||
// The findDNS() method contract guarantees that only IP addresses will be returned.
|
||||
List<String> res = findDNS();
|
||||
|
||||
if (res == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
final IpVersionSetting setting = DEFAULT_IP_VERSION_SETTING;
|
||||
|
||||
List<Inet4Address> ipv4DnsServer = null;
|
||||
List<Inet6Address> 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<InetAddress> 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<DnsServerLookupMechanism> 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
19
src/main/java/org/minidns/MiniDnsConfiguration.java
Normal file
19
src/main/java/org/minidns/MiniDnsConfiguration.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
public class MiniDnsConfiguration {
|
||||
|
||||
public static String getVersion() {
|
||||
return MiniDnsInitialization.VERSION;
|
||||
}
|
||||
|
||||
}
|
120
src/main/java/org/minidns/MiniDnsException.java
Normal file
120
src/main/java/org/minidns/MiniDnsException.java
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
|
||||
public abstract class MiniDnsException extends IOException {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected MiniDnsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static class IdMismatch extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DnsMessage request;
|
||||
private final DnsMessage response;
|
||||
|
||||
public IdMismatch(DnsMessage request, DnsMessage response) {
|
||||
super(getString(request, response));
|
||||
assert request.id != response.id;
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public DnsMessage getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public DnsMessage getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String getString(DnsMessage request, DnsMessage response) {
|
||||
return "The response's ID doesn't matches the request ID. Request: " + request.id + ". Response: " + response.id;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NullResultException extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DnsMessage request;
|
||||
|
||||
public NullResultException(DnsMessage request) {
|
||||
super("The request yielded a 'null' result while resolving.");
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public DnsMessage getRequest() {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ErrorResponseException extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DnsMessage request;
|
||||
private final DnsQueryResult result;
|
||||
|
||||
public ErrorResponseException(DnsMessage request, DnsQueryResult result) {
|
||||
super("Received " + result.response.responseCode + " error response\n" + result);
|
||||
this.request = request;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public DnsMessage getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public DnsQueryResult getResult() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NoQueryPossibleException extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DnsMessage request;
|
||||
|
||||
public NoQueryPossibleException(DnsMessage request) {
|
||||
super("No DNS server could be queried");
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public DnsMessage getRequest() {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
282
src/main/java/org/minidns/MiniDnsFuture.java
Normal file
282
src/main/java/org/minidns/MiniDnsFuture.java
Normal file
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.minidns.util.CallbackRecipient;
|
||||
import org.minidns.util.ExceptionCallback;
|
||||
import org.minidns.util.MultipleIoException;
|
||||
import org.minidns.util.SuccessCallback;
|
||||
|
||||
public abstract class MiniDnsFuture<V, E extends Exception> implements Future<V>, CallbackRecipient<V, E> {
|
||||
|
||||
private boolean cancelled;
|
||||
|
||||
protected V result;
|
||||
|
||||
protected E exception;
|
||||
|
||||
private SuccessCallback<V> successCallback;
|
||||
|
||||
private ExceptionCallback<E> 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<V, E> onSuccess(SuccessCallback<V> successCallback) {
|
||||
this.successCallback = successCallback;
|
||||
maybeInvokeCallbacks();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallbackRecipient<V, E> onError(ExceptionCallback<E> 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<Runnable> 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<V, E extends Exception> extends MiniDnsFuture<V, E> {
|
||||
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 <V, E extends Exception> MiniDnsFuture<V, E> from(V result) {
|
||||
InternalMiniDnsFuture<V, E> future = new InternalMiniDnsFuture<>();
|
||||
future.setResult(result);
|
||||
return future;
|
||||
}
|
||||
|
||||
public static <V> MiniDnsFuture<V, IOException> anySuccessfulOf(Collection<MiniDnsFuture<V, IOException>> futures) {
|
||||
return anySuccessfulOf(futures, exceptions -> MultipleIoException.toIOException(exceptions));
|
||||
}
|
||||
|
||||
public interface ExceptionsWrapper<EI extends Exception, EO extends Exception> {
|
||||
EO wrap(List<EI> exceptions);
|
||||
}
|
||||
|
||||
public static <V, EI extends Exception, EO extends Exception> MiniDnsFuture<V, EO> anySuccessfulOf(
|
||||
Collection<MiniDnsFuture<V, EI>> futures,
|
||||
ExceptionsWrapper<EI, EO> exceptionsWrapper) {
|
||||
InternalMiniDnsFuture<V, EO> returnedFuture = new InternalMiniDnsFuture<>();
|
||||
|
||||
final List<EI> exceptions = Collections.synchronizedList(new ArrayList<>(futures.size()));
|
||||
|
||||
for (MiniDnsFuture<V, EI> future : futures) {
|
||||
future.onSuccess(new SuccessCallback<V>() {
|
||||
@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<V, EI> futureToCancel : futures) {
|
||||
futureToCancel.cancel(true);
|
||||
}
|
||||
returnedFuture.setResult(result);
|
||||
}
|
||||
});
|
||||
future.onError(new ExceptionCallback<EI>() {
|
||||
@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;
|
||||
}
|
||||
}
|
48
src/main/java/org/minidns/MiniDnsInitialization.java
Normal file
48
src/main/java/org/minidns/MiniDnsInitialization.java
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class MiniDnsInitialization {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(MiniDnsInitialization.class.getName());
|
||||
|
||||
static final String VERSION;
|
||||
|
||||
static {
|
||||
String miniDnsVersion;
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
InputStream is = MiniDnsInitialization.class.getClassLoader().getResourceAsStream("org.minidns/version");
|
||||
reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
|
||||
miniDnsVersion = reader.readLine();
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.SEVERE, "Could not determine MiniDNS version", e);
|
||||
miniDnsVersion = "unkown";
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING, "IOException closing stream", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
VERSION = miniDnsVersion;
|
||||
}
|
||||
}
|
99
src/main/java/org/minidns/RrSet.java
Normal file
99
src/main/java/org/minidns/RrSet.java
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.Record.CLASS;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
public final class RrSet {
|
||||
|
||||
public final DnsName name;
|
||||
public final TYPE type;
|
||||
public final CLASS clazz;
|
||||
public final Set<Record<? extends Data>> records;
|
||||
|
||||
private RrSet(DnsName name, TYPE type, CLASS clazz, Set<Record<? extends Data>> 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<Record<? extends Data>> records = new LinkedHashSet<>(8);
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder addRecord(Record<? extends Data> 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<? extends Data> record) {
|
||||
if (name == null) {
|
||||
return true;
|
||||
}
|
||||
return name.equals(record.name) && type == record.type && clazz == record.clazz;
|
||||
}
|
||||
|
||||
public boolean addIfPossible(Record<? extends Data> 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);
|
||||
}
|
||||
}
|
||||
}
|
129
src/main/java/org/minidns/cache/ExtendedLruCache.java
vendored
Normal file
129
src/main/java/org/minidns/cache/ExtendedLruCache.java
vendored
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.cache;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.CachedDnsQueryResult;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.dnsqueryresult.SynthesizedCachedDnsQueryResult;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.Record;
|
||||
|
||||
/**
|
||||
* A variant of {@link LruCache} also using the data found in the sections for caching.
|
||||
*/
|
||||
public class ExtendedLruCache extends LruCache {
|
||||
|
||||
public ExtendedLruCache() {
|
||||
this(DEFAULT_CACHE_SIZE);
|
||||
}
|
||||
|
||||
public ExtendedLruCache(int capacity) {
|
||||
super(capacity);
|
||||
}
|
||||
|
||||
public ExtendedLruCache(int capacity, long maxTTL) {
|
||||
super(capacity, maxTTL);
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnsynchronizedOverridesSynchronized")
|
||||
@Override
|
||||
protected void putNormalized(DnsMessage q, DnsQueryResult result) {
|
||||
super.putNormalized(q, result);
|
||||
DnsMessage message = result.response;
|
||||
Map<DnsMessage, List<Record<? extends Data>>> 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<DnsMessage, List<Record<? extends Data>>> 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<DnsMessage, List<Record<?extends Data>>> extraCaches, DnsMessage q, List<Record<? extends Data>> records, DnsName authoritativeZone) {
|
||||
for (Record<? extends Data> 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<Record<? extends Data>> additionalRecords = extraCaches.get(additionalRecordQuestion);
|
||||
if (additionalRecords == null) {
|
||||
additionalRecords = new ArrayList<>();
|
||||
extraCaches.put(additionalRecordQuestion, additionalRecords);
|
||||
}
|
||||
additionalRecords.add(extraRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void putExtraCaches(DnsQueryResult synthesynthesizationSource, Map<DnsMessage, List<Record<? extends Data>>> extraCaches) {
|
||||
DnsMessage reply = synthesynthesizationSource.response;
|
||||
for (Entry<DnsMessage, List<Record<? extends Data>>> 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<? extends Data> 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;
|
||||
}
|
||||
|
||||
}
|
39
src/main/java/org/minidns/cache/FullLruCache.java
vendored
Normal file
39
src/main/java/org/minidns/cache/FullLruCache.java
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.cache;
|
||||
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.Record;
|
||||
|
||||
/**
|
||||
* An <b>insecure</b> 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<? extends Data> extraRecord, Question question, DnsName authoritativeZone) {
|
||||
return true;
|
||||
}
|
||||
}
|
169
src/main/java/org/minidns/cache/LruCache.java
vendored
Normal file
169
src/main/java/org/minidns/cache/LruCache.java
vendored
Normal file
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.cache;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.minidns.DnsCache;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.CachedDnsQueryResult;
|
||||
import org.minidns.dnsqueryresult.DirectCachedDnsQueryResult;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
|
||||
/**
|
||||
* LRU based DNSCache backed by a LinkedHashMap.
|
||||
*/
|
||||
public class LruCache extends DnsCache {
|
||||
|
||||
/**
|
||||
* Internal miss count.
|
||||
*/
|
||||
protected long missCount = 0L;
|
||||
|
||||
/**
|
||||
* Internal expire count (subset of misses that was caused by expire).
|
||||
*/
|
||||
protected long expireCount = 0L;
|
||||
|
||||
/**
|
||||
* Internal hit count.
|
||||
*/
|
||||
protected long hitCount = 0L;
|
||||
|
||||
/**
|
||||
* The internal capacity of the backend cache.
|
||||
*/
|
||||
protected int capacity;
|
||||
|
||||
/**
|
||||
* The upper bound of the ttl. All longer TTLs will be capped by this ttl.
|
||||
*/
|
||||
protected long maxTTL;
|
||||
|
||||
/**
|
||||
* The backend cache.
|
||||
*/
|
||||
protected LinkedHashMap<DnsMessage, CachedDnsQueryResult> 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<DnsMessage, CachedDnsQueryResult>(
|
||||
Math.min(capacity + (capacity + 3) / 4 + 2, 11), 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(
|
||||
Entry<DnsMessage, CachedDnsQueryResult> 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) {
|
||||
}
|
||||
}
|
19
src/main/java/org/minidns/cache/MiniDnsCacheFactory.java
vendored
Normal file
19
src/main/java/org/minidns/cache/MiniDnsCacheFactory.java
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.cache;
|
||||
|
||||
import org.minidns.DnsCache;
|
||||
|
||||
public interface MiniDnsCacheFactory {
|
||||
|
||||
DnsCache newCache();
|
||||
|
||||
}
|
107
src/main/java/org/minidns/constants/DnsRootServer.java
Normal file
107
src/main/java/org/minidns/constants/DnsRootServer.java
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.constants;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class DnsRootServer {
|
||||
|
||||
private static final Map<Character, Inet4Address> IPV4_ROOT_SERVER_MAP = new HashMap<>();
|
||||
|
||||
private static final Map<Character, Inet6Address> 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);
|
||||
}
|
||||
|
||||
}
|
99
src/main/java/org/minidns/constants/DnssecConstants.java
Normal file
99
src/main/java/org/minidns/constants/DnssecConstants.java
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.constants;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class DnssecConstants {
|
||||
/**
|
||||
* Do not allow to instantiate DNSSECConstants
|
||||
*/
|
||||
private DnssecConstants() {
|
||||
}
|
||||
|
||||
private static final Map<Byte, SignatureAlgorithm> SIGNATURE_ALGORITHM_LUT = new HashMap<>();
|
||||
|
||||
/**
|
||||
* DNSSEC Signature Algorithms.
|
||||
*
|
||||
* @see <a href=
|
||||
* "http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml">
|
||||
* IANA DNSSEC Algorithm Numbers</a>
|
||||
*/
|
||||
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<Byte, DigestAlgorithm> DELEGATION_DIGEST_LUT = new HashMap<>();
|
||||
|
||||
/**
|
||||
* DNSSEC Digest Algorithms.
|
||||
*
|
||||
* @see <a href=
|
||||
* "https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml">
|
||||
* IANA Delegation Signer (DS) Resource Record (RR)</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
65
src/main/java/org/minidns/dane/DaneCertificateException.java
Normal file
65
src/main/java/org/minidns/dane/DaneCertificateException.java
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dane;
|
||||
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.minidns.record.TLSA;
|
||||
|
||||
public abstract class DaneCertificateException extends CertificateException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected DaneCertificateException() {
|
||||
}
|
||||
|
||||
protected DaneCertificateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static class CertificateMismatch extends DaneCertificateException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public final TLSA tlsa;
|
||||
public final byte[] computed;
|
||||
|
||||
public CertificateMismatch(TLSA tlsa, byte[] computed) {
|
||||
super("The TLSA RR does not match the certificate");
|
||||
this.tlsa = tlsa;
|
||||
this.computed = computed;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MultipleCertificateMismatchExceptions extends DaneCertificateException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public final List<CertificateMismatch> certificateMismatchExceptions;
|
||||
|
||||
public MultipleCertificateMismatchExceptions(List<CertificateMismatch> 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);
|
||||
}
|
||||
}
|
||||
}
|
272
src/main/java/org/minidns/dane/DaneVerifier.java
Normal file
272
src/main/java/org/minidns/dane/DaneVerifier.java
Normal file
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dane;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnssec.DnssecClient;
|
||||
import org.minidns.dnssec.DnssecQueryResult;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.TLSA;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* A helper class to validate the usage of TLSA records.
|
||||
*/
|
||||
public class DaneVerifier {
|
||||
private static final Logger LOGGER = Logger.getLogger(DaneVerifier.class.getName());
|
||||
|
||||
private final DnssecClient client;
|
||||
|
||||
public DaneVerifier() {
|
||||
this(new DnssecClient());
|
||||
}
|
||||
|
||||
public DaneVerifier(DnssecClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected.
|
||||
*
|
||||
* @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE.
|
||||
* @return Whether the DANE verification is the only requirement according to the TLSA record.
|
||||
* If this method returns {@code false}, additional PKIX validation is required.
|
||||
* @throws CertificateException if the certificate chain provided differs from the one enforced using DANE.
|
||||
*/
|
||||
public boolean verify(SSLSocket socket) throws CertificateException {
|
||||
if (!socket.isConnected()) {
|
||||
throw new IllegalStateException("Socket not yet connected.");
|
||||
}
|
||||
return verify(socket.getSession());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the certificate chain in an active {@link SSLSession}.
|
||||
*
|
||||
* @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE.
|
||||
* @return Whether the DANE verification is the only requirement according to the TLSA record.
|
||||
* If this method returns {@code false}, additional PKIX validation is required.
|
||||
* @throws CertificateException if the certificate chain provided differs from the one enforced using DANE.
|
||||
*/
|
||||
public boolean verify(SSLSession session) throws CertificateException {
|
||||
try {
|
||||
return verifyCertificateChain(convert(session.getPeerCertificates()), session.getPeerHost(), session.getPeerPort());
|
||||
} catch (SSLPeerUnverifiedException e) {
|
||||
throw new CertificateException("Peer not verified", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a certificate chain to be valid when used with the given connection details using DANE.
|
||||
*
|
||||
* @param chain A certificate chain that should be verified using DANE.
|
||||
* @param hostName The DNS name of the host this certificate chain belongs to.
|
||||
* @param port The port number that was used to reach the server providing the certificate chain in question.
|
||||
* @return Whether the DANE verification is the only requirement according to the TLSA record.
|
||||
* If this method returns {@code false}, additional PKIX validation is required.
|
||||
* @throws CertificateException if the certificate chain provided differs from the one enforced using DANE.
|
||||
*/
|
||||
public boolean verifyCertificateChain(X509Certificate[] chain, String hostName, int port) throws CertificateException {
|
||||
DnsName req = DnsName.from("_" + port + "._tcp." + hostName);
|
||||
DnssecQueryResult result;
|
||||
try {
|
||||
result = client.queryDnssec(req, Record.TYPE.TLSA);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
DnsMessage res = result.dnsQueryResult.response;
|
||||
// TODO: We previously used the AD bit here. This allowed non-DNSSEC aware clients to be plugged into
|
||||
// DaneVerifier, which, in turn, allows to use a trusted forward as DNSSEC validator. Is this a good idea?
|
||||
if (!result.isAuthenticData()) {
|
||||
String msg = "Got TLSA response from DNS server, but was not signed properly.";
|
||||
msg += " Reasons:";
|
||||
for (DnssecUnverifiedReason reason : result.getUnverifiedReasons()) {
|
||||
msg += " " + reason;
|
||||
}
|
||||
LOGGER.info(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
List<DaneCertificateException.CertificateMismatch> certificateMismatchExceptions = new ArrayList<>();
|
||||
boolean verified = false;
|
||||
for (Record<? extends Data> 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<X509Certificate> certs = new ArrayList<>();
|
||||
for (Certificate certificate : certificates) {
|
||||
if (certificate instanceof X509Certificate) {
|
||||
certs.add((X509Certificate) certificate);
|
||||
}
|
||||
}
|
||||
return certs.toArray(new X509Certificate[certs.size()]);
|
||||
}
|
||||
}
|
63
src/main/java/org/minidns/dane/ExpectingTrustManager.java
Normal file
63
src/main/java/org/minidns/dane/ExpectingTrustManager.java
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dane;
|
||||
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class ExpectingTrustManager implements X509TrustManager {
|
||||
private CertificateException exception;
|
||||
private final X509TrustManager trustManager;
|
||||
|
||||
/**
|
||||
* Creates a new instance of ExpectingTrustManager.
|
||||
*
|
||||
* @param trustManager The {@link X509TrustManager} to be used for verification.
|
||||
* {@code null} to use the system default.
|
||||
*/
|
||||
public ExpectingTrustManager(X509TrustManager trustManager) {
|
||||
this.trustManager = trustManager == null ? X509TrustManagerUtil.getDefault() : trustManager;
|
||||
}
|
||||
|
||||
public boolean hasException() {
|
||||
return exception != null;
|
||||
}
|
||||
|
||||
public CertificateException getException() {
|
||||
CertificateException e = exception;
|
||||
exception = null;
|
||||
return e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
try {
|
||||
trustManager.checkClientTrusted(chain, authType);
|
||||
} catch (CertificateException e) {
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
try {
|
||||
trustManager.checkServerTrusted(chain, authType);
|
||||
} catch (CertificateException e) {
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return trustManager.getAcceptedIssuers();
|
||||
}
|
||||
}
|
44
src/main/java/org/minidns/dane/X509TrustManagerUtil.java
Normal file
44
src/main/java/org/minidns/dane/X509TrustManagerUtil.java
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dane;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
public class X509TrustManagerUtil {
|
||||
|
||||
public static X509TrustManager getDefault() {
|
||||
return getDefault(null);
|
||||
}
|
||||
|
||||
public static X509TrustManager getDefault(KeyStore keyStore) {
|
||||
String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
|
||||
TrustManagerFactory trustManagerFactory;
|
||||
try {
|
||||
trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
|
||||
trustManagerFactory.init(keyStore);
|
||||
} catch (NoSuchAlgorithmException | KeyStoreException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
|
||||
if (trustManager instanceof X509TrustManager) {
|
||||
return (X509TrustManager) trustManager;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("No trust manager for the default algorithm " + defaultAlgorithm + " found");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dane.java7;
|
||||
|
||||
import org.minidns.dane.DaneVerifier;
|
||||
import org.minidns.dane.X509TrustManagerUtil;
|
||||
import org.minidns.dnssec.DnssecClient;
|
||||
import org.minidns.util.InetAddressUtil;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class DaneExtendedTrustManager extends X509ExtendedTrustManager {
|
||||
private static final Logger LOGGER = Logger.getLogger(DaneExtendedTrustManager.class.getName());
|
||||
|
||||
private final X509TrustManager base;
|
||||
private final DaneVerifier verifier;
|
||||
|
||||
public static void inject() {
|
||||
inject(new DaneExtendedTrustManager());
|
||||
}
|
||||
|
||||
public static void inject(DaneExtendedTrustManager trustManager) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[] {trustManager}, null);
|
||||
SSLContext.setDefault(sslContext);
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public DaneExtendedTrustManager() {
|
||||
this(X509TrustManagerUtil.getDefault());
|
||||
}
|
||||
|
||||
public DaneExtendedTrustManager(DnssecClient client) {
|
||||
this(client, X509TrustManagerUtil.getDefault());
|
||||
}
|
||||
|
||||
public DaneExtendedTrustManager(X509TrustManager base) {
|
||||
this(new DaneVerifier(), base);
|
||||
}
|
||||
|
||||
public DaneExtendedTrustManager(DnssecClient client, X509TrustManager base) {
|
||||
this(new DaneVerifier(client), base);
|
||||
}
|
||||
|
||||
public DaneExtendedTrustManager(DaneVerifier verifier, X509TrustManager base) {
|
||||
this.verifier = verifier;
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
|
||||
if (base == null) {
|
||||
LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check forwarding request to fallback X509TrustManage");
|
||||
if (base instanceof X509ExtendedTrustManager) {
|
||||
((X509ExtendedTrustManager) base).checkClientTrusted(chain, authType, socket);
|
||||
} else {
|
||||
base.checkClientTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
|
||||
boolean verificationSuccessful = false;
|
||||
|
||||
if (socket instanceof SSLSocket) {
|
||||
final SSLSocket sslSocket = (SSLSocket) socket;
|
||||
final String hostname = sslSocket.getHandshakeSession().getPeerHost();
|
||||
|
||||
if (hostname == null) {
|
||||
LOGGER.warning("Hostname returned by sslSocket.getHandshakeSession().getPeerHost() is null");
|
||||
} else if (InetAddressUtil.isIpAddress(hostname)) {
|
||||
LOGGER.warning(
|
||||
"Hostname returned by sslSocket.getHandshakeSession().getPeerHost() '" + hostname
|
||||
+ "' is an IP address");
|
||||
} else {
|
||||
final int port = socket.getPort();
|
||||
verificationSuccessful = verifier.verifyCertificateChain(chain, hostname, port);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("The provided socket '" + socket + "' is not of type SSLSocket");
|
||||
}
|
||||
|
||||
if (verificationSuccessful) {
|
||||
// Verification successful, no need to delegate to base trust manager.
|
||||
return;
|
||||
}
|
||||
|
||||
if (base instanceof X509ExtendedTrustManager) {
|
||||
((X509ExtendedTrustManager) base).checkServerTrusted(chain, authType, socket);
|
||||
} else {
|
||||
base.checkServerTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
|
||||
if (base == null) {
|
||||
LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check, forwarding request to fallback X509TrustManage");
|
||||
if (base instanceof X509ExtendedTrustManager) {
|
||||
((X509ExtendedTrustManager) base).checkClientTrusted(chain, authType, engine);
|
||||
} else {
|
||||
base.checkClientTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
|
||||
if (verifier.verifyCertificateChain(chain, engine.getPeerHost(), engine.getPeerPort())) {
|
||||
// Verification successful, no need to delegate to base trust manager.
|
||||
return;
|
||||
}
|
||||
|
||||
if (base instanceof X509ExtendedTrustManager) {
|
||||
((X509ExtendedTrustManager) base).checkServerTrusted(chain, authType, engine);
|
||||
} else {
|
||||
base.checkServerTrusted(chain, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
if (base == null) {
|
||||
LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check, forwarding request to fallback X509TrustManage");
|
||||
base.checkClientTrusted(chain, authType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
LOGGER.info("DaneExtendedTrustManager cannot be used without hostname information, forwarding request to fallback X509TrustManage");
|
||||
base.checkServerTrusted(chain, authType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return base.getAcceptedIssuers();
|
||||
}
|
||||
}
|
25
src/main/java/org/minidns/dnslabel/ALabel.java
Normal file
25
src/main/java/org/minidns/dnslabel/ALabel.java
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
import org.minidns.idna.MiniDnsIdna;
|
||||
|
||||
public final class ALabel extends XnLabel {
|
||||
|
||||
ALabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInternationalizedRepresentationInternal() {
|
||||
return MiniDnsIdna.toUnicode(label);
|
||||
}
|
||||
}
|
280
src/main/java/org/minidns/dnslabel/DnsLabel.java
Normal file
280
src/main/java/org/minidns/dnslabel/DnsLabel.java
Normal file
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.minidns.util.SafeCharSequence;
|
||||
|
||||
/**
|
||||
* A DNS label is an individual component of a DNS name. Labels are usually shown separated by dots.
|
||||
* <p>
|
||||
* This class implements {@link Comparable} which compares DNS labels according to the Canonical DNS Name Order as
|
||||
* specified in <a href="https://tools.ietf.org/html/rfc4034#section-6.1">RFC 4034 § 6.1</a>.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that as per <a href="https://tools.ietf.org/html/rfc2181#section-11">RFC 2181 § 11</a> DNS labels may contain
|
||||
* any byte.
|
||||
* </p>
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5890#section-2.2">RFC 5890 § 2.2. DNS-Related Terminology</a>
|
||||
* @author Florian Schmaus
|
||||
*
|
||||
*/
|
||||
public abstract class DnsLabel extends SafeCharSequence implements Comparable<DnsLabel> {
|
||||
|
||||
/**
|
||||
* The maximum length of a DNS label in octets.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
19
src/main/java/org/minidns/dnslabel/FakeALabel.java
Normal file
19
src/main/java/org/minidns/dnslabel/FakeALabel.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
public final class FakeALabel extends XnLabel {
|
||||
|
||||
FakeALabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
}
|
81
src/main/java/org/minidns/dnslabel/LdhLabel.java
Normal file
81
src/main/java/org/minidns/dnslabel/LdhLabel.java
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A LDH (<b>L</b>etters, <b>D</b>igits, <b>H</b>yphen) label, which is the
|
||||
* classical label form.
|
||||
* <p>
|
||||
* Note that it is a common misconception that LDH labels can not start with a
|
||||
* digit. The origin of this misconception is likely that
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc1034#section-3.5">RFC 1034
|
||||
* § 3.5</a> specified
|
||||
* </p>
|
||||
* <blockquote>
|
||||
* 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.
|
||||
* </blockquote>.
|
||||
* However, this was relaxed in
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc1123#page-13">RFC 1123 §
|
||||
* 2.1</a>
|
||||
* <blockquote>
|
||||
* 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.
|
||||
* </blockquote>
|
||||
* and later summarized in
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc3696#section-2">RFC 3696 §
|
||||
* 2</a>:
|
||||
* <blockquote>
|
||||
* If the hyphen is used, it is not permitted to appear at either the beginning
|
||||
* or end of a label.
|
||||
* </blockquote>
|
||||
* Furthermore
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc5890#section-2.3.1">RFC
|
||||
* 5890 § 2.3.1</a> only mentions the requirement that hyphen must not be the
|
||||
* first or last character of a LDH label.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5890#section-2.3.1">RFC 5890 §
|
||||
* 2.3.1. LDH Label</a>
|
||||
*
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A DNS label with a leading or trailing hyphen ('-').
|
||||
*/
|
||||
public final class LeadingOrTrailingHyphenLabel extends NonLdhLabel {
|
||||
|
||||
LeadingOrTrailingHyphenLabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
static boolean isLeadingOrTrailingHypenLabelInternal(String label) {
|
||||
if (label.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (label.charAt(0) == '-') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (label.charAt(label.length() - 1) == '-') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
35
src/main/java/org/minidns/dnslabel/NonLdhLabel.java
Normal file
35
src/main/java/org/minidns/dnslabel/NonLdhLabel.java
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A DNS label which contains more than just letters, digits and a hyphen.
|
||||
*
|
||||
*/
|
||||
public abstract class NonLdhLabel extends DnsLabel {
|
||||
|
||||
protected NonLdhLabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
protected static DnsLabel fromInternal(String label) {
|
||||
if (UnderscoreLabel.isUnderscoreLabelInternal(label)) {
|
||||
return new UnderscoreLabel(label);
|
||||
}
|
||||
|
||||
if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) {
|
||||
return new LeadingOrTrailingHyphenLabel(label);
|
||||
}
|
||||
|
||||
return new OtherNonLdhLabel(label);
|
||||
}
|
||||
|
||||
}
|
34
src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java
Normal file
34
src/main/java/org/minidns/dnslabel/NonReservedLdhLabel.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A Non-Reserved LDH label (NR-LDH label), which do <em>not</em> 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);
|
||||
}
|
||||
}
|
23
src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java
Normal file
23
src/main/java/org/minidns/dnslabel/OtherNonLdhLabel.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A Non-LDH label which does <em>not</em> begin with an underscore ('_'), hyphen ('-') or ends with an hyphen.
|
||||
*
|
||||
*/
|
||||
public final class OtherNonLdhLabel extends NonLdhLabel {
|
||||
|
||||
OtherNonLdhLabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
}
|
36
src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java
Normal file
36
src/main/java/org/minidns/dnslabel/ReservedLdhLabel.java
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A reserved LDH label (R-LDH label), which have the property that they contain "--" in the third and fourth characters.
|
||||
*
|
||||
*/
|
||||
public class ReservedLdhLabel extends LdhLabel {
|
||||
|
||||
protected ReservedLdhLabel(String label) {
|
||||
super(label);
|
||||
assert isReservedLdhLabelInternal(label);
|
||||
}
|
||||
|
||||
public static boolean isReservedLdhLabel(String label) {
|
||||
if (!isLdhLabel(label)) {
|
||||
return false;
|
||||
}
|
||||
return isReservedLdhLabelInternal(label);
|
||||
}
|
||||
|
||||
static boolean isReservedLdhLabelInternal(String label) {
|
||||
return label.length() >= 4
|
||||
&& label.charAt(2) == '-'
|
||||
&& label.charAt(3) == '-';
|
||||
}
|
||||
}
|
26
src/main/java/org/minidns/dnslabel/UnderscoreLabel.java
Normal file
26
src/main/java/org/minidns/dnslabel/UnderscoreLabel.java
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
/**
|
||||
* A DNS label which begins with an underscore ('_').
|
||||
*
|
||||
*/
|
||||
public final class UnderscoreLabel extends NonLdhLabel {
|
||||
|
||||
UnderscoreLabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
static boolean isUnderscoreLabelInternal(String label) {
|
||||
return label.charAt(0) == '_';
|
||||
}
|
||||
}
|
50
src/main/java/org/minidns/dnslabel/XnLabel.java
Normal file
50
src/main/java/org/minidns/dnslabel/XnLabel.java
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnslabel;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.minidns.idna.MiniDnsIdna;
|
||||
|
||||
/**
|
||||
* A label that begins with "xn--" and follows the LDH rule.
|
||||
*/
|
||||
public abstract class XnLabel extends ReservedLdhLabel {
|
||||
|
||||
protected XnLabel(String label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
protected static LdhLabel fromInternal(String label) {
|
||||
assert isIdnAcePrefixed(label);
|
||||
|
||||
String uLabel = MiniDnsIdna.toUnicode(label);
|
||||
if (label.equals(uLabel)) {
|
||||
// No Punycode conversation to Unicode was performed, this is a fake A-label!
|
||||
return new FakeALabel(label);
|
||||
} else {
|
||||
return new ALabel(label);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isXnLabel(String label) {
|
||||
if (!isReservedLdhLabel(label)) {
|
||||
return false;
|
||||
}
|
||||
return isXnLabelInternal(label);
|
||||
}
|
||||
|
||||
static boolean isXnLabelInternal(String label) {
|
||||
// Note that we already ensure the minimum label length here, since reserved LDH
|
||||
// labels must start with "xn--".
|
||||
return label.substring(0, 2).toLowerCase(Locale.US).equals("xn");
|
||||
}
|
||||
}
|
1281
src/main/java/org/minidns/dnsmessage/DnsMessage.java
Normal file
1281
src/main/java/org/minidns/dnsmessage/DnsMessage.java
Normal file
File diff suppressed because it is too large
Load diff
180
src/main/java/org/minidns/dnsmessage/Question.java
Normal file
180
src/main/java/org/minidns/dnsmessage/Question.java
Normal file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsmessage;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.record.Record.CLASS;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
/**
|
||||
* A DNS question (request).
|
||||
*/
|
||||
public class Question {
|
||||
|
||||
/**
|
||||
* The question string (e.g. "measite.de").
|
||||
*/
|
||||
public final DnsName name;
|
||||
|
||||
/**
|
||||
* The question type (e.g. A).
|
||||
*/
|
||||
public final TYPE type;
|
||||
|
||||
/**
|
||||
* The question class (usually IN for Internet).
|
||||
*/
|
||||
public final CLASS clazz;
|
||||
|
||||
/**
|
||||
* UnicastQueries have the highest bit of the CLASS field set to 1.
|
||||
*/
|
||||
private final boolean unicastQuery;
|
||||
|
||||
/**
|
||||
* Cache for the serialized object.
|
||||
*/
|
||||
private byte[] byteArray;
|
||||
|
||||
/**
|
||||
* Create a dns question for the given name/type/class.
|
||||
* @param name The name e.g. "measite.de".
|
||||
* @param type The type, e.g. A.
|
||||
* @param clazz The class, usually IN (internet).
|
||||
* @param unicastQuery True if this is a unicast query.
|
||||
*/
|
||||
public Question(CharSequence name, TYPE type, CLASS clazz, boolean unicastQuery) {
|
||||
this(DnsName.from(name), type, clazz, unicastQuery);
|
||||
}
|
||||
|
||||
public Question(DnsName name, TYPE type, CLASS clazz, boolean unicastQuery) {
|
||||
assert name != null;
|
||||
assert type != null;
|
||||
assert clazz != null;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.clazz = clazz;
|
||||
this.unicastQuery = unicastQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dns question for the given name/type/class.
|
||||
* @param name The name e.g. "measite.de".
|
||||
* @param type The type, e.g. A.
|
||||
* @param clazz The class, usually IN (internet).
|
||||
*/
|
||||
public Question(DnsName name, TYPE type, CLASS clazz) {
|
||||
this(name, type, clazz, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dns question for the given name/type/IN (internet class).
|
||||
* @param name The name e.g. "measite.de".
|
||||
* @param type The type, e.g. A.
|
||||
*/
|
||||
public Question(DnsName name, TYPE type) {
|
||||
this(name, type, CLASS.IN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dns question for the given name/type/class.
|
||||
* @param name The name e.g. "measite.de".
|
||||
* @param type The type, e.g. A.
|
||||
* @param clazz The class, usually IN (internet).
|
||||
*/
|
||||
public Question(CharSequence name, TYPE type, CLASS clazz) {
|
||||
this(DnsName.from(name), type, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dns question for the given name/type/IN (internet class).
|
||||
* @param name The name e.g. "measite.de".
|
||||
* @param type The type, e.g. A.
|
||||
*/
|
||||
public Question(CharSequence name, TYPE type) {
|
||||
this(DnsName.from(name), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a byte array and rebuild the dns question from it.
|
||||
* @param dis The input stream.
|
||||
* @param data The plain data (for dns name references).
|
||||
* @throws IOException On errors (read outside of packet).
|
||||
*/
|
||||
public Question(DataInputStream dis, byte[] data) throws IOException {
|
||||
name = DnsName.parse(dis, data);
|
||||
type = TYPE.getType(dis.readUnsignedShort());
|
||||
clazz = CLASS.getClass(dis.readUnsignedShort());
|
||||
unicastQuery = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a binary paket for this dns question.
|
||||
* @return The dns question.
|
||||
*/
|
||||
public byte[] toByteArray() {
|
||||
if (byteArray == null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
|
||||
DataOutputStream dos = new DataOutputStream(baos);
|
||||
|
||||
try {
|
||||
name.writeToStream(dos);
|
||||
dos.writeShort(type.getValue());
|
||||
dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0));
|
||||
dos.flush();
|
||||
} catch (IOException e) {
|
||||
// Should never happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byteArray = baos.toByteArray();
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof Question)) {
|
||||
return false;
|
||||
}
|
||||
byte[] t = toByteArray();
|
||||
byte[] o = ((Question) other).toByteArray();
|
||||
return Arrays.equals(t, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name.getRawAce() + ".\t" + clazz + '\t' + type;
|
||||
}
|
||||
|
||||
public DnsMessage.Builder asMessageBuilder() {
|
||||
DnsMessage.Builder builder = DnsMessage.builder();
|
||||
builder.setQuestion(this);
|
||||
return builder;
|
||||
}
|
||||
|
||||
public DnsMessage asQueryMessage() {
|
||||
return asMessageBuilder().build();
|
||||
}
|
||||
}
|
646
src/main/java/org/minidns/dnsname/DnsName.java
Normal file
646
src/main/java/org/minidns/dnsname/DnsName.java
Normal file
|
@ -0,0 +1,646 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsname;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
import org.minidns.idna.MiniDnsIdna;
|
||||
import org.minidns.util.SafeCharSequence;
|
||||
|
||||
/**
|
||||
* A DNS name, also called "domain name". A DNS name consists of multiple 'labels' (see {@link DnsLabel}) and is subject to certain restrictions (see
|
||||
* for example <a href="https://tools.ietf.org/html/rfc3696#section-2">RFC 3696 § 2.</a>).
|
||||
* <p>
|
||||
* Instances of this class can be created by using {@link #from(String)}.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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()}.
|
||||
* </p>
|
||||
* More information about Internationalized Domain Names can be found at:
|
||||
* <ul>
|
||||
* <li><a href="https://unicode.org/reports/tr46/">UTS #46 - Unicode IDNA Compatibility Processing</a>
|
||||
* <li><a href="https://tools.ietf.org/html/rfc8753">RFC 8753 - Internationalized Domain Names for Applications (IDNA) Review for New Unicode Versions</a>
|
||||
* </ul>
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc3696">RFC 3696</a>
|
||||
* @see DnsLabel
|
||||
* @author Florian Schmaus
|
||||
*
|
||||
*/
|
||||
public final class DnsName extends SafeCharSequence implements Serializable, Comparable<DnsName> {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @see <a href="https://www.ietf.org/rfc/rfc3490.txt">RFC 3490 § 3.1 1.</a>
|
||||
*/
|
||||
private static final String LABEL_SEP_REGEX = "[.\u3002\uFF0E\uFF61]";
|
||||
|
||||
/**
|
||||
* See <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a>
|
||||
*/
|
||||
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 <b>reverse</b> 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.
|
||||
*
|
||||
* <b>Please refer to {@link #getAce()} for a discussion of the security
|
||||
* implications when working with the ACE representation of a DNS name.</b>
|
||||
*
|
||||
* @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.
|
||||
* <p>
|
||||
* 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".
|
||||
* </p>
|
||||
*
|
||||
* @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<Integer> jumps = new HashSet<Integer>();
|
||||
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<Integer> 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).
|
||||
* <p>
|
||||
* For example:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><code>"foo.bar.org".getParent() == "bar.org"</code></li>
|
||||
* <li><code> ".".getParent() == "."</code></li>
|
||||
* </ul>
|
||||
* @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(".");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsname;
|
||||
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
|
||||
public abstract class InvalidDnsNameException extends IllegalStateException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected final String ace;
|
||||
|
||||
protected InvalidDnsNameException(String ace) {
|
||||
this.ace = ace;
|
||||
}
|
||||
|
||||
public static class LabelTooLongException extends InvalidDnsNameException {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String label;
|
||||
|
||||
public LabelTooLongException(String ace, String label) {
|
||||
super(ace);
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "The DNS name '" + ace + "' contains the label '" + label
|
||||
+ "' which exceeds the maximum label length of " + DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS + " octets by "
|
||||
+ (label.length() - DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS) + " octets.";
|
||||
}
|
||||
}
|
||||
|
||||
public static class DNSNameTooLongException extends InvalidDnsNameException {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
public DNSNameTooLongException(String ace, byte[] bytes) {
|
||||
super(ace);
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "The DNS name '" + ace + "' exceeds the maximum name length of "
|
||||
+ DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS + " octets by "
|
||||
+ (bytes.length - DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS) + " octets.";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsqueryresult;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
|
||||
public abstract class CachedDnsQueryResult extends DnsQueryResult {
|
||||
|
||||
protected final DnsQueryResult cachedDnsQueryResult;
|
||||
|
||||
protected CachedDnsQueryResult(DnsMessage query, DnsQueryResult cachedDnsQueryResult) {
|
||||
super(QueryMethod.cachedDirect, query, cachedDnsQueryResult.response);
|
||||
this.cachedDnsQueryResult = cachedDnsQueryResult;
|
||||
}
|
||||
|
||||
protected CachedDnsQueryResult(DnsMessage query, DnsMessage response, DnsQueryResult synthesynthesizationSource) {
|
||||
super(QueryMethod.cachedSynthesized, query, response);
|
||||
this.cachedDnsQueryResult = synthesynthesizationSource;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsqueryresult;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
|
||||
public class DirectCachedDnsQueryResult extends CachedDnsQueryResult {
|
||||
|
||||
public DirectCachedDnsQueryResult(DnsMessage query, DnsQueryResult cachedDnsQueryResult) {
|
||||
super(query, cachedDnsQueryResult);
|
||||
}
|
||||
|
||||
}
|
52
src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java
Normal file
52
src/main/java/org/minidns/dnsqueryresult/DnsQueryResult.java
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsqueryresult;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
|
||||
|
||||
public abstract class DnsQueryResult {
|
||||
|
||||
public enum QueryMethod {
|
||||
udp,
|
||||
tcp,
|
||||
asyncUdp,
|
||||
asyncTcp,
|
||||
cachedDirect,
|
||||
cachedSynthesized,
|
||||
testWorld,
|
||||
}
|
||||
|
||||
public final QueryMethod queryMethod;
|
||||
|
||||
public final DnsMessage query;
|
||||
|
||||
public final DnsMessage response;
|
||||
|
||||
protected DnsQueryResult(QueryMethod queryMethod, DnsMessage query, DnsMessage response) {
|
||||
assert queryMethod != null;
|
||||
assert query != null;
|
||||
assert response != null;
|
||||
|
||||
this.queryMethod = queryMethod;
|
||||
this.query = query;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return response.toString();
|
||||
}
|
||||
|
||||
public boolean wasSuccessful() {
|
||||
return response.responseCode == RESPONSE_CODE.NO_ERROR;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsqueryresult;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
|
||||
public class StandardDnsQueryResult extends DnsQueryResult {
|
||||
|
||||
public final InetAddress serverAddress;
|
||||
|
||||
public final int port;
|
||||
|
||||
public StandardDnsQueryResult(InetAddress serverAddress, int port, QueryMethod queryMethod, DnsMessage query, DnsMessage responseDnsMessage) {
|
||||
super(queryMethod, query, responseDnsMessage);
|
||||
this.serverAddress = serverAddress;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsqueryresult;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
|
||||
public class SynthesizedCachedDnsQueryResult extends CachedDnsQueryResult {
|
||||
|
||||
public SynthesizedCachedDnsQueryResult(DnsMessage query, DnsMessage response, DnsQueryResult synthesynthesizationSource) {
|
||||
super(query, response, synthesynthesizationSource);
|
||||
}
|
||||
|
||||
}
|
15
src/main/java/org/minidns/dnssec/DigestCalculator.java
Normal file
15
src/main/java/org/minidns/dnssec/DigestCalculator.java
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
public interface DigestCalculator {
|
||||
byte[] digest(byte[] bytes);
|
||||
}
|
573
src/main/java/org/minidns/dnssec/DnssecClient.java
Normal file
573
src/main/java/org/minidns/dnssec/DnssecClient.java
Normal file
|
@ -0,0 +1,573 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import org.minidns.DnsCache;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.NoActiveSignaturesReason;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.NoSecureEntryPointReason;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.NoSignaturesReason;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.NoTrustAnchorReason;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.AuthorityDoesNotContainSoa;
|
||||
import org.minidns.iterative.ReliableDnsClient;
|
||||
import org.minidns.record.DLV;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.DS;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.DelegatingDnssecRR;
|
||||
import org.minidns.record.NSEC;
|
||||
import org.minidns.record.NSEC3;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.Record.CLASS;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class DnssecClient extends ReliableDnsClient {
|
||||
|
||||
/**
|
||||
* The root zone's KSK.
|
||||
* The ID of the current key is "Klajeyz", and the key tag value is "20326".
|
||||
*/
|
||||
private static final BigInteger rootEntryKey = new BigInteger("1628686155461064465348252249725010996177649738666492500572664444461532807739744536029771810659241049343994038053541290419968870563183856865780916376571550372513476957870843322273120879361960335192976656756972171258658400305760429696147778001233984421619267530978084631948434496468785021389956803104620471232008587410372348519229650742022804219634190734272506220018657920136902014393834092648785514548876370028925405557661759399901378816916683122474038734912535425670533237815676134840739565610963796427401855723026687073600445461090736240030247906095053875491225879656640052743394090544036297390104110989318819106653199917493");
|
||||
|
||||
private static final DnsName DEFAULT_DLV = DnsName.from("dlv.isc.org");
|
||||
|
||||
/**
|
||||
* Create a new DNSSEC aware DNS client using the global default cache.
|
||||
*/
|
||||
public DnssecClient() {
|
||||
this(DEFAULT_CACHE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DNSSEC aware DNS client with the given DNS cache.
|
||||
*
|
||||
* @param cache The backend DNS cache.
|
||||
*/
|
||||
public DnssecClient(DnsCache cache) {
|
||||
super(cache);
|
||||
addSecureEntryPoint(DnsName.ROOT, rootEntryKey.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Known secure entry points (SEPs).
|
||||
*/
|
||||
private final Map<DnsName, byte[]> knownSeps = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean stripSignatureRecords = true;
|
||||
|
||||
/**
|
||||
* The active DNSSEC Look-aside Validation Registry. May be <code>null</code>.
|
||||
*/
|
||||
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<DnssecUnverifiedReason> unverifiedReasons = verify(dnsMessage);
|
||||
|
||||
messageBuilder.setAuthenticData(unverifiedReasons.isEmpty());
|
||||
|
||||
List<Record<? extends Data>> answers = dnsMessage.answerSection;
|
||||
List<Record<? extends Data>> nameserverRecords = dnsMessage.authoritySection;
|
||||
List<Record<? extends Data>> additionalResourceRecords = dnsMessage.additionalSection;
|
||||
Set<Record<RRSIG>> 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<Record<? extends Data>> stripSignatureRecords(List<Record<? extends Data>> records) {
|
||||
if (records.isEmpty()) return records;
|
||||
List<Record<? extends Data>> recordList = new ArrayList<>(records.size());
|
||||
for (Record<? extends Data> record : records) {
|
||||
if (record.type != TYPE.RRSIG) {
|
||||
recordList.add(record);
|
||||
}
|
||||
}
|
||||
return recordList;
|
||||
}
|
||||
|
||||
private Set<DnssecUnverifiedReason> verify(DnsMessage dnsMessage) throws IOException {
|
||||
if (!dnsMessage.answerSection.isEmpty()) {
|
||||
return verifyAnswer(dnsMessage);
|
||||
} else {
|
||||
return verifyNsec(dnsMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<DnssecUnverifiedReason> verifyAnswer(DnsMessage dnsMessage) throws IOException {
|
||||
Question q = dnsMessage.questions.get(0);
|
||||
List<Record<? extends Data>> answers = dnsMessage.answerSection;
|
||||
List<Record<? extends Data>> toBeVerified = dnsMessage.copyAnswers();
|
||||
VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified);
|
||||
Set<DnssecUnverifiedReason> result = verifiedSignatures.reasons;
|
||||
if (!result.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep SEPs separated, we only need one valid SEP.
|
||||
boolean sepSignatureValid = false;
|
||||
Set<DnssecUnverifiedReason> sepReasons = new HashSet<>();
|
||||
for (Iterator<Record<? extends Data>> iterator = toBeVerified.iterator(); iterator.hasNext(); ) {
|
||||
Record<DNSKEY> 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<DnssecUnverifiedReason> 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<DnssecUnverifiedReason> verifyNsec(DnsMessage dnsMessage) throws IOException {
|
||||
Set<DnssecUnverifiedReason> 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<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
|
||||
for (Record<? extends Data> 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<? extends Data> record : authoritySection) {
|
||||
DnssecUnverifiedReason reason;
|
||||
|
||||
switch (record.type) {
|
||||
case NSEC:
|
||||
nsecPresent = true;
|
||||
Record<NSEC> nsecRecord = record.as(NSEC.class);
|
||||
reason = Verifier.verifyNsec(nsecRecord, q);
|
||||
break;
|
||||
case NSEC3:
|
||||
nsecPresent = true;
|
||||
Record<NSEC3> 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<Record<? extends Data>> 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<DnssecUnverifiedReason> reasons = new HashSet<>();
|
||||
}
|
||||
|
||||
@SuppressWarnings("JavaUtilDate")
|
||||
private VerifySignaturesResult verifySignatures(Question q, Collection<Record<? extends Data>> reference, List<Record<? extends Data>> toBeVerified) throws IOException {
|
||||
final Date now = new Date();
|
||||
final List<RRSIG> outdatedRrSigs = new ArrayList<>();
|
||||
VerifySignaturesResult result = new VerifySignaturesResult();
|
||||
final List<Record<RRSIG>> rrsigs = new ArrayList<>(toBeVerified.size());
|
||||
|
||||
for (Record<? extends Data> recordToBeVerified : toBeVerified) {
|
||||
Record<RRSIG> 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<RRSIG> sigRecord : rrsigs) {
|
||||
RRSIG rrsig = sigRecord.payloadData;
|
||||
|
||||
List<Record<? extends Data>> records = new ArrayList<>(reference.size());
|
||||
for (Record<? extends Data> record : reference) {
|
||||
if (record.type == rrsig.typeCovered && record.name.equals(sigRecord.name)) {
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
Set<DnssecUnverifiedReason> reasons = verifySignedRecords(q, rrsig, records);
|
||||
result.reasons.addAll(reasons);
|
||||
|
||||
if (q.name.equals(rrsig.signerName) && rrsig.typeCovered == TYPE.DNSKEY) {
|
||||
for (Iterator<Record<? extends Data>> iterator = records.iterator(); iterator.hasNext(); ) {
|
||||
Record<DNSKEY> 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<DnssecUnverifiedReason> verifySignedRecords(Question q, RRSIG rrsig, List<Record<? extends Data>> records) throws IOException {
|
||||
Set<DnssecUnverifiedReason> result = new HashSet<>();
|
||||
DNSKEY dnskey = null;
|
||||
|
||||
if (rrsig.typeCovered == TYPE.DNSKEY) {
|
||||
// Key must be present
|
||||
List<Record<DNSKEY>> dnskeyRrs = Record.filter(DNSKEY.class, records);
|
||||
for (Record<DNSKEY> 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<Record<DNSKEY>> dnskeyRrs = dnskeyRes.dnsQueryResult.response.filterAnswerSectionBy(DNSKEY.class);
|
||||
for (Record<DNSKEY> 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<DnssecUnverifiedReason> verifySecureEntryPoint(final Record<DNSKEY> sepRecord) throws IOException {
|
||||
final DNSKEY dnskey = sepRecord.payloadData;
|
||||
|
||||
Set<DnssecUnverifiedReason> unverifiedReasons = new HashSet<>();
|
||||
Set<DnssecUnverifiedReason> 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<Record<DS>> dsRrs = dsResp.dnsQueryResult.response.filterAnswerSectionBy(DS.class);
|
||||
for (Record<DS> 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<Record<DLV>> dlvRrs = dlvResp.dnsQueryResult.response.filterAnswerSectionBy(DLV.class);
|
||||
for (Record<DLV> 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;
|
||||
}
|
||||
}
|
53
src/main/java/org/minidns/dnssec/DnssecQueryResult.java
Normal file
53
src/main/java/org/minidns/dnssec/DnssecQueryResult.java
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.record.Record;
|
||||
|
||||
public class DnssecQueryResult {
|
||||
|
||||
public final DnsMessage synthesizedResponse;
|
||||
public final DnsQueryResult dnsQueryResult;
|
||||
|
||||
private final Set<Record<RRSIG>> signatures;
|
||||
private final Set<DnssecUnverifiedReason> dnssecUnverifiedReasons;
|
||||
|
||||
DnssecQueryResult(DnsMessage synthesizedResponse, DnsQueryResult dnsQueryResult, Set<Record<RRSIG>> signatures,
|
||||
Set<DnssecUnverifiedReason> 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<Record<RRSIG>> getSignatures() {
|
||||
return signatures;
|
||||
}
|
||||
|
||||
public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
|
||||
return dnssecUnverifiedReasons;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.MiniDnsException;
|
||||
|
||||
public final class DnssecResultNotAuthenticException extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Set<DnssecUnverifiedReason> unverifiedReasons;
|
||||
|
||||
private DnssecResultNotAuthenticException(String message, Set<DnssecUnverifiedReason> unverifiedReasons) {
|
||||
super(message);
|
||||
if (unverifiedReasons.isEmpty()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons);
|
||||
}
|
||||
|
||||
public static DnssecResultNotAuthenticException from(Set<DnssecUnverifiedReason> 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<DnssecUnverifiedReason> getUnverifiedReasons() {
|
||||
return unverifiedReasons;
|
||||
}
|
||||
}
|
175
src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java
Normal file
175
src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.minidns.constants.DnssecConstants.DigestAlgorithm;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
public abstract class DnssecUnverifiedReason {
|
||||
public abstract String getReasonString();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getReasonString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getReasonString().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof DnssecUnverifiedReason && ((DnssecUnverifiedReason) obj).getReasonString().equals(getReasonString());
|
||||
}
|
||||
|
||||
public static class AlgorithmNotSupportedReason extends DnssecUnverifiedReason {
|
||||
private final String algorithm;
|
||||
private final TYPE type;
|
||||
private final Record<? extends Data> record;
|
||||
|
||||
public AlgorithmNotSupportedReason(byte algorithm, TYPE type, Record<? extends Data> 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<? extends Data> record;
|
||||
|
||||
public AlgorithmExceptionThrownReason(DigestAlgorithm algorithm, String kind, Record<? extends Data> 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<DNSKEY> record;
|
||||
|
||||
public ConflictsWithSep(Record<DNSKEY> 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<RRSIG> outdatedRrSigs;
|
||||
|
||||
public NoActiveSignaturesReason(Question question, List<RRSIG> 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<RRSIG> getOutdatedRrSigs() {
|
||||
return outdatedRrSigs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NSECDoesNotMatchReason extends DnssecUnverifiedReason {
|
||||
private final Question question;
|
||||
private final Record<? extends Data> record;
|
||||
|
||||
public NSECDoesNotMatchReason(Question question, Record<? extends Data> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.DelegatingDnssecRR;
|
||||
import org.minidns.record.Record;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class DnssecValidationFailedException extends IOException {
|
||||
private static final long serialVersionUID = 5413184667629832742L;
|
||||
|
||||
public DnssecValidationFailedException(Question question, String reason) {
|
||||
super("Validation of request to " + question + " failed: " + reason);
|
||||
}
|
||||
|
||||
public DnssecValidationFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DnssecValidationFailedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public DnssecValidationFailedException(Record<? extends Data> record, String reason) {
|
||||
super("Validation of record " + record + " failed: " + reason);
|
||||
}
|
||||
|
||||
public DnssecValidationFailedException(List<Record<? extends Data>> 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<? extends Data> record;
|
||||
private final DelegatingDnssecRR ds;
|
||||
private final byte[] digest;
|
||||
private final String digestHex;
|
||||
|
||||
private DigestComparisonFailedException(String message, Record<? extends Data> record, DelegatingDnssecRR ds, byte[] digest, String digestHex) {
|
||||
super(message);
|
||||
this.record = record;
|
||||
this.ds = ds;
|
||||
this.digest = digest;
|
||||
this.digestHex = digestHex;
|
||||
}
|
||||
|
||||
public Record<? extends Data> getRecord() {
|
||||
return record;
|
||||
}
|
||||
|
||||
public DelegatingDnssecRR getDelegaticDnssecRr() {
|
||||
return ds;
|
||||
}
|
||||
|
||||
public byte[] getDigest() {
|
||||
return digest.clone();
|
||||
}
|
||||
|
||||
public String getDigestHex() {
|
||||
return digestHex;
|
||||
}
|
||||
|
||||
public static DigestComparisonFailedException from(Record<? extends Data> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
public class DnssecValidatorInitializationException extends RuntimeException {
|
||||
private static final long serialVersionUID = -1464257268053507791L;
|
||||
|
||||
public DnssecValidatorInitializationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
18
src/main/java/org/minidns/dnssec/SignatureVerifier.java
Normal file
18
src/main/java/org/minidns/dnssec/SignatureVerifier.java
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.RRSIG;
|
||||
|
||||
public interface SignatureVerifier {
|
||||
boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException;
|
||||
}
|
219
src/main/java/org/minidns/dnssec/Verifier.java
Normal file
219
src/main/java/org/minidns/dnssec/Verifier.java
Normal file
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec;
|
||||
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmExceptionThrownReason;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmNotSupportedReason;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason.NSECDoesNotMatchReason;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DigestComparisonFailedException;
|
||||
import org.minidns.dnssec.algorithms.AlgorithmMap;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.DelegatingDnssecRR;
|
||||
import org.minidns.record.NSEC;
|
||||
import org.minidns.record.NSEC3;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.util.Base32;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
class Verifier {
|
||||
private static final AlgorithmMap algorithmMap = AlgorithmMap.INSTANCE;
|
||||
|
||||
public static DnssecUnverifiedReason verify(Record<DNSKEY> 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<Record<? extends Data>> 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<NSEC> 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<NSEC3> 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<Record<? extends Data>> 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<byte[]> recordBytes = new ArrayList<>(records.size());
|
||||
for (Record<? extends Data> record : records) {
|
||||
Record<Data> ref = new Record<Data>(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<byte[]>() {
|
||||
@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;
|
||||
}
|
||||
}
|
122
src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java
Normal file
122
src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.constants.DnssecConstants.DigestAlgorithm;
|
||||
import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
|
||||
import org.minidns.dnssec.DnssecValidatorInitializationException;
|
||||
import org.minidns.dnssec.DigestCalculator;
|
||||
import org.minidns.dnssec.SignatureVerifier;
|
||||
import org.minidns.record.NSEC3.HashAlgorithm;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class AlgorithmMap {
|
||||
private Logger LOGGER = Logger.getLogger(AlgorithmMap.class.getName());
|
||||
|
||||
public static final AlgorithmMap INSTANCE = new AlgorithmMap();
|
||||
|
||||
private final Map<DigestAlgorithm, DigestCalculator> dsDigestMap = new HashMap<>();
|
||||
private final Map<SignatureAlgorithm, SignatureVerifier> signatureMap = new HashMap<>();
|
||||
private final Map<HashAlgorithm, DigestCalculator> 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.DSAPublicKeySpec;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
class DsaSignatureVerifier extends JavaSecSignatureVerifier {
|
||||
private static final int LENGTH = 20;
|
||||
|
||||
DsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException {
|
||||
super("DSA", algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException {
|
||||
DataInput dis = rrsig.getSignatureAsDataInputStream();
|
||||
|
||||
ByteArrayOutputStream bos;
|
||||
try {
|
||||
// Convert RFC 2536 to ASN.1
|
||||
@SuppressWarnings("unused")
|
||||
byte t = dis.readByte();
|
||||
|
||||
byte[] r = new byte[LENGTH];
|
||||
dis.readFully(r);
|
||||
int roff = 0;
|
||||
final int rlen;
|
||||
if (r[0] == 0) {
|
||||
while (roff < LENGTH && r[roff] == 0) {
|
||||
roff++;
|
||||
}
|
||||
rlen = r.length - roff;
|
||||
} else if (r[0] < 0) {
|
||||
rlen = r.length + 1;
|
||||
} else {
|
||||
rlen = r.length;
|
||||
}
|
||||
|
||||
byte[] s = new byte[LENGTH];
|
||||
dis.readFully(s);
|
||||
int soff = 0;
|
||||
final int slen;
|
||||
if (s[0] == 0) {
|
||||
while (soff < LENGTH && s[soff] == 0) {
|
||||
soff++;
|
||||
}
|
||||
slen = s.length - soff;
|
||||
} else if (s[0] < 0) {
|
||||
slen = s.length + 1;
|
||||
} else {
|
||||
slen = s.length;
|
||||
}
|
||||
|
||||
bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
|
||||
dos.writeByte(0x30);
|
||||
dos.writeByte(rlen + slen + 4);
|
||||
|
||||
dos.writeByte(0x2);
|
||||
dos.writeByte(rlen);
|
||||
if (rlen > LENGTH)
|
||||
dos.writeByte(0);
|
||||
dos.write(r, roff, LENGTH - roff);
|
||||
|
||||
dos.writeByte(0x2);
|
||||
dos.writeByte(slen);
|
||||
if (slen > LENGTH)
|
||||
dos.writeByte(0);
|
||||
dos.write(s, soff, LENGTH - soff);
|
||||
} catch (IOException e) {
|
||||
throw new DataMalformedException(e, rrsig.getSignature());
|
||||
}
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
|
||||
DataInput dis = key.getKeyAsDataInputStream();
|
||||
BigInteger subPrime, prime, base, pubKey;
|
||||
|
||||
try {
|
||||
int t = dis.readUnsignedByte();
|
||||
|
||||
byte[] subPrimeBytes = new byte[LENGTH];
|
||||
dis.readFully(subPrimeBytes);
|
||||
subPrime = new BigInteger(1, subPrimeBytes);
|
||||
|
||||
byte[] primeBytes = new byte[64 + t * 8];
|
||||
dis.readFully(primeBytes);
|
||||
prime = new BigInteger(1, primeBytes);
|
||||
|
||||
byte[] baseBytes = new byte[64 + t * 8];
|
||||
dis.readFully(baseBytes);
|
||||
base = new BigInteger(1, baseBytes);
|
||||
|
||||
byte[] pubKeyBytes = new byte[64 + t * 8];
|
||||
dis.readFully(pubKeyBytes);
|
||||
pubKey = new BigInteger(1, pubKeyBytes);
|
||||
} catch (IOException e) {
|
||||
throw new DataMalformedException(e, key.getKey());
|
||||
}
|
||||
|
||||
try {
|
||||
return getKeyFactory().generatePublic(new DSAPublicKeySpec(pubKey, prime, subPrime, base));
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new DnssecInvalidKeySpecException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.RRSIG;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.ECFieldFp;
|
||||
import java.security.spec.ECParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.ECPublicKeySpec;
|
||||
import java.security.spec.EllipticCurve;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
abstract class EcdsaSignatureVerifier extends JavaSecSignatureVerifier {
|
||||
private final ECParameterSpec spec;
|
||||
private final int length;
|
||||
|
||||
EcdsaSignatureVerifier(BigInteger[] spec, int length, String algorithm) throws NoSuchAlgorithmException {
|
||||
this(new ECParameterSpec(new EllipticCurve(new ECFieldFp(spec[0]), spec[1], spec[2]), new ECPoint(spec[3], spec[4]), spec[5], 1), length, algorithm);
|
||||
}
|
||||
|
||||
EcdsaSignatureVerifier(ECParameterSpec spec, int length, String algorithm) throws NoSuchAlgorithmException {
|
||||
super("EC", algorithm);
|
||||
this.length = length;
|
||||
this.spec = spec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException {
|
||||
DataInput dis = rrsig.getSignatureAsDataInputStream();
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
|
||||
try {
|
||||
byte[] r = new byte[length];
|
||||
dis.readFully(r);
|
||||
int rlen = (r[0] < 0) ? length + 1 : length;
|
||||
|
||||
byte[] s = new byte[length];
|
||||
dis.readFully(s);
|
||||
int slen = (s[0] < 0) ? length + 1 : length;
|
||||
|
||||
dos.writeByte(0x30);
|
||||
dos.writeByte(rlen + slen + 4);
|
||||
|
||||
dos.writeByte(0x2);
|
||||
dos.writeByte(rlen);
|
||||
if (rlen > length) dos.writeByte(0);
|
||||
dos.write(r);
|
||||
|
||||
dos.writeByte(0x2);
|
||||
dos.writeByte(slen);
|
||||
if (slen > length) dos.writeByte(0);
|
||||
dos.write(s);
|
||||
} catch (IOException e) {
|
||||
throw new DataMalformedException(e, rrsig.getSignature());
|
||||
}
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
|
||||
DataInput dis = key.getKeyAsDataInputStream();
|
||||
BigInteger x, y;
|
||||
|
||||
try {
|
||||
byte[] xBytes = new byte[length];
|
||||
dis.readFully(xBytes);
|
||||
x = new BigInteger(1, xBytes);
|
||||
|
||||
byte[] yBytes = new byte[length];
|
||||
dis.readFully(yBytes);
|
||||
y = new BigInteger(1, yBytes);
|
||||
} catch (IOException e) {
|
||||
throw new DataMalformedException(e, key.getKey());
|
||||
}
|
||||
|
||||
try {
|
||||
return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), spec));
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new DnssecInvalidKeySpecException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class P256SHA256 extends EcdsaSignatureVerifier {
|
||||
private static BigInteger[] SPEC = {
|
||||
new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16),
|
||||
new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16),
|
||||
new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16),
|
||||
new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16),
|
||||
new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16),
|
||||
new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16)
|
||||
};
|
||||
|
||||
P256SHA256() throws NoSuchAlgorithmException {
|
||||
super(SPEC, 32, "SHA256withECDSA");
|
||||
}
|
||||
}
|
||||
|
||||
public static class P384SHA284 extends EcdsaSignatureVerifier {
|
||||
private static BigInteger[] SPEC = {
|
||||
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16),
|
||||
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16),
|
||||
new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16),
|
||||
new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16),
|
||||
new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16),
|
||||
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16)
|
||||
};
|
||||
|
||||
P384SHA284() throws NoSuchAlgorithmException {
|
||||
super(SPEC, 48, "SHA384withECDSA");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.ECFieldFp;
|
||||
import java.security.spec.ECParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.ECPublicKeySpec;
|
||||
import java.security.spec.EllipticCurve;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
class EcgostSignatureVerifier extends JavaSecSignatureVerifier {
|
||||
private static final int LENGTH = 32;
|
||||
private static final ECParameterSpec SPEC = new ECParameterSpec(
|
||||
new EllipticCurve(
|
||||
new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97", 16)),
|
||||
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD94", 16),
|
||||
new BigInteger("A6", 16)
|
||||
),
|
||||
new ECPoint(BigInteger.ONE, new BigInteger("8D91E471E0989CDA27DF505A453F2B7635294F2DDF23E3B122ACC99C9E9F1E14", 16)),
|
||||
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C611070995AD10045841B09B761B893", 16),
|
||||
1
|
||||
);
|
||||
|
||||
EcgostSignatureVerifier() throws NoSuchAlgorithmException {
|
||||
super("ECGOST3410", "GOST3411withECGOST3410");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] getSignature(RRSIG rrsig) {
|
||||
return rrsig.getSignature();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
|
||||
DataInput dis = key.getKeyAsDataInputStream();
|
||||
BigInteger x, y;
|
||||
|
||||
try {
|
||||
byte[] xBytes = new byte[LENGTH];
|
||||
dis.readFully(xBytes);
|
||||
reverse(xBytes);
|
||||
x = new BigInteger(1, xBytes);
|
||||
|
||||
byte[] yBytes = new byte[LENGTH];
|
||||
dis.readFully(yBytes);
|
||||
reverse(yBytes);
|
||||
y = new BigInteger(1, yBytes);
|
||||
} catch (IOException e) {
|
||||
throw new DataMalformedException(e, key.getKey());
|
||||
}
|
||||
|
||||
try {
|
||||
return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), SPEC));
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new DnssecInvalidKeySpecException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void reverse(byte[] array) {
|
||||
for (int i = 0; i < array.length / 2; i++) {
|
||||
int j = array.length - i - 1;
|
||||
byte tmp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.dnssec.DigestCalculator;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class JavaSecDigestCalculator implements DigestCalculator {
|
||||
private MessageDigest md;
|
||||
|
||||
public JavaSecDigestCalculator(String algorithm) throws NoSuchAlgorithmException {
|
||||
md = MessageDigest.getInstance(algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] digest(byte[] bytes) {
|
||||
return md.digest(bytes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.dnssec.DnssecValidationFailedException;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
|
||||
import org.minidns.dnssec.SignatureVerifier;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.RRSIG;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
|
||||
public abstract class JavaSecSignatureVerifier implements SignatureVerifier {
|
||||
private final KeyFactory keyFactory;
|
||||
private final String signatureAlgorithm;
|
||||
|
||||
public JavaSecSignatureVerifier(String keyAlgorithm, String signatureAlgorithm) throws NoSuchAlgorithmException {
|
||||
keyFactory = KeyFactory.getInstance(keyAlgorithm);
|
||||
this.signatureAlgorithm = signatureAlgorithm;
|
||||
|
||||
// Verify signature algorithm to be valid
|
||||
Signature.getInstance(signatureAlgorithm);
|
||||
}
|
||||
|
||||
public KeyFactory getKeyFactory() {
|
||||
return keyFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException {
|
||||
try {
|
||||
PublicKey publicKey = getPublicKey(key);
|
||||
Signature signature = Signature.getInstance(signatureAlgorithm);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(content);
|
||||
return signature.verify(getSignature(rrsig));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// We checked against this before, it should never happen!
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException | SignatureException | ArithmeticException e) {
|
||||
throw new DnssecValidationFailedException("Validating signature failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract byte[] getSignature(RRSIG rrsig) throws DataMalformedException;
|
||||
|
||||
protected abstract PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException;
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnssec.algorithms;
|
||||
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
|
||||
import org.minidns.record.DNSKEY;
|
||||
import org.minidns.record.RRSIG;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
|
||||
class RsaSignatureVerifier extends JavaSecSignatureVerifier {
|
||||
RsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException {
|
||||
super("RSA", algorithm);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
|
||||
DataInput dis = key.getKeyAsDataInputStream();
|
||||
BigInteger exponent, modulus;
|
||||
|
||||
try {
|
||||
int exponentLength = dis.readUnsignedByte();
|
||||
int bytesRead = 1;
|
||||
if (exponentLength == 0) {
|
||||
bytesRead += 2;
|
||||
exponentLength = dis.readUnsignedShort();
|
||||
}
|
||||
|
||||
byte[] exponentBytes = new byte[exponentLength];
|
||||
dis.readFully(exponentBytes);
|
||||
bytesRead += exponentLength;
|
||||
exponent = new BigInteger(1, exponentBytes);
|
||||
|
||||
byte[] modulusBytes = new byte[key.getKeyLength() - bytesRead];
|
||||
dis.readFully(modulusBytes);
|
||||
modulus = new BigInteger(1, modulusBytes);
|
||||
} catch (IOException e) {
|
||||
throw new DataMalformedException(e, key.getKey());
|
||||
}
|
||||
|
||||
try {
|
||||
return getKeyFactory().generatePublic(new RSAPublicKeySpec(modulus, exponent));
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new DnssecInvalidKeySpecException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] getSignature(RRSIG rrsig) {
|
||||
return rrsig.getSignature();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsserverlookup;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public abstract class AbstractDnsServerLookupMechanism implements DnsServerLookupMechanism {
|
||||
|
||||
protected static final Logger LOGGER = Logger.getLogger(AbstractDnsServerLookupMechanism.class.getName());
|
||||
|
||||
private final String name;
|
||||
private final int priority;
|
||||
|
||||
protected AbstractDnsServerLookupMechanism(String name, int priority) {
|
||||
this.name = name;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int compareTo(DnsServerLookupMechanism other) {
|
||||
int myPriority = getPriority();
|
||||
int otherPriority = other.getPriority();
|
||||
|
||||
return Integer.compare(myPriority, otherPriority);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract List<String> getDnsServerAddresses();
|
||||
|
||||
protected static List<String> toListOfStrings(Collection<? extends InetAddress> inetAddresses) {
|
||||
List<String> result = new ArrayList<>(inetAddresses.size());
|
||||
for (InetAddress inetAddress : inetAddresses) {
|
||||
String address = inetAddress.getHostAddress();
|
||||
result.add(address);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
115
src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java
Normal file
115
src/main/java/org/minidns/dnsserverlookup/AndroidUsingExec.java
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsserverlookup;
|
||||
|
||||
import org.minidns.util.PlatformDetection;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.LineNumberReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Try to retrieve the list of DNS server by executing getprop.
|
||||
*/
|
||||
public final class AndroidUsingExec extends AbstractDnsServerLookupMechanism {
|
||||
|
||||
public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingExec();
|
||||
public static final int PRIORITY = AndroidUsingReflection.PRIORITY - 1;
|
||||
|
||||
private AndroidUsingExec() {
|
||||
super(AndroidUsingExec.class.getSimpleName(), PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getDnsServerAddresses() {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec("getprop");
|
||||
InputStream inputStream = process.getInputStream();
|
||||
LineNumberReader lnr = new LineNumberReader(
|
||||
new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
Set<String> server = parseProps(lnr, true);
|
||||
if (server.size() > 0) {
|
||||
List<String> 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<String> parseProps(BufferedReader lnr, boolean logWarning) throws UnknownHostException, IOException {
|
||||
String line = null;
|
||||
Set<String> server = new HashSet<String>(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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsserverlookup;
|
||||
|
||||
import org.minidns.util.PlatformDetection;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Try to retrieve the list of DNS server by calling SystemProperties.
|
||||
*/
|
||||
public class AndroidUsingReflection extends AbstractDnsServerLookupMechanism {
|
||||
|
||||
public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingReflection();
|
||||
public static final int PRIORITY = 1000;
|
||||
|
||||
private final Method systemPropertiesGet;
|
||||
|
||||
protected AndroidUsingReflection() {
|
||||
super(AndroidUsingReflection.class.getSimpleName(), PRIORITY);
|
||||
Method systemPropertiesGet = null;
|
||||
if (PlatformDetection.isAndroid()) {
|
||||
try {
|
||||
Class<?> SystemProperties = Class.forName("android.os.SystemProperties");
|
||||
systemPropertiesGet = SystemProperties.getMethod("get", new Class<?>[] { String.class });
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
|
||||
// This is not unexpected, as newer Android versions do not provide access to it any more.
|
||||
LOGGER.log(Level.FINE, "Can not get method handle for android.os.SystemProperties.get(String).", e);
|
||||
}
|
||||
}
|
||||
this.systemPropertiesGet = systemPropertiesGet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getDnsServerAddresses() {
|
||||
ArrayList<String> servers = new ArrayList<String>(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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsserverlookup;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DnsServerLookupMechanism extends Comparable<DnsServerLookupMechanism> {
|
||||
|
||||
String getName();
|
||||
|
||||
int getPriority();
|
||||
|
||||
boolean isAvailable();
|
||||
|
||||
/**
|
||||
* Returns a List of String representing ideally IP addresses. The list must be modifiable.
|
||||
* <p>
|
||||
* 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()}.
|
||||
* </p>
|
||||
*
|
||||
* @return a List of Strings presenting hopefully IP addresses.
|
||||
*/
|
||||
List<String> getDnsServerAddresses();
|
||||
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsserverlookup;
|
||||
|
||||
import org.minidns.util.PlatformDetection;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class UnixUsingEtcResolvConf extends AbstractDnsServerLookupMechanism {
|
||||
|
||||
public static final DnsServerLookupMechanism INSTANCE = new UnixUsingEtcResolvConf();
|
||||
public static final int PRIORITY = 2000;
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(UnixUsingEtcResolvConf.class.getName());
|
||||
|
||||
private static final String RESOLV_CONF_FILE = "/etc/resolv.conf";
|
||||
private static final Pattern NAMESERVER_PATTERN = Pattern.compile("^nameserver\\s+(.*)$");
|
||||
|
||||
private static List<String> cached;
|
||||
private static long lastModified;
|
||||
|
||||
private UnixUsingEtcResolvConf() {
|
||||
super(UnixUsingEtcResolvConf.class.getSimpleName(), PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.dnsserverlookup.android21;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.RouteInfo;
|
||||
import android.os.Build;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.minidns.DnsClient;
|
||||
import org.minidns.dnsserverlookup.AbstractDnsServerLookupMechanism;
|
||||
import org.minidns.dnsserverlookup.AndroidUsingExec;
|
||||
|
||||
/**
|
||||
* A DNS server lookup mechanism using Android's Link Properties method available on Android API 21 or higher. Use
|
||||
* {@link #setup(Context)} to setup this mechanism.
|
||||
* <p>
|
||||
* Requires the ACCESS_NETWORK_STATE permission.
|
||||
* </p>
|
||||
*/
|
||||
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<String> 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<InetAddress> dnsServers = linkProperties.getDnsServers();
|
||||
return toListOfStrings(dnsServers);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(21)
|
||||
public List<String> getDnsServerAddresses() {
|
||||
// First, try the API 23 approach using ConnectivityManager.getActiveNetwork().
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
}
|
235
src/main/java/org/minidns/edns/Edns.java
Normal file
235
src/main/java/org/minidns/edns/Edns.java
Normal file
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.edns;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.OPT;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
/**
|
||||
* EDNS - Extension Mechanism for DNS.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6891">RFC 6891 - Extension Mechanisms for DNS (EDNS(0))</a>
|
||||
*
|
||||
*/
|
||||
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 <a href="http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11">IANA - DNS EDNS0 Option Codes (OPT)</a>
|
||||
*/
|
||||
public enum OptionCode {
|
||||
UNKNOWN(-1, UnknownEdnsOption.class),
|
||||
NSID(3, Nsid.class),
|
||||
;
|
||||
|
||||
private static Map<Integer, OptionCode> 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<? extends EdnsOption> clazz;
|
||||
|
||||
OptionCode(int optionCode, Class<? extends EdnsOption> 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<EdnsOption> variablePart;
|
||||
|
||||
public final boolean dnssecOk;
|
||||
|
||||
private Record<OPT> optRecord;
|
||||
|
||||
public Edns(Record<OPT> 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 extends EdnsOption> O getEdnsOption(OptionCode optionCode) {
|
||||
for (EdnsOption o : variablePart) {
|
||||
if (o.getOptionCode().equals(optionCode)) {
|
||||
return (O) o;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Record<OPT> asRecord() {
|
||||
if (optRecord == null) {
|
||||
long optFlags = flags;
|
||||
optFlags |= extendedRcode << 8;
|
||||
optFlags |= version << 16;
|
||||
optRecord = new Record<OPT>(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<EdnsOption> 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<? extends Data> record) {
|
||||
if (record.type != TYPE.OPT) return null;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Record<OPT> optRecord = (Record<OPT>) 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<EdnsOption> 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);
|
||||
}
|
||||
}
|
||||
}
|
83
src/main/java/org/minidns/edns/EdnsOption.java
Normal file
83
src/main/java/org/minidns/edns/EdnsOption.java
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.edns;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.minidns.edns.Edns.OptionCode;
|
||||
|
||||
public abstract class EdnsOption {
|
||||
|
||||
public final int optionCode;
|
||||
public final int optionLength;
|
||||
|
||||
protected final byte[] optionData;
|
||||
|
||||
protected EdnsOption(int optionCode, byte[] optionData) {
|
||||
this.optionCode = optionCode;
|
||||
this.optionLength = optionData.length;
|
||||
this.optionData = optionData;
|
||||
}
|
||||
|
||||
@SuppressWarnings("this-escape")
|
||||
protected EdnsOption(byte[] optionData) {
|
||||
this.optionCode = getOptionCode().asInt;
|
||||
this.optionLength = optionData.length;
|
||||
this.optionData = optionData;
|
||||
}
|
||||
|
||||
public final void writeToDos(DataOutputStream dos) throws IOException {
|
||||
dos.writeShort(optionCode);
|
||||
dos.writeShort(optionLength);
|
||||
dos.write(optionData);
|
||||
}
|
||||
|
||||
public abstract OptionCode getOptionCode();
|
||||
|
||||
private String toStringCache;
|
||||
|
||||
@Override
|
||||
public final String toString() {
|
||||
if (toStringCache == null) {
|
||||
toStringCache = toStringInternal().toString();
|
||||
}
|
||||
return toStringCache;
|
||||
}
|
||||
|
||||
protected abstract CharSequence toStringInternal();
|
||||
|
||||
private String terminalOutputCache;
|
||||
|
||||
public final String asTerminalOutput() {
|
||||
if (terminalOutputCache == null) {
|
||||
terminalOutputCache = asTerminalOutputInternal().toString();
|
||||
}
|
||||
return terminalOutputCache;
|
||||
}
|
||||
|
||||
protected abstract CharSequence asTerminalOutputInternal();
|
||||
|
||||
public static EdnsOption parse(int intOptionCode, byte[] optionData) {
|
||||
OptionCode optionCode = OptionCode.from(intOptionCode);
|
||||
EdnsOption res;
|
||||
switch (optionCode) {
|
||||
case NSID:
|
||||
res = new Nsid(optionData);
|
||||
break;
|
||||
default:
|
||||
res = new UnknownEdnsOption(intOptionCode, optionData);
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
47
src/main/java/org/minidns/edns/Nsid.java
Normal file
47
src/main/java/org/minidns/edns/Nsid.java
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.edns;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.minidns.edns.Edns.OptionCode;
|
||||
import org.minidns.util.Hex;
|
||||
|
||||
public class Nsid extends EdnsOption {
|
||||
|
||||
public static final Nsid REQUEST = new Nsid();
|
||||
|
||||
private Nsid() {
|
||||
this(new byte[0]);
|
||||
}
|
||||
|
||||
public Nsid(byte[] payload) {
|
||||
super(payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionCode getOptionCode() {
|
||||
return OptionCode.NSID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence toStringInternal() {
|
||||
String res = OptionCode.NSID + ": ";
|
||||
res += new String(optionData, StandardCharsets.US_ASCII);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence asTerminalOutputInternal() {
|
||||
return Hex.from(optionData);
|
||||
}
|
||||
|
||||
}
|
37
src/main/java/org/minidns/edns/UnknownEdnsOption.java
Normal file
37
src/main/java/org/minidns/edns/UnknownEdnsOption.java
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.edns;
|
||||
|
||||
import org.minidns.edns.Edns.OptionCode;
|
||||
import org.minidns.util.Hex;
|
||||
|
||||
public class UnknownEdnsOption extends EdnsOption {
|
||||
|
||||
protected UnknownEdnsOption(int optionCode, byte[] optionData) {
|
||||
super(optionCode, optionData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionCode getOptionCode() {
|
||||
return OptionCode.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence asTerminalOutputInternal() {
|
||||
return Hex.from(optionData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence toStringInternal() {
|
||||
return asTerminalOutputInternal();
|
||||
}
|
||||
|
||||
}
|
124
src/main/java/org/minidns/hla/DnssecResolverApi.java
Normal file
124
src/main/java/org/minidns/hla/DnssecResolverApi.java
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.DnsCache;
|
||||
import org.minidns.MiniDnsException.NullResultException;
|
||||
import org.minidns.cache.LruCache;
|
||||
import org.minidns.cache.MiniDnsCacheFactory;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnssec.DnssecClient;
|
||||
import org.minidns.dnssec.DnssecQueryResult;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason;
|
||||
import org.minidns.iterative.ReliableDnsClient.Mode;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
public class DnssecResolverApi extends ResolverApi {
|
||||
|
||||
public static final DnssecResolverApi INSTANCE = new DnssecResolverApi();
|
||||
|
||||
private final DnssecClient dnssecClient;
|
||||
private final DnssecClient iterativeOnlyDnssecClient;
|
||||
private final DnssecClient recursiveOnlyDnssecClient;
|
||||
|
||||
public DnssecResolverApi() {
|
||||
this(new MiniDnsCacheFactory() {
|
||||
@Override
|
||||
public DnsCache newCache() {
|
||||
return new LruCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public DnssecResolverApi(MiniDnsCacheFactory cacheFactory) {
|
||||
this(new DnssecClient(cacheFactory.newCache()), cacheFactory);
|
||||
}
|
||||
|
||||
private DnssecResolverApi(DnssecClient dnssecClient, MiniDnsCacheFactory cacheFactory) {
|
||||
super(dnssecClient);
|
||||
this.dnssecClient = dnssecClient;
|
||||
|
||||
// Set the *_ONLY_DNSSEC ResolverApi. It is important that the two do *not* share the same cache, since we
|
||||
// probably fall back to iterativeOnly and in that case do not want the cached results of the recursive result.
|
||||
iterativeOnlyDnssecClient = new DnssecClient(cacheFactory.newCache());
|
||||
iterativeOnlyDnssecClient.setMode(Mode.iterativeOnly);
|
||||
|
||||
recursiveOnlyDnssecClient = new DnssecClient(cacheFactory.newCache());
|
||||
recursiveOnlyDnssecClient.setMode(Mode.recursiveOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <D extends Data> ResolverResult<D> 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 <D> the RR type to resolve.
|
||||
* @return the resolver result.
|
||||
* @throws IOException in case an exception happens while resolving.
|
||||
* @see #resolveDnssecReliable(Question)
|
||||
*/
|
||||
public <D extends Data> ResolverResult<D> resolveDnssecReliable(String name, Class<D> 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 <D> the RR type to resolve.
|
||||
* @return the resolver result.
|
||||
* @throws IOException in case an exception happens while resolving.
|
||||
* @see #resolveDnssecReliable(Question)
|
||||
*/
|
||||
public <D extends Data> ResolverResult<D> resolveDnssecReliable(DnsName name, Class<D> 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 <D> the RR type to resolve.
|
||||
* @return the resolver result.
|
||||
* @throws IOException in case an exception happens while resolving.
|
||||
*/
|
||||
public <D extends Data> ResolverResult<D> 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 <D extends Data> ResolverResult<D> toResolverResult(Question question, DnssecQueryResult dnssecMessage) throws NullResultException {
|
||||
Set<DnssecUnverifiedReason> unverifiedReasons = dnssecMessage.getUnverifiedReasons();
|
||||
|
||||
return new ResolverResult<D>(question, dnssecMessage.dnsQueryResult, unverifiedReasons);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla;
|
||||
|
||||
import org.minidns.MiniDnsException;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
|
||||
|
||||
public class ResolutionUnsuccessfulException extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public final Question question;
|
||||
public final RESPONSE_CODE responseCode;
|
||||
|
||||
public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) {
|
||||
super("Asking for " + question + " yielded an error response " + responseCode);
|
||||
this.question = question;
|
||||
this.responseCode = responseCode;
|
||||
}
|
||||
}
|
222
src/main/java/org/minidns/hla/ResolverApi.java
Normal file
222
src/main/java/org/minidns/hla/ResolverApi.java
Normal file
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.minidns.AbstractDnsClient;
|
||||
import org.minidns.DnsClient;
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.hla.srv.SrvProto;
|
||||
import org.minidns.hla.srv.SrvService;
|
||||
import org.minidns.hla.srv.SrvServiceProto;
|
||||
import org.minidns.hla.srv.SrvType;
|
||||
import org.minidns.iterative.ReliableDnsClient;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.PTR;
|
||||
import org.minidns.record.SRV;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
/**
|
||||
* The high-level MiniDNS resolving API. It is designed to be easy to use.
|
||||
* <p>
|
||||
* A simple exammple how to resolve the IPv4 address of a given domain:
|
||||
* </p>
|
||||
* <pre>
|
||||
* {@code
|
||||
* ResolverResult<A> 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<A> answers = result.getAnswers();
|
||||
* for (A a : answers) {
|
||||
* InetAddress inetAddress = a.getInetAddress();
|
||||
* // Do someting with the InetAddress, e.g. connect to.
|
||||
* …
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* MiniDNS also supports SRV resource records as first class citizens:
|
||||
* </p>
|
||||
* <pre>
|
||||
* {@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<ResolvedSrvRecord> 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.
|
||||
* …
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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 <D extends Data> ResolverResult<D> resolve(String name, Class<D> type) throws IOException {
|
||||
return resolve(DnsName.from(name), type);
|
||||
}
|
||||
|
||||
public final <D extends Data> ResolverResult<D> resolve(DnsName name, Class<D> type) throws IOException {
|
||||
TYPE t = TYPE.getType(type);
|
||||
Question q = new Question(name, t);
|
||||
return resolve(q);
|
||||
}
|
||||
|
||||
public <D extends Data> ResolverResult<D> resolve(Question question) throws IOException {
|
||||
DnsQueryResult dnsQueryResult = dnsClient.query(question);
|
||||
|
||||
return new ResolverResult<D>(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<PTR> reverseLookup(CharSequence inetAddressCs) throws IOException {
|
||||
InetAddress inetAddress = InetAddress.getByName(inetAddressCs.toString());
|
||||
return reverseLookup(inetAddress);
|
||||
}
|
||||
|
||||
public ResolverResult<PTR> 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<PTR> reverseLookup(Inet4Address inet4Address) throws IOException {
|
||||
Question question = DnsClient.getReverseIpLookupQuestionFor(inet4Address);
|
||||
return resolve(question);
|
||||
}
|
||||
|
||||
public ResolverResult<PTR> 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.
|
||||
* <p>
|
||||
* The name of SRV records is "_[service]._[protocol].[serviceDomain]", for example "_xmpp-client._tcp.example.org".
|
||||
* </p>
|
||||
*
|
||||
* @param srvDnsName the name to resolve.
|
||||
* @return a <code>SrvResolverResult</code> 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 <code>SrvResolverResult</code> 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<SRV> result = resolve(srvDnsName, SRV.class);
|
||||
|
||||
return new SrvResolverResult(result, srvServiceProto, this);
|
||||
}
|
||||
|
||||
public final AbstractDnsClient getClient() {
|
||||
return dnsClient;
|
||||
}
|
||||
}
|
178
src/main/java/org/minidns/hla/ResolverResult.java
Normal file
178
src/main/java/org/minidns/hla/ResolverResult.java
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.MiniDnsException;
|
||||
import org.minidns.MiniDnsException.NullResultException;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
|
||||
import org.minidns.dnssec.DnssecResultNotAuthenticException;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason;
|
||||
import org.minidns.record.Data;
|
||||
|
||||
public class ResolverResult<D extends Data> {
|
||||
|
||||
protected final Question question;
|
||||
private final RESPONSE_CODE responseCode;
|
||||
private final Set<D> data;
|
||||
private final boolean isAuthenticData;
|
||||
protected final Set<DnssecUnverifiedReason> unverifiedReasons;
|
||||
protected final DnsMessage answer;
|
||||
protected final DnsQueryResult result;
|
||||
|
||||
ResolverResult(Question question, DnsQueryResult result, Set<DnssecUnverifiedReason> 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<D> 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<D> getAnswers() {
|
||||
throwIseIfErrorResponse();
|
||||
return data;
|
||||
}
|
||||
|
||||
public Set<D> 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 <code>null</code>.
|
||||
*/
|
||||
public Set<DnssecUnverifiedReason> 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. <b>This is likely not what you want</b>, 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);
|
||||
}
|
||||
}
|
204
src/main/java/org/minidns/hla/SrvResolverResult.java
Normal file
204
src/main/java/org/minidns/hla/SrvResolverResult.java
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.AbstractDnsClient.IpVersionSetting;
|
||||
import org.minidns.MiniDnsException.NullResultException;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.hla.srv.SrvServiceProto;
|
||||
import org.minidns.record.A;
|
||||
import org.minidns.record.AAAA;
|
||||
import org.minidns.record.InternetAddressRR;
|
||||
import org.minidns.record.SRV;
|
||||
import org.minidns.util.SrvUtil;
|
||||
|
||||
public class SrvResolverResult extends ResolverResult<SRV> {
|
||||
|
||||
private final ResolverApi resolver;
|
||||
private final IpVersionSetting ipVersion;
|
||||
private final SrvServiceProto srvServiceProto;
|
||||
|
||||
private List<ResolvedSrvRecord> sortedSrvResolvedAddresses;
|
||||
|
||||
SrvResolverResult(ResolverResult<SRV> 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<ResolvedSrvRecord> getSortedSrvResolvedAddresses() throws IOException {
|
||||
if (sortedSrvResolvedAddresses != null) {
|
||||
return sortedSrvResolvedAddresses;
|
||||
}
|
||||
|
||||
throwIseIfErrorResponse();
|
||||
|
||||
if (isServiceDecidedlyNotAvailableAtThisDomain()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<SRV> srvRecords = SrvUtil.sortSrvRecords(getAnswers());
|
||||
|
||||
List<ResolvedSrvRecord> res = new ArrayList<>(srvRecords.size());
|
||||
for (SRV srvRecord : srvRecords) {
|
||||
ResolverResult<A> aRecordsResult = null;
|
||||
ResolverResult<AAAA> aaaaRecordsResult = null;
|
||||
Set<A> aRecords = Collections.emptySet();
|
||||
if (ipVersion.v4) {
|
||||
aRecordsResult = resolver.resolve(srvRecord.target, A.class);
|
||||
if (aRecordsResult.wasSuccessful() && !aRecordsResult.hasUnverifiedReasons()) {
|
||||
aRecords = aRecordsResult.getAnswers();
|
||||
}
|
||||
}
|
||||
|
||||
Set<AAAA> 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<CNAME> cnameRecordResult = resolve(srvRecord.name, CNAME.class);
|
||||
if (cnameRecordResult.wasSuccessful()) {
|
||||
}
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
List<InternetAddressRR<? extends InetAddress>> 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<SRV> 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<InternetAddressRR<? extends InetAddress>> addresses;
|
||||
public final ResolverResult<A> aRecordsResult;
|
||||
public final ResolverResult<AAAA> aaaaRecordsResult;
|
||||
|
||||
/**
|
||||
* The port announced by the SRV RR. This is simply a shortcut for <code>srv.port</code>.
|
||||
*/
|
||||
public final int port;
|
||||
|
||||
private ResolvedSrvRecord(DnsName name, SrvServiceProto srvServiceProto, SRV srv,
|
||||
List<InternetAddressRR<? extends InetAddress>> addresses, ResolverResult<A> aRecordsResult,
|
||||
ResolverResult<AAAA> 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<ResolvedSrvRecord> sortMultiple(Collection<ResolvedSrvRecord>... resolvedSrvRecordCollections) {
|
||||
int srvRecordsCount = 0;
|
||||
for (Collection<ResolvedSrvRecord> resolvedSrvRecords : resolvedSrvRecordCollections) {
|
||||
if (resolvedSrvRecords == null) {
|
||||
continue;
|
||||
}
|
||||
srvRecordsCount += resolvedSrvRecords.size();
|
||||
}
|
||||
|
||||
List<SRV> srvToSort = new ArrayList<>(srvRecordsCount);
|
||||
IdentityHashMap<SRV, ResolvedSrvRecord> identityMap = new IdentityHashMap<>(srvRecordsCount);
|
||||
for (Collection<ResolvedSrvRecord> resolvedSrvRecords : resolvedSrvRecordCollections) {
|
||||
if (resolvedSrvRecords == null) {
|
||||
continue;
|
||||
}
|
||||
for (ResolvedSrvRecord resolvedSrvRecord : resolvedSrvRecords) {
|
||||
srvToSort.add(resolvedSrvRecord.srv);
|
||||
identityMap.put(resolvedSrvRecord.srv, resolvedSrvRecord);
|
||||
}
|
||||
}
|
||||
|
||||
List<SRV> sortedSrvs = SrvUtil.sortSrvRecords(srvToSort);
|
||||
assert sortedSrvs.size() == srvRecordsCount;
|
||||
|
||||
List<ResolvedSrvRecord> res = new ArrayList<>(srvRecordsCount);
|
||||
for (SRV sortedSrv : sortedSrvs) {
|
||||
ResolvedSrvRecord resolvedSrvRecord = identityMap.get(sortedSrv);
|
||||
res.add(resolvedSrvRecord);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
29
src/main/java/org/minidns/hla/srv/SrvProto.java
Normal file
29
src/main/java/org/minidns/hla/srv/SrvProto.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla.srv;
|
||||
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
|
||||
public enum SrvProto {
|
||||
|
||||
// @formatter:off
|
||||
tcp,
|
||||
udp,
|
||||
;
|
||||
// @formatter:on
|
||||
|
||||
@SuppressWarnings("ImmutableEnumChecker")
|
||||
public final DnsLabel dnsLabel;
|
||||
|
||||
SrvProto() {
|
||||
dnsLabel = DnsLabel.from('_' + name());
|
||||
}
|
||||
}
|
44
src/main/java/org/minidns/hla/srv/SrvService.java
Normal file
44
src/main/java/org/minidns/hla/srv/SrvService.java
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla.srv;
|
||||
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
|
||||
public enum SrvService {
|
||||
|
||||
// @formatter:off
|
||||
xmpp_client,
|
||||
xmpp_server,
|
||||
|
||||
/**
|
||||
* XMPP client-to-server (c2s) connections using implicit TLS (also known as "Direct TLS").
|
||||
*
|
||||
* @see <a href="https://xmpp.org/extensions/xep-0368.html">XEP-0368: SRV records for XMPP over TLS</a>
|
||||
*/
|
||||
xmpps_client,
|
||||
|
||||
/**
|
||||
* XMPP server-to-server (s2s) connections using implicit TLS (also known as "Direct TLS").
|
||||
*
|
||||
* @see <a href="https://xmpp.org/extensions/xep-0368.html">XEP-0368: SRV records for XMPP over TLS</a>
|
||||
*/
|
||||
xmpps_server,
|
||||
;
|
||||
// @formatter:on
|
||||
|
||||
@SuppressWarnings("ImmutableEnumChecker")
|
||||
public final DnsLabel dnsLabel;
|
||||
|
||||
SrvService() {
|
||||
String enumName = name().replaceAll("_", "-");
|
||||
dnsLabel = DnsLabel.from('_' + enumName);
|
||||
}
|
||||
}
|
27
src/main/java/org/minidns/hla/srv/SrvServiceProto.java
Normal file
27
src/main/java/org/minidns/hla/srv/SrvServiceProto.java
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla.srv;
|
||||
|
||||
import org.minidns.dnslabel.DnsLabel;
|
||||
|
||||
/**
|
||||
* The Serivce and Protocol part of a SRV owner name. The format of a SRV owner name is "_Service._Proto.Name".
|
||||
*/
|
||||
public class SrvServiceProto {
|
||||
|
||||
public final DnsLabel service;
|
||||
public final DnsLabel proto;
|
||||
|
||||
public SrvServiceProto(DnsLabel service, DnsLabel proto) {
|
||||
this.service = service;
|
||||
this.proto = proto;
|
||||
}
|
||||
}
|
28
src/main/java/org/minidns/hla/srv/SrvType.java
Normal file
28
src/main/java/org/minidns/hla/srv/SrvType.java
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.hla.srv;
|
||||
|
||||
public enum SrvType {
|
||||
|
||||
// @formatter:off
|
||||
xmpp_client(SrvService.xmpp_client, SrvProto.tcp),
|
||||
xmpp_server(SrvService.xmpp_server, SrvProto.tcp),
|
||||
;
|
||||
// @formatter:on
|
||||
|
||||
public final SrvService service;
|
||||
public final SrvProto proto;
|
||||
|
||||
SrvType(SrvService service, SrvProto proto) {
|
||||
this.service = service;
|
||||
this.proto = proto;
|
||||
}
|
||||
}
|
36
src/main/java/org/minidns/idna/DefaultIdnaTransformator.java
Normal file
36
src/main/java/org/minidns/idna/DefaultIdnaTransformator.java
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.idna;
|
||||
|
||||
import java.net.IDN;
|
||||
|
||||
import org.minidns.dnsname.DnsName;
|
||||
|
||||
public class DefaultIdnaTransformator implements IdnaTransformator {
|
||||
|
||||
@Override
|
||||
public String toASCII(String input) {
|
||||
// Special case if input is ".", i.e. a string containing only a single dot character. This is a workaround for
|
||||
// IDN.toASCII() implementations throwing an IllegalArgumentException on this input string (for example Android
|
||||
// APIs level 26, see https://issuetracker.google.com/issues/113070416).
|
||||
if (DnsName.ROOT.ace.equals(input)) {
|
||||
return DnsName.ROOT.ace;
|
||||
}
|
||||
|
||||
return IDN.toASCII(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toUnicode(String input) {
|
||||
return IDN.toUnicode(input);
|
||||
}
|
||||
|
||||
}
|
19
src/main/java/org/minidns/idna/IdnaTransformator.java
Normal file
19
src/main/java/org/minidns/idna/IdnaTransformator.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.idna;
|
||||
|
||||
public interface IdnaTransformator {
|
||||
|
||||
String toASCII(String input);
|
||||
|
||||
String toUnicode(String input);
|
||||
|
||||
}
|
31
src/main/java/org/minidns/idna/MiniDnsIdna.java
Normal file
31
src/main/java/org/minidns/idna/MiniDnsIdna.java
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.idna;
|
||||
|
||||
public class MiniDnsIdna {
|
||||
|
||||
private static IdnaTransformator idnaTransformator = new DefaultIdnaTransformator();
|
||||
|
||||
public static String toASCII(String string) {
|
||||
return idnaTransformator.toASCII(string);
|
||||
}
|
||||
|
||||
public static String toUnicode(String string) {
|
||||
return idnaTransformator.toUnicode(string);
|
||||
}
|
||||
|
||||
public static void setActiveTransformator(IdnaTransformator idnaTransformator) {
|
||||
if (idnaTransformator == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
MiniDnsIdna.idnaTransformator = idnaTransformator;
|
||||
}
|
||||
}
|
56
src/main/java/org/minidns/integrationtest/AsyncApiTest.java
Normal file
56
src/main/java/org/minidns/integrationtest/AsyncApiTest.java
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.minidns.DnsClient;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.MiniDnsFuture;
|
||||
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.source.AbstractDnsDataSource;
|
||||
import org.minidns.source.AbstractDnsDataSource.QueryMode;
|
||||
import org.minidns.source.async.AsyncNetworkDataSource;
|
||||
|
||||
public class AsyncApiTest {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
tcpAsyncApiTest();
|
||||
}
|
||||
|
||||
public static void simpleAsyncApiTest() throws IOException {
|
||||
DnsClient client = new DnsClient();
|
||||
client.setDataSource(new AsyncNetworkDataSource());
|
||||
client.getDataSource().setTimeout(60 * 60 * 1000);
|
||||
|
||||
MiniDnsFuture<DnsQueryResult, IOException> 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<DnsQueryResult, IOException> future = client.queryAsync("google.com", Record.TYPE.AAAA);
|
||||
DnsQueryResult result = future.getOrThrow();
|
||||
assertEquals(RESPONSE_CODE.NO_ERROR, result.response.responseCode);
|
||||
}
|
||||
}
|
58
src/main/java/org/minidns/integrationtest/CoreTest.java
Normal file
58
src/main/java/org/minidns/integrationtest/CoreTest.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import org.minidns.DnsClient;
|
||||
import org.minidns.cache.LruCache;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.Record;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class CoreTest {
|
||||
@IntegrationTest
|
||||
public static void testExampleCom() throws IOException {
|
||||
DnsClient client = new DnsClient(new LruCache(1024));
|
||||
String exampleIp4 = "93.184.216.34"; // stable?
|
||||
String exampleIp6 = "2606:2800:220:1:248:1893:25c8:1946"; // stable?
|
||||
assertEquals(client.query("example.com", Record.TYPE.A).response.answerSection.get(0).payloadData.toString(), exampleIp4);
|
||||
assertEquals(client.query("www.example.com", Record.TYPE.A).response.answerSection.get(0).payloadData.toString(), exampleIp4);
|
||||
assertEquals(client.query("example.com", Record.TYPE.AAAA).response.answerSection.get(0).payloadData.toString(), exampleIp6);
|
||||
assertEquals(client.query("www.example.com", Record.TYPE.AAAA).response.answerSection.get(0).payloadData.toString(), exampleIp6);
|
||||
|
||||
DnsQueryResult nsResult = client.query("example.com", Record.TYPE.NS);
|
||||
List<String> values = new ArrayList<>();
|
||||
for (Record<? extends Data> 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);
|
||||
}
|
||||
}
|
45
src/main/java/org/minidns/integrationtest/DaneTest.java
Normal file
45
src/main/java/org/minidns/integrationtest/DaneTest.java
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import org.minidns.dane.DaneVerifier;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
import org.junit.Ignore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
public class DaneTest {
|
||||
|
||||
@Ignore
|
||||
@IntegrationTest
|
||||
public static void testOarcDaneGood() throws IOException, CertificateException {
|
||||
DaneVerifier daneVerifier = new DaneVerifier();
|
||||
daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://good.dane.dns-oarc.net/").openConnection());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@IntegrationTest()
|
||||
public static void testOarcDaneBadHash() throws IOException, CertificateException {
|
||||
DaneVerifier daneVerifier = new DaneVerifier();
|
||||
daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://bad-hash.dane.dns-oarc.net/").openConnection());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@IntegrationTest
|
||||
public static void testOarcDaneBadParams() throws IOException, CertificateException {
|
||||
DaneVerifier daneVerifier = new DaneVerifier();
|
||||
daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://bad-params.dane.dns-oarc.net/").openConnection());
|
||||
}
|
||||
}
|
65
src/main/java/org/minidns/integrationtest/DnssecTest.java
Normal file
65
src/main/java/org/minidns/integrationtest/DnssecTest.java
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.minidns.cache.LruCache;
|
||||
import org.minidns.dnssec.DnssecClient;
|
||||
import org.minidns.dnssec.DnssecQueryResult;
|
||||
import org.minidns.dnssec.DnssecUnverifiedReason;
|
||||
import org.minidns.dnssec.DnssecValidationFailedException;
|
||||
import org.minidns.record.Record;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
public class DnssecTest {
|
||||
|
||||
@Ignore
|
||||
@IntegrationTest
|
||||
public static void testOarcDaneBadSig() throws Exception {
|
||||
DnssecClient client = new DnssecClient(new LruCache(1024));
|
||||
assertFalse(client.queryDnssec("_443._tcp.bad-sig.dane.dns-oarc.net", Record.TYPE.TLSA).isAuthenticData());
|
||||
}
|
||||
|
||||
@IntegrationTest
|
||||
public static void testUniDueSigOk() throws IOException {
|
||||
DnssecClient client = new DnssecClient(new LruCache(1024));
|
||||
assertAuthentic(client.queryDnssec("sigok.verteiltesysteme.net", Record.TYPE.A));
|
||||
}
|
||||
|
||||
@IntegrationTest(expected = DnssecValidationFailedException.class)
|
||||
public static void testUniDueSigFail() throws IOException {
|
||||
DnssecClient client = new DnssecClient(new LruCache(1024));
|
||||
client.query("sigfail.verteiltesysteme.net", Record.TYPE.A);
|
||||
}
|
||||
|
||||
@IntegrationTest
|
||||
public static void testCloudFlare() throws IOException {
|
||||
DnssecClient client = new DnssecClient(new LruCache(1024));
|
||||
assertAuthentic(client.queryDnssec("www.cloudflare-dnssec-auth.com", Record.TYPE.A));
|
||||
}
|
||||
|
||||
private static void assertAuthentic(DnssecQueryResult dnssecMessage) {
|
||||
if (dnssecMessage.isAuthenticData()) return;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Answer should contain authentic data while it does not. Reasons:\n");
|
||||
for (Iterator<DnssecUnverifiedReason> it = dnssecMessage.getUnverifiedReasons().iterator(); it.hasNext(); ) {
|
||||
DnssecUnverifiedReason unverifiedReason = it.next();
|
||||
sb.append(unverifiedReason);
|
||||
if (it.hasNext()) sb.append('\n');
|
||||
}
|
||||
throw new AssertionError(sb.toString());
|
||||
}
|
||||
}
|
57
src/main/java/org/minidns/integrationtest/HlaTest.java
Normal file
57
src/main/java/org/minidns/integrationtest/HlaTest.java
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.hla.ResolverApi;
|
||||
import org.minidns.hla.ResolverResult;
|
||||
import org.minidns.hla.SrvResolverResult;
|
||||
import org.minidns.record.A;
|
||||
import org.minidns.record.SRV;
|
||||
|
||||
public class HlaTest {
|
||||
|
||||
@IntegrationTest
|
||||
public static void resolverTest() throws IOException {
|
||||
ResolverResult<A> res = ResolverApi.INSTANCE.resolve("geekplace.eu", A.class);
|
||||
assertEquals(true, res.wasSuccessful());
|
||||
Set<A> 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<SRV> res = ResolverApi.INSTANCE.resolve("_xmpp-client._tcp.im.plä.net", SRV.class);
|
||||
Set<SRV> answers = res.getAnswers();
|
||||
assertEquals(1, answers.size());
|
||||
|
||||
SRV srv = answers.iterator().next();
|
||||
|
||||
ResolverResult<A> 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<SRV> answers = resolverResult.getAnswers();
|
||||
assertFalse(answers.isEmpty());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface IntegrationTest {
|
||||
Class<?> expected() default Class.class;
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.jul.MiniDnsJul;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
public class IntegrationTestHelper {
|
||||
|
||||
public static final DnsName DNSSEC_DOMAIN = DnsName.from("verteiltesysteme.net");
|
||||
public static final TYPE RR_TYPE = TYPE.A;
|
||||
|
||||
private static Set<Class<?>> 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<Method> successfulTests = new ArrayList<>();
|
||||
List<Method> failedTests = new ArrayList<>();
|
||||
List<Method> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import org.minidns.DnsCache;
|
||||
import org.minidns.cache.ExtendedLruCache;
|
||||
import org.minidns.cache.FullLruCache;
|
||||
import org.minidns.cache.LruCache;
|
||||
import org.minidns.dnssec.DnssecClient;
|
||||
import org.minidns.source.NetworkDataSourceWithAccounting;
|
||||
|
||||
public class IntegrationTestTools {
|
||||
|
||||
public enum CacheConfig {
|
||||
without,
|
||||
normal,
|
||||
extended,
|
||||
full,
|
||||
}
|
||||
|
||||
public static DnssecClient getClient(CacheConfig cacheConfig) {
|
||||
DnsCache cache;
|
||||
switch (cacheConfig) {
|
||||
case without:
|
||||
cache = null;
|
||||
break;
|
||||
case normal:
|
||||
cache = new LruCache();
|
||||
break;
|
||||
case extended:
|
||||
cache = new ExtendedLruCache();
|
||||
break;
|
||||
case full:
|
||||
cache = new FullLruCache();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
DnssecClient client = new DnssecClient(cache);
|
||||
client.setDataSource(new NetworkDataSourceWithAccounting());
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnssec.DnssecClient;
|
||||
import org.minidns.dnssec.DnssecQueryResult;
|
||||
import org.minidns.integrationtest.IntegrationTestTools.CacheConfig;
|
||||
import org.minidns.iterative.ReliableDnsClient.Mode;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
import org.minidns.source.NetworkDataSourceWithAccounting;
|
||||
|
||||
public class IterativeDnssecTest {
|
||||
|
||||
private static final DnsName DNSSEC_DOMAIN = IntegrationTestHelper.DNSSEC_DOMAIN;
|
||||
private static final TYPE RR_TYPE = IntegrationTestHelper.RR_TYPE;
|
||||
|
||||
@IntegrationTest
|
||||
public static void shouldRequireLessQueries() throws IOException {
|
||||
DnssecClient normalCacheClient = getClient(CacheConfig.normal);
|
||||
DnssecQueryResult normalCacheResult = normalCacheClient.queryDnssec(DNSSEC_DOMAIN, RR_TYPE);
|
||||
assertTrue(normalCacheResult.isAuthenticData());
|
||||
NetworkDataSourceWithAccounting normalCacheNdswa = NetworkDataSourceWithAccounting.from(normalCacheClient);
|
||||
|
||||
DnssecClient extendedCacheClient = getClient(CacheConfig.extended);
|
||||
DnssecQueryResult extendedCacheResult = extendedCacheClient.queryDnssec(DNSSEC_DOMAIN, RR_TYPE);
|
||||
assertTrue(extendedCacheResult.isAuthenticData());
|
||||
NetworkDataSourceWithAccounting extendedCacheNdswa = NetworkDataSourceWithAccounting.from(extendedCacheClient);
|
||||
|
||||
final int normalCacheSuccessfulQueries = normalCacheNdswa.getStats().successfulQueries;
|
||||
final int extendedCacheSuccessfulQueries = extendedCacheNdswa.getStats().successfulQueries;
|
||||
assertTrue(
|
||||
normalCacheSuccessfulQueries > extendedCacheSuccessfulQueries,
|
||||
"Extend cache successful query count " + extendedCacheSuccessfulQueries
|
||||
+ " is not less than normal cache successful query count " + normalCacheSuccessfulQueries);
|
||||
}
|
||||
|
||||
private static DnssecClient getClient(CacheConfig cacheConfig) {
|
||||
DnssecClient client = IntegrationTestTools.getClient(cacheConfig);
|
||||
client.setMode(Mode.iterativeOnly);
|
||||
return client;
|
||||
}
|
||||
}
|
52
src/main/java/org/minidns/integrationtest/NsidTest.java
Normal file
52
src/main/java/org/minidns/integrationtest/NsidTest.java
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.integrationtest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.minidns.DnsClient;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.edns.Nsid;
|
||||
import org.minidns.edns.Edns.OptionCode;
|
||||
import org.minidns.iterative.IterativeDnsClient;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class NsidTest {
|
||||
|
||||
@IntegrationTest
|
||||
public static Nsid testNsidLRoot() {
|
||||
DnsClient client = new DnsClient(null) {
|
||||
@Override
|
||||
protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) {
|
||||
message.getEdnsBuilder().addEdnsOption(Nsid.REQUEST);
|
||||
return super.newQuestion(message);
|
||||
}
|
||||
};
|
||||
DnsQueryResult result = null;
|
||||
Question q = new Question("de", TYPE.NS);
|
||||
for (InetAddress lRoot : IterativeDnsClient.getRootServer('l')) {
|
||||
try {
|
||||
result = client.query(q, lRoot);
|
||||
} catch (IOException e) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Nsid nsid = result.response.getEdns().getEdnsOption(OptionCode.NSID);
|
||||
assertNotNull(nsid);
|
||||
return nsid;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.iterative;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.minidns.MiniDnsException;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
|
||||
public abstract class IterativeClientException extends MiniDnsException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected IterativeClientException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static class LoopDetected extends IterativeClientException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public final InetAddress address;
|
||||
public final Question question;
|
||||
|
||||
public LoopDetected(InetAddress address, Question question) {
|
||||
super("Resolution loop detected: We already asked " + address + " about " + question);
|
||||
this.address = address;
|
||||
this.question = question;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class MaxIterativeStepsReached extends IterativeClientException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MaxIterativeStepsReached() {
|
||||
super("Maxmimum steps reached");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class NotAuthoritativeNorGlueRrFound extends IterativeClientException {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final DnsMessage request;
|
||||
private final DnsQueryResult result;
|
||||
private final DnsName authoritativeZone;
|
||||
|
||||
public NotAuthoritativeNorGlueRrFound(DnsMessage request, DnsQueryResult result, DnsName authoritativeZone) {
|
||||
super("Did not receive an authoritative answer, nor did the result contain any glue records");
|
||||
this.request = request;
|
||||
this.result = result;
|
||||
this.authoritativeZone = authoritativeZone;
|
||||
}
|
||||
|
||||
public DnsMessage getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public DnsQueryResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public DnsName getAuthoritativeZone() {
|
||||
return authoritativeZone;
|
||||
}
|
||||
}
|
||||
}
|
502
src/main/java/org/minidns/iterative/IterativeDnsClient.java
Normal file
502
src/main/java/org/minidns/iterative/IterativeDnsClient.java
Normal file
|
@ -0,0 +1,502 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.iterative;
|
||||
|
||||
import static org.minidns.constants.DnsRootServer.getIpv4RootServerById;
|
||||
import static org.minidns.constants.DnsRootServer.getIpv6RootServerById;
|
||||
import static org.minidns.constants.DnsRootServer.getRandomIpv4RootServer;
|
||||
import static org.minidns.constants.DnsRootServer.getRandomIpv6RootServer;
|
||||
|
||||
import org.minidns.AbstractDnsClient;
|
||||
import org.minidns.DnsCache;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsname.DnsName;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.iterative.IterativeClientException.LoopDetected;
|
||||
import org.minidns.iterative.IterativeClientException.NotAuthoritativeNorGlueRrFound;
|
||||
import org.minidns.record.A;
|
||||
import org.minidns.record.AAAA;
|
||||
import org.minidns.record.RRWithTarget;
|
||||
import org.minidns.record.Record;
|
||||
import org.minidns.record.Record.TYPE;
|
||||
import org.minidns.record.Data;
|
||||
import org.minidns.record.InternetAddressRR;
|
||||
import org.minidns.record.NS;
|
||||
import org.minidns.util.MultipleIoException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class IterativeDnsClient extends AbstractDnsClient {
|
||||
|
||||
int maxSteps = 128;
|
||||
|
||||
/**
|
||||
* Create a new recursive DNS client using the global default cache.
|
||||
*/
|
||||
public IterativeDnsClient() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new recursive DNS client with the given DNS cache.
|
||||
*
|
||||
* @param cache The backend DNS cache.
|
||||
*/
|
||||
public IterativeDnsClient(DnsCache cache) {
|
||||
super(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively query the DNS system for one entry.
|
||||
*
|
||||
* @param queryBuilder The query DNS message builder.
|
||||
* @return The response (or null on timeout/error).
|
||||
* @throws IOException if an IO error occurs.
|
||||
*/
|
||||
@Override
|
||||
protected DnsQueryResult query(DnsMessage.Builder queryBuilder) throws IOException {
|
||||
DnsMessage q = queryBuilder.build();
|
||||
ResolutionState resolutionState = new ResolutionState(this);
|
||||
DnsQueryResult result = queryRecursive(resolutionState, q);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static InetAddress[] getTargets(Collection<? extends InternetAddressRR<? extends InetAddress>> primaryTargets,
|
||||
Collection<? extends InternetAddressRR<? extends InetAddress>> secondaryTargets) {
|
||||
InetAddress[] res = new InetAddress[2];
|
||||
|
||||
for (InternetAddressRR<? extends InetAddress> 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<? extends InetAddress> 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<IOException> 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<Record<? extends Data>> authorities = resMessage.copyAuthority();
|
||||
|
||||
List<IOException> ioExceptions = new ArrayList<>();
|
||||
|
||||
// Glued NS first
|
||||
for (Iterator<Record<? extends Data>> iterator = authorities.iterator(); iterator.hasNext(); ) {
|
||||
Record<NS> record = iterator.next().ifPossibleAs(NS.class);
|
||||
if (record == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
DnsName name = record.payloadData.target;
|
||||
IpResultSet gluedNs = searchAdditional(resMessage, name);
|
||||
for (Iterator<InetAddress> 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<? extends Data> 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<? extends Data> 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<? extends Data> 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<? extends Data> 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<InetAddress> getRootServer(char rootServerId) {
|
||||
return getRootServer(rootServerId, DEFAULT_IP_VERSION_SETTING);
|
||||
}
|
||||
|
||||
public static List<InetAddress> getRootServer(char rootServerId, IpVersionSetting setting) {
|
||||
Inet4Address ipv4Root = getIpv4RootServerById(rootServerId);
|
||||
Inet6Address ipv6Root = getIpv6RootServerById(rootServerId);
|
||||
List<InetAddress> 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<InetAddress> addresses;
|
||||
|
||||
private IpResultSet(List<InetAddress> ipv4Addresses, List<InetAddress> 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<InetAddress> 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<InetAddress> ipv4Addresses = new ArrayList<>(8);
|
||||
private final List<InetAddress> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
190
src/main/java/org/minidns/iterative/ReliableDnsClient.java
Normal file
190
src/main/java/org/minidns/iterative/ReliableDnsClient.java
Normal file
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.iterative;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.minidns.AbstractDnsClient;
|
||||
import org.minidns.DnsCache;
|
||||
import org.minidns.DnsClient;
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.dnsqueryresult.DnsQueryResult;
|
||||
import org.minidns.source.DnsDataSource;
|
||||
import org.minidns.util.MultipleIoException;
|
||||
|
||||
/**
|
||||
* A DNS client using a reliable strategy. First the configured resolver of the
|
||||
* system are used, then, in case there is no answer, a fall back to iterative
|
||||
* resolving is performed.
|
||||
*/
|
||||
public class ReliableDnsClient extends AbstractDnsClient {
|
||||
|
||||
public enum Mode {
|
||||
/**
|
||||
* Try the recursive servers first and fallback to iterative resolving if it fails. This is the default mode.
|
||||
*/
|
||||
recursiveWithIterativeFallback,
|
||||
|
||||
/**
|
||||
* Only try the recursive servers. This makes {@code ReliableDnsClient} behave like a {@link DnsClient}.
|
||||
*/
|
||||
recursiveOnly,
|
||||
|
||||
/**
|
||||
* Only use iterative resolving. This makes {@code ReliableDnsClient} behave like a {@link IterativeDnsClient}.
|
||||
*/
|
||||
iterativeOnly,
|
||||
}
|
||||
|
||||
private final IterativeDnsClient recursiveDnsClient;
|
||||
private final DnsClient dnsClient;
|
||||
|
||||
private Mode mode = Mode.recursiveWithIterativeFallback;
|
||||
|
||||
public ReliableDnsClient(DnsCache dnsCache) {
|
||||
super(dnsCache);
|
||||
recursiveDnsClient = new IterativeDnsClient(dnsCache) {
|
||||
@Override
|
||||
protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
|
||||
questionMessage = super.newQuestion(questionMessage);
|
||||
return ReliableDnsClient.this.newQuestion(questionMessage);
|
||||
}
|
||||
// TODO: Rename dnsMessage to result.
|
||||
@Override
|
||||
protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) {
|
||||
boolean res = super.isResponseCacheable(q, dnsMessage);
|
||||
return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
|
||||
}
|
||||
};
|
||||
dnsClient = new DnsClient(dnsCache) {
|
||||
@Override
|
||||
protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
|
||||
questionMessage = super.newQuestion(questionMessage);
|
||||
return ReliableDnsClient.this.newQuestion(questionMessage);
|
||||
}
|
||||
// TODO: Rename dnsMessage to result.
|
||||
@Override
|
||||
protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) {
|
||||
boolean res = super.isResponseCacheable(q, dnsMessage);
|
||||
return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public ReliableDnsClient() {
|
||||
this(DEFAULT_CACHE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DnsQueryResult query(DnsMessage.Builder q) throws IOException {
|
||||
DnsQueryResult dnsMessage = null;
|
||||
String unacceptableReason = null;
|
||||
List<IOException> 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 <code>null</code> 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 <code>null</code> 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);
|
||||
}
|
||||
|
||||
}
|
53
src/main/java/org/minidns/iterative/ResolutionState.java
Normal file
53
src/main/java/org/minidns/iterative/ResolutionState.java
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.iterative;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.minidns.dnsmessage.DnsMessage;
|
||||
import org.minidns.dnsmessage.Question;
|
||||
import org.minidns.iterative.IterativeClientException.LoopDetected;
|
||||
import org.minidns.iterative.IterativeClientException.MaxIterativeStepsReached;
|
||||
|
||||
public class ResolutionState {
|
||||
|
||||
private final IterativeDnsClient recursiveDnsClient;
|
||||
private final HashMap<InetAddress, Set<Question>> 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<Question>());
|
||||
} 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--;
|
||||
}
|
||||
|
||||
}
|
121
src/main/java/org/minidns/jul/MiniDnsJul.java
Normal file
121
src/main/java/org/minidns/jul/MiniDnsJul.java
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2015-2024 the original author or authors
|
||||
*
|
||||
* This software is licensed under the Apache License, Version 2.0,
|
||||
* the GNU Lesser General Public License version 2 or later ("LGPL")
|
||||
* and the WTFPL.
|
||||
* You may choose either license to govern your use of this software only
|
||||
* upon the condition that you accept all of the terms of either
|
||||
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
|
||||
*/
|
||||
package org.minidns.jul;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.logging.ConsoleHandler;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogManager;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@SuppressWarnings("DateFormatConstant")
|
||||
public class MiniDnsJul {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(MiniDnsJul.class.getName());
|
||||
|
||||
private static final InputStream LOG_MANAGER_CONFIG = new ByteArrayInputStream((
|
||||
// @formatter:off
|
||||
"org.minidns.level=FINEST" + '\n'
|
||||
).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
private static final DateTimeFormatter LONG_LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("hh:mm:ss.SSS");
|
||||
|
||||
private static final DateTimeFormatter SHORT_LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("mm:ss.SSS");
|
||||
|
||||
private static final Handler CONSOLE_HANDLER = new ConsoleHandler();
|
||||
|
||||
private static boolean shortLog = true;
|
||||
|
||||
static {
|
||||
try {
|
||||
LogManager.getLogManager().readConfiguration(LOG_MANAGER_CONFIG);
|
||||
} catch (SecurityException | IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "Could not apply MiniDNS JUL configuration", e);
|
||||
}
|
||||
|
||||
CONSOLE_HANDLER.setLevel(Level.OFF);
|
||||
CONSOLE_HANDLER.setFormatter(new Formatter() {
|
||||
@Override
|
||||
public String format(LogRecord logRecord) {
|
||||
StringBuilder sb = new StringBuilder(256);
|
||||
|
||||
Instant date = Instant.ofEpochMilli(logRecord.getMillis());
|
||||
String dateString;
|
||||
if (shortLog) {
|
||||
dateString = SHORT_LOG_TIME_FORMAT.format(date);
|
||||
} else {
|
||||
dateString = LONG_LOG_TIME_FORMAT.format(date);
|
||||
}
|
||||
sb.append(dateString).append(' ');
|
||||
|
||||
String level = logRecord.getLevel().toString();
|
||||
if (shortLog) {
|
||||
level = level.substring(0, 1);
|
||||
}
|
||||
sb.append(level).append(' ');
|
||||
|
||||
String loggerName = logRecord.getLoggerName();
|
||||
if (shortLog) {
|
||||
String[] parts = loggerName.split("\\.");
|
||||
loggerName = parts[parts.length > 1 ? parts.length - 1 : 0];
|
||||
}
|
||||
sb.append(loggerName);
|
||||
sb.append(' ').append(logRecord.getSourceMethodName());
|
||||
|
||||
if (shortLog) {
|
||||
sb.append(' ');
|
||||
} else {
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
sb.append(formatMessage(logRecord));
|
||||
if (logRecord.getThrown() != null) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
// CHECKSTYLE:OFF
|
||||
pw.println();
|
||||
logRecord.getThrown().printStackTrace(pw);
|
||||
// CHECKSTYLE:ON
|
||||
pw.close();
|
||||
sb.append(sw);
|
||||
}
|
||||
sb.append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
});
|
||||
Logger.getLogger("org.minidns").addHandler(CONSOLE_HANDLER);
|
||||
}
|
||||
|
||||
public static void enableMiniDnsTrace() {
|
||||
enableMiniDnsTrace(true);
|
||||
}
|
||||
|
||||
public static void enableMiniDnsTrace(boolean shortLog) {
|
||||
MiniDnsJul.shortLog = shortLog;
|
||||
CONSOLE_HANDLER.setLevel(Level.FINEST);
|
||||
}
|
||||
|
||||
public static void disableMiniDnsTrace() {
|
||||
CONSOLE_HANDLER.setLevel(Level.OFF);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue