diff --git a/core/src/main/java/com/google/cloud/sql/core/DnsResolver.java b/core/src/main/java/com/google/cloud/sql/core/DnsResolver.java new file mode 100644 index 000000000..ccd867d07 --- /dev/null +++ b/core/src/main/java/com/google/cloud/sql/core/DnsResolver.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.core; + +import java.util.Collection; +import javax.naming.NameNotFoundException; + +interface DnsResolver { + Collection resolveSrv(String domainName) throws NameNotFoundException; +} diff --git a/core/src/main/java/com/google/cloud/sql/core/DnsSrvRecord.java b/core/src/main/java/com/google/cloud/sql/core/DnsSrvRecord.java new file mode 100644 index 000000000..bf53b1b7b --- /dev/null +++ b/core/src/main/java/com/google/cloud/sql/core/DnsSrvRecord.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.core; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** This represents the value of an SRV DNS Record. */ +public class DnsSrvRecord { + private static final Pattern RECORD_FORMAT = Pattern.compile("(\\d+) +(\\d+) +(\\d+) +(.*)"); + private final int priority; + private final int weight; + private final int port; + private final String target; + + DnsSrvRecord(String record) { + Matcher m = RECORD_FORMAT.matcher(record); + if (!m.find()) { + throw new IllegalArgumentException("Malformed SRV record: " + record); + } + + this.priority = Integer.parseInt(m.group(1)); + this.weight = Integer.parseInt(m.group(2)); + this.port = Integer.parseInt(m.group(3)); + this.target = m.group(4); + } + + public int getPriority() { + return priority; + } + + public int getWeight() { + return weight; + } + + public int getPort() { + return port; + } + + public String getTarget() { + return target; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DnsSrvRecord)) { + return false; + } + DnsSrvRecord that = (DnsSrvRecord) o; + return priority == that.priority + && weight == that.weight + && port == that.port + && target.equals(that.target); + } + + @Override + public int hashCode() { + return Objects.hash(priority, weight, port, target); + } +} diff --git a/core/src/main/java/com/google/cloud/sql/core/JndiDnsResolver.java b/core/src/main/java/com/google/cloud/sql/core/JndiDnsResolver.java new file mode 100644 index 000000000..1342332ac --- /dev/null +++ b/core/src/main/java/com/google/cloud/sql/core/JndiDnsResolver.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.stream.Collectors; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.InitialDirContext; + +/** Implements DnsResolver using the Java JNDI built-in DNS directory. */ +class JndiDnsResolver implements DnsResolver { + private final String jndiPrefix; + + /** Creates a resolver using the system DNS settings. */ + JndiDnsResolver() { + this.jndiPrefix = "dns:"; + } + + /** + * Creates a DNS resolver that uses a specific DNS server. + * + * @param dnsServer the DNS server hostname + * @param port the DNS server port (DNS servers usually use port 53) + */ + JndiDnsResolver(String dnsServer, int port) { + this.jndiPrefix = "dns://" + dnsServer + ":" + port + "/"; + } + + /** + * Returns DNS records for a domain name, sorted by priority, then target alphabetically. + * + * @param domainName the domain name to lookup + * @return the list of record + * @throws javax.naming.NameNotFoundException when the domain name did not resolve. + */ + @Override + public Collection resolveSrv(String domainName) + throws javax.naming.NameNotFoundException { + try { + // Notice: This is old Java 1.2 style code. It uses the ancient JNDI DNS Provider api. + // See https://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-dns.html + Attribute attr = + new InitialDirContext() + .getAttributes(jndiPrefix + domainName, new String[] {"SRV"}) + .get("SRV"); + // attr.getAll() returns a Vector containing strings, one for each record returned by dns. + return Collections.list(attr.getAll()).stream() + .map((Object v) -> new DnsSrvRecord((String) v)) + .sorted(Comparator.comparing(DnsSrvRecord::getPriority)) + .collect(Collectors.toList()); + } catch (NameNotFoundException e) { + throw e; + } catch (NamingException e) { + throw new RuntimeException("Unable to look up domain name " + domainName, e); + } + } +} diff --git a/core/src/test/java/com/google/cloud/sql/core/DnsSrvRecordTest.java b/core/src/test/java/com/google/cloud/sql/core/DnsSrvRecordTest.java new file mode 100644 index 000000000..44c2cc53d --- /dev/null +++ b/core/src/test/java/com/google/cloud/sql/core/DnsSrvRecordTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.sql.core; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class DnsSrvRecordTest { + + @Test + public void testValidSrvRecord() { + DnsSrvRecord r = new DnsSrvRecord("0 10 3307 sample-project:us-central1:my-database."); + assertThat(r.getTarget()).isEqualTo("sample-project:us-central1:my-database."); + assertThat(r.getPort()).isEqualTo(3307); + assertThat(r.getWeight()).isEqualTo(10); + assertThat(r.getPriority()).isEqualTo(0); + } + + @Test + public void testInvalidSrvRecordThrows() { + assertThrows(IllegalArgumentException.class, () -> new DnsSrvRecord("bad record format")); + } +}