com.couchbase.client.core.deps.org.xbill.DNS.SVCBBase Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-io Show documentation
Show all versions of core-io Show documentation
The official Couchbase JVM Core IO Library
// SPDX-License-Identifier: BSD-3-Clause
package com.couchbase.client.core.deps.org.xbill.DNS;
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.Base64;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Implements common functionality for SVCB and HTTPS records
*
* @see draft-ietf-dnsop-svcb-https
* @since 3.3
*/
public abstract class SVCBBase extends Record {
protected int svcPriority;
protected Name targetName;
protected final Map svcParams;
public static final int MANDATORY = 0;
public static final int ALPN = 1;
public static final int NO_DEFAULT_ALPN = 2;
public static final int PORT = 3;
public static final int IPV4HINT = 4;
public static final int ECH = 5;
public static final int IPV6HINT = 6;
/** @deprecated use {@link #ECH} */
@Deprecated public static final int ECHCONFIG = 5;
protected SVCBBase() {
svcParams = new TreeMap<>();
}
protected SVCBBase(Name name, int type, int dclass, long ttl) {
super(name, type, dclass, ttl);
svcParams = new TreeMap<>();
}
protected SVCBBase(
Name name,
int type,
int dclass,
long ttl,
int priority,
Name domain,
List params) {
super(name, type, dclass, ttl);
svcPriority = priority;
targetName = domain;
svcParams = new TreeMap<>();
for (ParameterBase param : params) {
if (svcParams.containsKey(param.getKey())) {
throw new IllegalArgumentException("Duplicate SvcParam for key " + param.getKey());
}
svcParams.put(param.getKey(), param);
}
}
public int getSvcPriority() {
return svcPriority;
}
public Name getTargetName() {
return targetName;
}
public Set getSvcParamKeys() {
return svcParams.keySet();
}
public ParameterBase getSvcParamValue(int key) {
return svcParams.get(key);
}
private static class ParameterMnemonic extends Mnemonic {
private final HashMap> factories;
public ParameterMnemonic() {
super("SVCB/HTTPS Parameters", Mnemonic.CASE_LOWER);
setPrefix("key");
setNumericAllowed(true);
setMaximum(0xFFFF);
factories = new HashMap<>();
}
public void add(int val, String str, Supplier factory) {
super.add(val, str);
factories.put(val, factory);
}
public Supplier getFactory(int val) {
return factories.get(val);
}
}
private static final ParameterMnemonic parameters = new ParameterMnemonic();
static {
parameters.add(MANDATORY, "mandatory", ParameterMandatory::new);
parameters.add(ALPN, "alpn", ParameterAlpn::new);
parameters.add(NO_DEFAULT_ALPN, "no-default-alpn", ParameterNoDefaultAlpn::new);
parameters.add(PORT, "port", ParameterPort::new);
parameters.add(IPV4HINT, "ipv4hint", ParameterIpv4Hint::new);
parameters.add(ECH, "ech", ParameterEch::new);
parameters.add(IPV6HINT, "ipv6hint", ParameterIpv6Hint::new);
/* Support obsolete echconfig name as an alias for ech */
parameters.addAlias(ECH, "echconfig");
}
public abstract static class ParameterBase {
public ParameterBase() {}
public abstract int getKey();
public abstract void fromWire(byte[] bytes) throws IOException;
public abstract void fromString(String string) throws IOException;
public abstract byte[] toWire();
@Override
public abstract String toString();
// Split string on commas, but not if comma is escaped with a '\'
public static String[] splitStringWithEscapedCommas(String string) {
return string.split("(? values;
public ParameterMandatory() {
super();
values = new ArrayList<>();
}
public ParameterMandatory(List values) {
super();
this.values = values;
}
public List getValues() {
return values;
}
@Override
public int getKey() {
return MANDATORY;
}
@Override
public void fromWire(byte[] bytes) throws IOException {
values.clear();
DNSInput in = new DNSInput(bytes);
while (in.remaining() >= 2) {
int key = in.readU16();
values.add(key);
}
if (in.remaining() > 0) {
throw new WireParseException("Unexpected number of bytes in mandatory parameter");
}
}
@Override
public void fromString(String string) throws TextParseException {
values.clear();
if (string == null || string.isEmpty()) {
throw new TextParseException("Non-empty list must be specified for mandatory");
}
for (String str : splitStringWithEscapedCommas(string)) {
int key = parameters.getValue(str);
if (key == MANDATORY) {
throw new TextParseException("Key mandatory must not appear in its own list");
}
if (values.contains(key)) {
throw new TextParseException("Duplicate key " + str + " not allowed in mandatory list");
}
values.add(key);
}
}
@Override
public byte[] toWire() {
DNSOutput out = new DNSOutput();
for (Integer val : values) {
out.writeU16(val);
}
return out.toByteArray();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Integer val : values) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(parameters.getText(val));
}
return sb.toString();
}
}
public static class ParameterAlpn extends ParameterBase {
private final List values;
public ParameterAlpn() {
super();
values = new ArrayList<>();
}
public ParameterAlpn(List values) throws TextParseException {
super();
this.values = new ArrayList<>();
for (String str : values) {
this.values.add(byteArrayFromString(str));
}
}
public List getValues() {
List values = new ArrayList<>();
for (byte[] b : this.values) {
values.add(byteArrayToString(b, false));
}
return values;
}
@Override
public int getKey() {
return ALPN;
}
@Override
public void fromWire(byte[] bytes) throws IOException {
values.clear();
DNSInput in = new DNSInput(bytes);
while (in.remaining() > 0) {
byte[] b = in.readCountedString();
values.add(b);
}
}
@Override
public void fromString(String string) throws TextParseException {
values.clear();
if (string == null || string.isEmpty()) {
throw new TextParseException("Non-empty list must be specified for alpn");
}
for (String str : splitStringWithEscapedCommas(string)) {
values.add(byteArrayFromString(str));
}
}
@Override
public byte[] toWire() {
DNSOutput out = new DNSOutput();
for (byte[] b : values) {
out.writeCountedString(b);
}
return out.toByteArray();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (byte[] b : values) {
if (sb.length() > 0) {
sb.append(",");
}
String str = byteArrayToString(b, false);
str = str.replaceAll(",", "\\\\,");
sb.append(str);
}
return sb.toString();
}
}
public static class ParameterNoDefaultAlpn extends ParameterBase {
public ParameterNoDefaultAlpn() {
super();
}
@Override
public int getKey() {
return NO_DEFAULT_ALPN;
}
@Override
public void fromWire(byte[] bytes) throws WireParseException {
if (bytes.length > 0) {
throw new WireParseException("No value can be specified for no-default-alpn");
}
}
@Override
public void fromString(String string) throws TextParseException {
if (string != null && !string.isEmpty()) {
throw new TextParseException("No value can be specified for no-default-alpn");
}
}
@Override
public byte[] toWire() {
return new byte[0];
}
@Override
public String toString() {
return "";
}
}
public static class ParameterPort extends ParameterBase {
private int port;
public ParameterPort() {
super();
}
public ParameterPort(int port) {
super();
this.port = port;
}
public int getPort() {
return port;
}
@Override
public int getKey() {
return PORT;
}
@Override
public void fromWire(byte[] bytes) throws IOException {
DNSInput in = new DNSInput(bytes);
port = in.readU16();
if (in.remaining() > 0) {
throw new WireParseException("Unexpected number of bytes in port parameter");
}
}
@Override
public void fromString(String string) throws TextParseException {
if (string == null || string.isEmpty()) {
throw new TextParseException("Integer value must be specified for port");
}
port = Integer.parseInt(string);
}
@Override
public byte[] toWire() {
DNSOutput out = new DNSOutput();
out.writeU16(port);
return out.toByteArray();
}
@Override
public String toString() {
return Integer.toString(port);
}
}
public static class ParameterIpv4Hint extends ParameterBase {
private final List addresses;
public ParameterIpv4Hint() {
super();
addresses = new ArrayList<>();
}
public ParameterIpv4Hint(List addresses) {
super();
this.addresses =
addresses.stream().map(Inet4Address::getAddress).collect(Collectors.toList());
}
public List getAddresses() throws UnknownHostException {
List addresses = new LinkedList<>();
for (byte[] bytes : this.addresses) {
InetAddress address = InetAddress.getByAddress(bytes);
if (address instanceof Inet4Address) {
addresses.add((Inet4Address) address);
}
}
return addresses;
}
@Override
public int getKey() {
return IPV4HINT;
}
@Override
public void fromWire(byte[] bytes) throws IOException {
addresses.clear();
DNSInput in = new DNSInput(bytes);
while (in.remaining() >= 4) {
addresses.add(in.readByteArray(4));
}
if (in.remaining() > 0) {
throw new WireParseException("Unexpected number of bytes in ipv4hint parameter");
}
}
@Override
public void fromString(String string) throws IOException {
addresses.clear();
if (string == null || string.isEmpty()) {
throw new TextParseException("Non-empty IPv4 list must be specified for ipv4hint");
}
for (String str : string.split(",")) {
byte[] address = Address.toByteArray(str, Address.IPv4);
if (address == null) {
throw new TextParseException("Invalid ipv4hint value '" + str + "'");
}
addresses.add(address);
}
}
@Override
public byte[] toWire() {
DNSOutput out = new DNSOutput();
for (byte[] b : addresses) {
out.writeByteArray(b);
}
return out.toByteArray();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (byte[] b : addresses) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(Address.toDottedQuad(b));
}
return sb.toString();
}
}
public static class ParameterEch extends ParameterBase {
private byte[] data;
public ParameterEch() {
super();
}
public ParameterEch(byte[] data) {
super();
this.data = data;
}
public byte[] getData() {
return data;
}
@Override
public int getKey() {
return ECH;
}
@Override
public void fromWire(byte[] bytes) {
data = bytes;
}
@Override
public void fromString(String string) throws TextParseException {
if (string == null || string.isEmpty()) {
throw new TextParseException("Non-empty base64 value must be specified for ech");
}
data = Base64.getDecoder().decode(string);
}
@Override
public byte[] toWire() {
return data;
}
@Override
public String toString() {
return Base64.getEncoder().encodeToString(data);
}
}
/** @deprecated use {@link ParameterEch} */
@Deprecated
public static class ParameterEchConfig extends ParameterBase {
private byte[] data;
public ParameterEchConfig() {
super();
}
public ParameterEchConfig(byte[] data) {
super();
this.data = data;
}
public byte[] getData() {
return data;
}
@Override
public int getKey() {
return ECHCONFIG;
}
@Override
public void fromWire(byte[] bytes) {
data = bytes;
}
@Override
public void fromString(String string) throws TextParseException {
if (string == null || string.isEmpty()) {
throw new TextParseException("Non-empty base64 value must be specified for echconfig");
}
data = Base64.getDecoder().decode(string);
}
@Override
public byte[] toWire() {
return data;
}
@Override
public String toString() {
return Base64.getEncoder().encodeToString(data);
}
}
public static class ParameterIpv6Hint extends ParameterBase {
private final List addresses;
public ParameterIpv6Hint() {
super();
addresses = new ArrayList<>();
}
public ParameterIpv6Hint(List addresses) {
super();
this.addresses =
addresses.stream().map(Inet6Address::getAddress).collect(Collectors.toList());
}
public List getAddresses() throws UnknownHostException {
List addresses = new LinkedList<>();
for (byte[] bytes : this.addresses) {
InetAddress address = InetAddress.getByAddress(bytes);
if (address instanceof Inet6Address) {
addresses.add((Inet6Address) address);
}
}
return addresses;
}
@Override
public int getKey() {
return IPV6HINT;
}
@Override
public void fromWire(byte[] bytes) throws IOException {
addresses.clear();
DNSInput in = new DNSInput(bytes);
while (in.remaining() >= 16) {
addresses.add(in.readByteArray(16));
}
if (in.remaining() > 0) {
throw new WireParseException("Unexpected number of bytes in ipv6hint parameter");
}
}
@Override
public void fromString(String string) throws IOException {
addresses.clear();
if (string == null || string.isEmpty()) {
throw new TextParseException("Non-empty IPv6 list must be specified for ipv6hint");
}
for (String str : string.split(",")) {
byte[] address = Address.toByteArray(str, Address.IPv6);
if (address == null) {
throw new TextParseException("Invalid ipv6hint value '" + str + "'");
}
addresses.add(address);
}
}
@Override
public byte[] toWire() {
DNSOutput out = new DNSOutput();
for (byte[] b : addresses) {
out.writeByteArray(b);
}
return out.toByteArray();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (byte[] b : addresses) {
if (sb.length() > 0) {
sb.append(",");
}
try {
InetAddress addr = InetAddress.getByAddress(null, b);
sb.append(addr.getHostAddress());
} catch (UnknownHostException e) {
// should not happen, but returning null and throwing is bad
return e.getMessage();
}
}
return sb.toString();
}
}
public static class ParameterUnknown extends ParameterBase {
private final int key;
private byte[] value;
public ParameterUnknown(int key) {
super();
this.key = key;
this.value = new byte[0];
}
public ParameterUnknown(int key, byte[] value) {
super();
this.key = key;
this.value = value;
}
public byte[] getValue() {
return value;
}
@Override
public int getKey() {
return key;
}
@Override
public void fromWire(byte[] bytes) {
value = bytes;
}
@Override
public void fromString(String string) throws IOException {
if (string == null || string.isEmpty()) {
value = new byte[0];
} else {
value = byteArrayFromString(string);
}
}
@Override
public byte[] toWire() {
return value;
}
@Override
public String toString() {
return byteArrayToString(value, false);
}
}
protected boolean checkMandatoryParams() {
ParameterMandatory param = (ParameterMandatory) getSvcParamValue(MANDATORY);
if (param != null) {
for (int key : param.values) {
if (getSvcParamValue(key) == null) {
return false;
}
}
}
return true;
}
@Override
protected void rrFromWire(DNSInput in) throws IOException {
svcPriority = in.readU16();
targetName = new Name(in);
svcParams.clear();
while (in.remaining() >= 4) {
int key = in.readU16();
int length = in.readU16();
byte[] value = in.readByteArray(length);
ParameterBase param;
Supplier factory = parameters.getFactory(key);
if (factory != null) {
param = factory.get();
} else {
param = new ParameterUnknown(key);
}
param.fromWire(value);
svcParams.put(key, param);
}
if (in.remaining() > 0) {
throw new WireParseException("Record had unexpected number of bytes");
}
if (!checkMandatoryParams()) {
throw new WireParseException("Not all mandatory SvcParams are specified");
}
}
@Override
protected String rrToString() {
StringBuilder sb = new StringBuilder();
sb.append(svcPriority);
sb.append(" ");
sb.append(targetName);
for (Integer key : svcParams.keySet()) {
sb.append(" ");
sb.append(parameters.getText(key));
ParameterBase param = svcParams.get(key);
String value = param.toString();
if (value != null && !value.isEmpty()) {
sb.append("=");
sb.append(value);
}
}
return sb.toString();
}
@Override
protected void rdataFromString(Tokenizer st, Name origin) throws IOException {
svcPriority = st.getUInt16();
targetName = st.getName(origin);
svcParams.clear();
while (true) {
String keyStr = null;
String valueStr = null;
Tokenizer.Token t = st.get();
if (!t.isString()) {
break;
}
int indexOfEquals = t.value().indexOf('=');
if (indexOfEquals == -1) {
// No "=" is key with no value case, leave value string as null
keyStr = t.value();
} else if (indexOfEquals == t.value().length() - 1) {
// Ends with "=" means the next token is quoted string with the value
keyStr = t.value().substring(0, indexOfEquals);
Tokenizer.Token valueToken = st.get();
if (!valueToken.isString()) {
throw new TextParseException("Expected value for parameter key '" + keyStr + "'");
}
valueStr = valueToken.value();
} else if (indexOfEquals > 0) {
// If "=" is in the middle then need to split the key and value from this token
keyStr = t.value().substring(0, indexOfEquals);
valueStr = t.value().substring(indexOfEquals + 1);
} else {
throw new TextParseException("Expected valid parameter key=value for '" + t.value() + "'");
}
ParameterBase param;
int key = parameters.getValue(keyStr);
if (key == -1) {
throw new TextParseException("Expected a valid parameter key for '" + keyStr + "'");
}
if (svcParams.containsKey(key)) {
throw new TextParseException("Duplicate parameter key for '" + keyStr + "'");
}
Supplier factory = parameters.getFactory(key);
if (factory != null) {
param = factory.get();
} else {
param = new ParameterUnknown(key);
}
param.fromString(valueStr);
svcParams.put(key, param);
}
st.unget();
if (svcPriority == 0 && !svcParams.isEmpty()) {
throw new TextParseException("No parameter values allowed for AliasMode");
}
if (!checkMandatoryParams()) {
throw new TextParseException("Not all mandatory SvcParams are specified");
}
}
@Override
protected void rrToWire(DNSOutput out, Compression c, boolean canonical) {
out.writeU16(svcPriority);
targetName.toWire(out, null, canonical);
for (Integer key : svcParams.keySet()) {
out.writeU16(key);
ParameterBase param = svcParams.get(key);
byte[] value = param.toWire();
out.writeU16(value.length);
out.writeByteArray(value);
}
}
}