diff --git a/src/dorkbox/network/dns/DatagramDnsQueryEncoder.java b/src/dorkbox/network/dns/DatagramDnsQueryEncoder.java new file mode 100644 index 00000000..f8b9b853 --- /dev/null +++ b/src/dorkbox/network/dns/DatagramDnsQueryEncoder.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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 dorkbox.network.dns; + +import java.net.InetSocketAddress; +import java.util.List; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.buffer.ByteBuf; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.util.internal.UnstableApi; + +/** + * Encodes an {@link AddressedEnvelope} of {@link DnsQuestion}} into a {@link DatagramPacket}. + */ +@UnstableApi +@ChannelHandler.Sharable +public +class DatagramDnsQueryEncoder extends MessageToMessageEncoder> { + + private final int maxPayloadSize; + + /** + * Creates a new encoder + */ + public + DatagramDnsQueryEncoder(int maxPayloadSize) { + this.maxPayloadSize = maxPayloadSize; + } + + @Override + protected + void encode(ChannelHandlerContext ctx, AddressedEnvelope in, List out) throws Exception { + + final InetSocketAddress recipient = in.recipient(); + final DnsMessage query = in.content(); + final ByteBuf buf = ctx.alloc() + .ioBuffer(maxPayloadSize); + + boolean success = false; + try { + DnsOutput dnsOutput = new DnsOutput(buf); + query.toWire(dnsOutput); + success = true; + } finally { + if (!success) { + buf.release(); + } + } + + out.add(new DatagramPacket(buf, recipient, null)); + } + + @Override + public + void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { + cause.printStackTrace(); + } +} diff --git a/src/dorkbox/network/dns/DatagramDnsResponseDecoder.java b/src/dorkbox/network/dns/DatagramDnsResponseDecoder.java new file mode 100644 index 00000000..1a55938a --- /dev/null +++ b/src/dorkbox/network/dns/DatagramDnsResponseDecoder.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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 dorkbox.network.dns; + +import java.util.List; + +import dorkbox.network.dns.exceptions.WireParseException; +import dorkbox.network.dns.records.Header; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.internal.UnstableApi; + +/** + * Decodes a {@link DatagramPacket} into a {@link DnsResponse}. + */ +@UnstableApi +@ChannelHandler.Sharable +public class DatagramDnsResponseDecoder extends MessageToMessageDecoder { + + /** + * Creates a new DNS Response decoder + */ + public DatagramDnsResponseDecoder() { + } + + @Override + protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List out) throws Exception { + final ByteBuf buf = packet.content(); + + // Check that the response is long enough. + if (buf.readableBytes() < Header.LENGTH) { + throw new WireParseException("invalid DNS header - " + "too short"); + } + + DnsInput dnsInput = new DnsInput(buf); + DnsResponse dnsMessage = new DnsResponse(packet.sender(), packet.recipient(), dnsInput); + out.add(dnsMessage); + } +} diff --git a/src/dorkbox/network/dns/DnsQuestion.java b/src/dorkbox/network/dns/DnsQuestion.java new file mode 100644 index 00000000..87fd67cc --- /dev/null +++ b/src/dorkbox/network/dns/DnsQuestion.java @@ -0,0 +1,228 @@ +package dorkbox.network.dns; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Locale; + +import dorkbox.network.dns.constants.DnsClass; +import dorkbox.network.dns.constants.DnsOpCode; +import dorkbox.network.dns.constants.DnsRecordType; +import dorkbox.network.dns.constants.DnsSection; +import dorkbox.network.dns.constants.Flags; +import dorkbox.network.dns.records.DnsMessage; +import dorkbox.network.dns.records.DnsRecord; +import io.netty.channel.AddressedEnvelope; +import io.netty.util.internal.StringUtil; + +/** + * + */ +public +class DnsQuestion extends DnsMessage implements AddressedEnvelope { + private InetSocketAddress recipient; + private final boolean isResolveQuestion; + + /** + * Creates a new instance. + * + * @param isResolveQuestion true if it's a resolve question, which means we ALSO are going to keep resolving names until we get an IP + * address. + */ + private + DnsQuestion(final boolean isResolveQuestion) { + this.isResolveQuestion = isResolveQuestion; + this.recipient = null; + } + + public + boolean isResolveQuestion() { + return isResolveQuestion; + } + + + + public static + DnsQuestion newResolveQuestion(final String inetHost, final int type, final boolean isRecursionDesired) { + return newQuestion(inetHost, type, isRecursionDesired, true); + } + + public static + DnsQuestion newQuery(final String inetHost, final int type, final boolean isRecursionDesired) { + return newQuestion(inetHost, type, isRecursionDesired, false); + } + + + + private static + DnsQuestion newQuestion(final String inetHost, final int type, final boolean isRecursionDesired, boolean isResolveQuestion) { + + // Convert to ASCII which will also check that the length is not too big. + // See: + // - https://github.com/netty/netty/issues/4937 + // - https://github.com/netty/netty/issues/4935 + String hostname = hostname(checkNotNull(inetHost, "hostname")); + + if (hostname == null) { + return null; + } + + hostname = hostname.toLowerCase(Locale.US); + + if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { + // resolving a hostname -> ip address, the hostname MUST end in a dot + hostname = appendTrailingDot(hostname); + } + + try { + DnsRecord questionRecord = DnsRecord.newRecord(Name.fromString(hostname), type, DnsClass.IN); + DnsQuestion question = new DnsQuestion(isResolveQuestion); + question.getHeader() + .setOpcode(DnsOpCode.QUERY); + + if (isRecursionDesired) { + question.getHeader() + .setFlag(Flags.RD); + } + question.addRecord(questionRecord, DnsSection.QUESTION); + + // keep the question around so we can compare the response to it. + question.retain(); + + return question; + } catch (Exception e) { + // Name.fromString may throw a TextParseException if it fails to parse + } + + return null; + } + + public static + String hostname(String inetHost) { + try { + String hostname = java.net.IDN.toASCII(inetHost); + + // Check for http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6894622 + if (StringUtil.endsWith(inetHost, '.') && !StringUtil.endsWith(hostname, '.')) { + return hostname + '.'; + } + + return hostname; + } catch (Exception e) { + // java.net.IDN.toASCII(...) may throw an IllegalArgumentException if it fails to parse the hostname + } + + return null; + } + + private static + String appendTrailingDot(String hostname) { + if (!StringUtil.endsWith(hostname, '.')) { + return hostname + '.'; + } + return hostname; + } + + public + void init(int id, InetSocketAddress recipient) { + getHeader().setID(id); + this.recipient = recipient; + } + + @Override + public + DnsQuestion content() { + return this; + } + + @Override + public + InetSocketAddress sender() { + return null; + } + + @Override + public + InetSocketAddress recipient() { + return recipient; + } + + + + @Override + public + DnsQuestion touch() { + return (DnsQuestion) super.touch(); + } + + @Override + public + DnsQuestion touch(Object hint) { + return (DnsQuestion) super.touch(hint); + } + + @Override + public + DnsQuestion retain() { + return (DnsQuestion) super.retain(); + } + + @Override + public + DnsQuestion retain(int increment) { + return (DnsQuestion) super.retain(increment); + } + + @Override + public + boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!super.equals(obj)) { + return false; + } + + if (!(obj instanceof AddressedEnvelope)) { + return false; + } + + @SuppressWarnings("unchecked") + final AddressedEnvelope that = (AddressedEnvelope) obj; + if (sender() == null) { + if (that.sender() != null) { + return false; + } + } + else if (!sender().equals(that.sender())) { + return false; + } + + if (recipient() == null) { + if (that.recipient() != null) { + return false; + } + } + else if (!recipient().equals(that.recipient())) { + return false; + } + + return true; + } + + @Override + public + int hashCode() { + int hashCode = super.hashCode(); + if (sender() != null) { + hashCode = hashCode * 31 + sender().hashCode(); + } + if (recipient() != null) { + hashCode = hashCode * 31 + recipient().hashCode(); + } + return hashCode; + } +} + diff --git a/src/dorkbox/network/dns/DnsResponse.java b/src/dorkbox/network/dns/DnsResponse.java new file mode 100644 index 00000000..6236af23 --- /dev/null +++ b/src/dorkbox/network/dns/DnsResponse.java @@ -0,0 +1,142 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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 dorkbox.network.dns; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import dorkbox.network.dns.records.DnsMessage; +import io.netty.channel.AddressedEnvelope; +import io.netty.util.internal.UnstableApi; + +/** + * A {@link DnsResponse} implementation for UDP/IP. + */ +@UnstableApi +public class DnsResponse extends DnsMessage implements AddressedEnvelope { + + private final InetSocketAddress sender; + private final InetSocketAddress recipient; + + /** + * Creates a new instance. + * + * @param sender the address of the sender + * @param recipient the address of the recipient + */ + public + DnsResponse(InetSocketAddress sender, InetSocketAddress recipient, final DnsInput dnsInput) throws IOException { + super(dnsInput); + + if (recipient == null && sender == null) { + throw new NullPointerException("recipient and sender"); + } + + this.sender = sender; + this.recipient = recipient; + } + + @Override + public + DnsResponse content() { + return this; + } + + @Override + public InetSocketAddress sender() { + return sender; + } + + @Override + public InetSocketAddress recipient() { + return recipient; + } + + + + + @Override + public + DnsResponse touch() { + return (DnsResponse) super.touch(); + } + + @Override + public + DnsResponse touch(Object hint) { + return (DnsResponse) super.touch(hint); + } + + @Override + public + DnsResponse retain() { + return (DnsResponse) super.retain(); + } + + @Override + public + DnsResponse retain(int increment) { + return (DnsResponse) super.retain(increment); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!super.equals(obj)) { + return false; + } + + if (!(obj instanceof AddressedEnvelope)) { + return false; + } + + @SuppressWarnings("unchecked") + final AddressedEnvelope that = (AddressedEnvelope) obj; + if (sender() == null) { + if (that.sender() != null) { + return false; + } + } else if (!sender().equals(that.sender())) { + return false; + } + + if (recipient() == null) { + if (that.recipient() != null) { + return false; + } + } else if (!recipient().equals(that.recipient())) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + if (sender() != null) { + hashCode = hashCode * 31 + sender().hashCode(); + } + if (recipient() != null) { + hashCode = hashCode * 31 + recipient().hashCode(); + } + return hashCode; + } +}