diff options
Diffstat (limited to 'libs/minidns/src/main/java')
15 files changed, 2034 insertions, 0 deletions
diff --git a/libs/minidns/src/main/java/de/measite/minidns/Client.java b/libs/minidns/src/main/java/de/measite/minidns/Client.java new file mode 100644 index 00000000..827aa772 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/Client.java @@ -0,0 +1,323 @@ +package de.measite.minidns; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.lang.reflect.Method; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; + +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; + +/** + * 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 Client { + + private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + + /** + * The internal random class for sequence generation. + */ + protected Random random; + + /** + * The buffer size for dns replies. + */ + protected int bufferSize = 1500; + + /** + * DNS timeout. + */ + protected int timeout = 5000; + + /** + * The internal DNS cache. + */ + protected DNSCache cache; + + /** + * Create a new DNS client with the given DNS cache. + * @param cache The backend DNS cache. + */ + public Client(DNSCache cache) { + try { + random = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e1) { + random = new SecureRandom(); + } + this.cache = cache; + } + + /** + * Create a new DNS client. + */ + public Client() { + this(null); + } + + /** + * 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 host The DNS server host. + * @param port The DNS server port. + * @return The response (or null on timeout / failure). + * @throws IOException On IO Errors. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz, String host, int port) + throws IOException + { + Question q = new Question(name, type, clazz); + return query(q, host, 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 host The DNS server host. + * @return The response (or null on timeout / failure). + * @throws IOException On IO Errors. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz, String host) + throws IOException + { + Question q = new Question(name, type, clazz); + return query(q, host); + } + + /** + * Query the system 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). + * @return The response (or null on timeout/error). + * @return The DNSMessage reply or null. + */ + public DNSMessage query(String name, TYPE type, CLASS clazz) + { + Question q = new Question(name, type, clazz); + return query(q); + } + + /** + * Query a specific server for one entry. + * @param q The question section of the DNS query. + * @param host The dns server host. + * @return The response (or null on timeout/error). + * @throws IOException On IOErrors. + */ + public DNSMessage query(Question q, String host) throws IOException { + return query(q, host, 53); + } + + /** + * Query a specific server for one entry. + * @param q The question section of the DNS query. + * @param host The dns server host. + * @param port the dns port. + * @return The response (or null on timeout/error). + * @throws IOException On IOErrors. + */ + public DNSMessage query(Question q, String host, int port) throws IOException { + DNSMessage dnsMessage = (cache == null) ? null : cache.get(q); + if (dnsMessage != null) { + return dnsMessage; + } + DNSMessage message = new DNSMessage(); + message.setQuestions(new Question[]{q}); + message.setRecursionDesired(true); + message.setId(random.nextInt()); + byte[] buf = message.toArray(); + try (DatagramSocket socket = new DatagramSocket()) { + DatagramPacket packet = new DatagramPacket(buf, buf.length, + InetAddress.getByName(host), port); + socket.setSoTimeout(timeout); + socket.send(packet); + packet = new DatagramPacket(new byte[bufferSize], bufferSize); + socket.receive(packet); + dnsMessage = DNSMessage.parse(packet.getData()); + if (dnsMessage.getId() != message.getId()) { + return null; + } + for (Record record : dnsMessage.getAnswers()) { + if (record.isAnswer(q)) { + if (cache != null) { + cache.put(q, dnsMessage); + } + break; + } + } + return dnsMessage; + } + } + + /** + * Query the system DNS server for one entry. + * @param q The question section of the DNS query. + * @return The response (or null on timeout/error). + */ + public DNSMessage query(Question q) { + // 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). + DNSMessage message = cache.get(q); + if (message != null) { + return message; + } + String dnsServer[] = findDNS(); + for (String dns : dnsServer) { + try { + message = query(q, dns); + if (message == null) { + continue; + } + if (message.getResponseCode() != + DNSMessage.RESPONSE_CODE.NO_ERROR) { + continue; + } + for (Record record: message.getAnswers()) { + if (record.isAnswer(q)) { + return message; + } + } + } catch (IOException ioe) { + LOGGER.log(Level.FINE, "IOException in query", ioe); + } + } + return null; + } + + /** + * Retrieve a list of currently configured DNS servers. + * @return The server array. + */ + public String[] findDNS() { + String[] result = findDNSByReflection(); + if (result != null) { + LOGGER.fine("Got DNS servers via reflection: " + Arrays.toString(result)); + return result; + } + + result = findDNSByExec(); + if (result != null) { + LOGGER.fine("Got DNS servers via exec: " + Arrays.toString(result)); + return result; + } + + // fallback for ipv4 and ipv6 connectivity + // see https://developers.google.com/speed/public-dns/docs/using + LOGGER.fine("No DNS found? Using fallback [8.8.8.8, [2001:4860:4860::8888]]"); + + return new String[]{"8.8.8.8", "[2001:4860:4860::8888]"}; + } + + /** + * Try to retrieve the list of dns server by executing getprop. + * @return Array of servers, or null on failure. + */ + protected String[] findDNSByExec() { + try { + Process process = Runtime.getRuntime().exec("getprop"); + InputStream inputStream = process.getInputStream(); + LineNumberReader lnr = new LineNumberReader( + new InputStreamReader(inputStream)); + String line = null; + HashSet<String> server = new HashSet<String>(6); + while ((line = lnr.readLine()) != null) { + int split = line.indexOf("]: ["); + if (split == -1) { + continue; + } + String property = line.substring(1, split); + String value = line.substring(split + 4, line.length() - 1); + 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); + } + } + if (server.size() > 0) { + return server.toArray(new String[server.size()]); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e); + } + return null; + } + + /** + * Try to retrieve the list of dns server by calling SystemProperties. + * @return Array of servers, or null on failure. + */ + protected String[] findDNSByReflection() { + try { + Class<?> SystemProperties = + Class.forName("android.os.SystemProperties"); + Method method = SystemProperties.getMethod("get", + new Class[] { String.class }); + + ArrayList<String> servers = new ArrayList<String>(5); + + for (String propKey : new String[] { + "net.dns1", "net.dns2", "net.dns3", "net.dns4"}) { + + String value = (String)method.invoke(null, propKey); + + if (value == null) continue; + if (value.length() == 0) continue; + if (servers.contains(value)) continue; + + InetAddress ip = InetAddress.getByName(value); + + 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.toArray(new String[servers.size()]); + } + } catch (Exception e) { + // we might trigger some problems this way + LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e); + } + return null; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/DNSCache.java b/libs/minidns/src/main/java/de/measite/minidns/DNSCache.java new file mode 100644 index 00000000..14a3a776 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/DNSCache.java @@ -0,0 +1,23 @@ +package de.measite.minidns; + +/** + * Cache for DNS Entries. Implementations must be thread safe. + */ +public interface DNSCache { + + /** + * Add an an dns answer/response for a given dns question. Implementations + * should honor the ttl / receive timestamp. + * @param q The question. + * @param message The dns message. + */ + void put(Question q, DNSMessage message); + + /** + * Request a cached dns response. + * @param q The dns question. + * @return The dns message. + */ + DNSMessage get(Question q); + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/DNSMessage.java b/libs/minidns/src/main/java/de/measite/minidns/DNSMessage.java new file mode 100644 index 00000000..ab2535ce --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/DNSMessage.java @@ -0,0 +1,524 @@ +package de.measite.minidns; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +/** + * A DNS message as defined by rfc1035. The message consists of a header and + * 4 sections: question, answer, nameserver and addition resource record + * section. + * A message can either be parsed ({@link DNSMessage#parse(byte[])}) or serialized + * ({@link DNSMessage#toArray()}). + */ +public class DNSMessage { + + /** + * Possible DNS reply codes. + */ + public static enum RESPONSE_CODE { + NO_ERROR(0), FORMAT_ERR(1), SERVER_FAIL(2), NX_DOMAIN(3), + NO_IMP(4), REFUSED(5), YXDOMAIN(6), YXRRSET(7), + NXRRSET(8), NOT_AUTH(9),NOT_ZONE(10); + + /** + * Reverse lookup table for response codes. + */ + private final static RESPONSE_CODE INVERSE_LUT[] = new RESPONSE_CODE[]{ + NO_ERROR, FORMAT_ERR, SERVER_FAIL, NX_DOMAIN, NO_IMP, + REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOT_AUTH, NOT_ZONE, + null, null, null, null, null + }; + + /** + * The response code value. + */ + private final byte value; + + /** + * Create a new response code. + * @param value The response code value. + */ + private RESPONSE_CODE(int value) { + this.value = (byte)value; + } + + /** + * Retrieve the byte value of the response code. + * @return the response code. + */ + public byte getValue() { + return (byte) value; + } + + /** + * Retrieve the response code for a byte value. + * @param value The byte value. + * @return The symbolic response code or null. + * @throws IllegalArgumentException if the value is not in the range of + * 0..15. + */ + public static RESPONSE_CODE getResponseCode(int value) { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT[value]; + } + + }; + + /** + * Symbolic DNS Opcode values. + */ + public static enum OPCODE { + QUERY(0), + INVERSE_QUERY(1), + STATUS(2), + NOTIFY(4), + UPDATE(5); + + /** + * Lookup table for for obcode reolution. + */ + private final static OPCODE INVERSE_LUT[] = new OPCODE[]{ + QUERY, INVERSE_QUERY, STATUS, null, NOTIFY, UPDATE, null, + null, null, null, null, null, null, null, null + }; + + /** + * The value of this opcode. + */ + private final byte value; + + /** + * Create a new opcode for a given byte value. + * @param value The byte value of the opcode. + */ + private OPCODE(int value) { + this.value = (byte)value; + } + + /** + * Retrieve the byte value of this opcode. + * @return The byte value of this opcode. + */ + public byte getValue() { + return value; + } + + /** + * Retrieve the symbolic name of an opcode byte. + * @param value The byte value of the opcode. + * @return The symbolic opcode or null. + * @throws IllegalArgumentException If the byte value is not in the + * range 0..15. + */ + public static OPCODE getOpcode(int value) { + if (value < 0 || value > 15) { + throw new IllegalArgumentException(); + } + return INVERSE_LUT[value]; + } + + }; + + /** + * The DNS message id. + */ + protected int id; + + /** + * The DNS message opcode. + */ + protected OPCODE opcode; + + /** + * The response code of this dns message. + */ + protected RESPONSE_CODE responseCode; + + /** + * True if this is a query. + */ + protected boolean query; + + /** + * True if this is a authorative response. + */ + protected boolean authoritativeAnswer; + + /** + * True on truncate, tcp should be used. + */ + protected boolean truncated; + + /** + * True if the server should recurse. + */ + protected boolean recursionDesired; + + /** + * True if recursion is possible. + */ + protected boolean recursionAvailable; + + /** + * True if the server regarded the response as authentic. + */ + protected boolean authenticData; + + /** + * True if the server should not check the replies. + */ + protected boolean checkDisabled; + + /** + * The question section content. + */ + protected Question questions[]; + + /** + * The answers section content. + */ + protected Record answers[]; + + /** + * The nameserver records. + */ + protected Record nameserverRecords[]; + + /** + * Additional resousrce records. + */ + protected Record additionalResourceRecords[]; + + /** + * The receive timestamp of this message. + */ + protected long receiveTimestamp; + + /** + * Retrieve the current DNS message id. + * @return The current DNS message id. + */ + public int getId() { + return id; + } + + /** + * Set the current DNS message id. + * @param id The new DNS message id. + */ + public void setId(int id) { + this.id = id & 0xffff; + } + + /** + * Get the receive timestamp if this message was created via parse. + * This should be used to evaluate TTLs. + * @return The receive timestamp in milliseconds. + */ + public long getReceiveTimestamp() { + return receiveTimestamp; + } + + /** + * Retrieve the query type (true or false; + * @return True if this DNS message is a query. + */ + public boolean isQuery() { + return query; + } + + /** + * Set the query status of this message. + * @param query The new query status. + */ + public void setQuery(boolean query) { + this.query = query; + } + + /** + * True if the DNS message is an authoritative answer. + * @return True if this an authoritative DNS message. + */ + public boolean isAuthoritativeAnswer() { + return authoritativeAnswer; + } + + /** + * Set the authoritative answer flag. + * @param authoritativeAnswer Tge new authoritative answer value. + */ + public void setAuthoritativeAnswer(boolean authoritativeAnswer) { + this.authoritativeAnswer = authoritativeAnswer; + } + + /** + * Retrieve the truncation status of this message. True means that the + * client should try a tcp lookup. + * @return True if this message was truncated. + */ + public boolean isTruncated() { + return truncated; + } + + /** + * Set the truncation bit on this DNS message. + * @param truncated The new truncated bit status. + */ + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } + + /** + * Check if this message preferes recursion. + * @return True if recursion is desired. + */ + public boolean isRecursionDesired() { + return recursionDesired; + } + + /** + * Set the recursion desired flag on this message. + * @param recursionDesired The new recusrion setting. + */ + public void setRecursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + } + + /** + * Retrieve the recursion available flag of this DNS message. + * @return The recursion available flag of this message. + */ + public boolean isRecursionAvailable() { + return recursionAvailable; + } + + /** + * Set the recursion available flog from this DNS message. + * @param recursionAvailable The new recursion available status. + */ + public void setRecursionAvailable(boolean recursionAvailable) { + this.recursionAvailable = recursionAvailable; + } + + /** + * Retrieve the authentic data flag of this message. + * @return The authentic data flag. + */ + public boolean isAuthenticData() { + return authenticData; + } + + /** + * Set the authentic data flag on this DNS message. + * @param authenticData The new authentic data flag value. + */ + public void setAuthenticData(boolean authenticData) { + this.authenticData = authenticData; + } + + /** + * Check if checks are disabled. + * @return The status of the CheckDisabled flag. + */ + public boolean isCheckDisabled() { + return checkDisabled; + } + + /** + * Change the check status of this packet. + * @param checkDisabled The new check disabled value. + */ + public void setCheckDisabled(boolean checkDisabled) { + this.checkDisabled = checkDisabled; + } + + /** + * Generate a binary dns packet out of this message. + * @return byte[] the binary representation. + * @throws IOException Should never happen. + */ + public byte[] toArray() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + DataOutputStream dos = new DataOutputStream(baos); + int header = 0; + if (query) { + header += 1 << 15; + } + if (opcode != null) { + header += opcode.getValue() << 11; + } + if (authoritativeAnswer) { + header += 1 << 10; + } + if (truncated) { + header += 1 << 9; + } + if (recursionDesired) { + header += 1 << 8; + } + if (recursionAvailable) { + header += 1 << 7; + } + if (authenticData) { + header += 1 << 5; + } + if (checkDisabled) { + header += 1 << 4; + } + if (responseCode != null) { + header += responseCode.getValue(); + } + dos.writeShort((short)id); + dos.writeShort((short)header); + if (questions == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)questions.length); + } + if (answers == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)answers.length); + } + if (nameserverRecords == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)nameserverRecords.length); + } + if (additionalResourceRecords == null) { + dos.writeShort(0); + } else { + dos.writeShort((short)additionalResourceRecords.length); + } + for (Question question: questions) { + dos.write(question.toByteArray()); + } + dos.flush(); + return baos.toByteArray(); + } + + /** + * Build a DNS Message based on a binary DNS message. + * @param data The DNS message data. + * @return Parsed DNSMessage message. + * @throws IOException On read errors. + */ + public static DNSMessage parse(byte data[]) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + DataInputStream dis = new DataInputStream(bis); + DNSMessage message = new DNSMessage(); + message.id = dis.readUnsignedShort(); + int header = dis.readUnsignedShort(); + message.query = ((header >> 15) & 1) == 0; + message.opcode = OPCODE.getOpcode((header >> 11) & 0xf); + message.authoritativeAnswer = ((header >> 10) & 1) == 1; + message.truncated = ((header >> 9) & 1) == 1; + message.recursionDesired = ((header >> 8) & 1) == 1; + message.recursionAvailable = ((header >> 7) & 1) == 1; + message.authenticData = ((header >> 5) & 1) == 1; + message.checkDisabled = ((header >> 4) & 1) == 1; + message.responseCode = RESPONSE_CODE.getResponseCode(header & 0xf); + message.receiveTimestamp = System.currentTimeMillis(); + int questionCount = dis.readUnsignedShort(); + int answerCount = dis.readUnsignedShort(); + int nameserverCount = dis.readUnsignedShort(); + int additionalResourceRecordCount = dis.readUnsignedShort(); + message.questions = new Question[questionCount]; + while (questionCount-- > 0) { + Question q = Question.parse(dis, data); + message.questions[questionCount] = q; + } + message.answers = new Record[answerCount]; + while (answerCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.answers[answerCount] = rr; + } + message.nameserverRecords = new Record[nameserverCount]; + while (nameserverCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.nameserverRecords[nameserverCount] = rr; + } + message.additionalResourceRecords = + new Record[additionalResourceRecordCount]; + while (additionalResourceRecordCount-- > 0) { + Record rr = new Record(); + rr.parse(dis, data); + message.additionalResourceRecords[additionalResourceRecordCount] = + rr; + } + return message; + } + + /** + * Set the question part of this message. + * @param questions The questions. + */ + public void setQuestions(Question ... questions) { + this.questions = questions; + } + + /** + * Retrieve the opcode of this message. + * @return The opcode of this message. + */ + public OPCODE getOpcode() { + return opcode; + } + + /** + * Retrieve the response code of this message. + * @return The response code. + */ + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + /** + * Retrieve the question section of this message. + * @return The DNS question section. + */ + public Question[] getQuestions() { + return questions; + } + + /** + * Retrieve the answer records of this DNS message. + * @return The answer section of this DNS message. + */ + public Record[] getAnswers() { + return answers; + } + + /** + * Retrieve the nameserver records of this DNS message. + * @return The nameserver section of this DNS message. + */ + public Record[] getNameserverRecords() { + return nameserverRecords; + } + + /** + * Retrieve the additional resource records attached to this DNS message. + * @return The additional resource record section of this DNS message. + */ + public Record[] getAdditionalResourceRecords() { + return additionalResourceRecords; + } + + public String toString() { + return "-- DNSMessage " + id + " --\n" + + "Q" + Arrays.toString(questions) + + "NS" + Arrays.toString(nameserverRecords) + + "A" + Arrays.toString(answers) + + "ARR" + Arrays.toString(additionalResourceRecords); + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/LRUCache.java b/libs/minidns/src/main/java/de/measite/minidns/LRUCache.java new file mode 100644 index 00000000..6b9bbdc1 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/LRUCache.java @@ -0,0 +1,139 @@ +package de.measite.minidns; + +import java.util.LinkedHashMap; +import java.util.Map.Entry; + +/** + * LRU based DNSCache backed by a LinkedHashMap. + */ +public class LRUCache implements 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<Question, DNSMessage> 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<Question,DNSMessage>( + Math.min(capacity + (capacity + 3) / 4 + 2, 11), 0.75f, true) + { + @Override + protected boolean removeEldestEntry( + Entry<Question, DNSMessage> 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); + } + + @Override + public synchronized void put(Question q, DNSMessage message) { + if (message.getReceiveTimestamp() <= 0l) { + return; + } + backend.put(q, message); + } + + @Override + public synchronized DNSMessage get(Question q) { + DNSMessage message = backend.get(q); + if (message == null) { + missCount++; + return null; + } + + long ttl = maxTTL; + for (Record r : message.getAnswers()) { + ttl = Math.min(ttl, r.ttl); + } + for (Record r : message.getAdditionalResourceRecords()) { + ttl = Math.min(ttl, r.ttl); + } + if (message.getReceiveTimestamp() + ttl > System.currentTimeMillis()) { + missCount++; + expireCount++; + backend.remove(q); + return null; + } else { + hitCount++; + return message; + } + } + + /** + * 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 sucessful calls to get). + * @return The hit count. + */ + public long getHitCount() { + return hitCount; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/Question.java b/libs/minidns/src/main/java/de/measite/minidns/Question.java new file mode 100644 index 00000000..3b2fa1a1 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/Question.java @@ -0,0 +1,158 @@ +package de.measite.minidns; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * A DNS question (request). + */ +public class Question { + + /** + * The question string (e.g. "measite.de"). + */ + private final String name; + + /** + * The question type (e.g. A). + */ + private final TYPE type; + + /** + * The question class (usually IN / internet). + */ + private 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). + */ + public Question(String name, TYPE type, CLASS clazz, boolean unicastQuery) { + 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(String 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(String name, TYPE type) { + this(name, type, CLASS.IN); + } + + /** + * Retrieve the type of this question. + * @return The type. + */ + public TYPE getType() { + return type; + } + + /** + * Retrieve the class of this dns question (usually internet). + * @return The class of this dns question. + */ + public CLASS getClazz() { + return clazz; + } + + /** + * Retrieve the name of this dns question (e.g. "measite.de"). + * @return The name of this dns question. + */ + public String getName() { + return name; + } + + /** + * 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). + * @return The parsed dns question. + * @throws IOException On errors (read outside of packet). + */ + public static Question parse(DataInputStream dis, byte[] data) throws IOException { + String name = NameUtil.parse(dis, data); + TYPE type = TYPE.getType(dis.readUnsignedShort()); + CLASS clazz = CLASS.getClass(dis.readUnsignedShort()); + return new Question (name, type, clazz); + } + + /** + * 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 { + dos.write(NameUtil.toByteArray(this.name)); + dos.writeShort(type.getValue()); + dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0)); + dos.flush(); + } catch (IOException e) { + // Should never happen + throw new IllegalStateException(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 "Question/" + clazz + "/" + type + ": " + name; + } +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/Record.java b/libs/minidns/src/main/java/de/measite/minidns/Record.java new file mode 100644 index 00000000..ab081426 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/Record.java @@ -0,0 +1,343 @@ +package de.measite.minidns; + +import java.io.DataInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.CNAME; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.NS; +import de.measite.minidns.record.PTR; +import de.measite.minidns.record.SRV; +import de.measite.minidns.record.TXT; +import de.measite.minidns.util.NameUtil; + +/** + * A generic DNS record. + */ +public class Record { + + private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + + /** + * The record type. + * @see <a href="http://www.iana.org/assignments/dns-parameters">IANA DNS Parameters</a> + */ + public static enum TYPE { + A(1), + NS(2), + MD(3), + MF(4), + CNAME(5), + SOA(6), + MB(7), + MG(8), + MR(9), + NULL(10), + WKS(11), + PTR(12), + HINFO(13), + MINFO(14), + MX(15), + TXT(16), + RP(17), + AFSDB(18), + X25(19), + ISDN(20), + RT(21), + NSAP(22), + NSAP_PTR(23), + SIG(24), + KEY(25), + PX(26), + GPOS(27), + AAAA(28), + LOC(29), + NXT(30), + EID(31), + NIMLOC(32), + SRV(33), + ATMA(34), + NAPTR(35), + KX(36), + CERT(37), + A6(38), + DNAME(39), + SINK(40), + OPT(41), + APL(42), + DS(43), + SSHFP(44), + IPSECKEY(45), + RRSIG(46), + NSEC(47), + DNSKEY(48), + DHCID(49), + NSEC3(50), + NSEC3PARAM(51), + HIP(55), + NINFO(56), + RKEY(57), + TALINK(58), + SPF(99), + UINFO(100), + UID(101), + GID(102), + TKEY(249), + TSIG(250), + IXFR(251), + AXFR(252), + MAILB(253), + MAILA(254), + ANY(255), + TA(32768), + DLV(32769); + + /** + * The value of this DNS record type. + */ + private final int value; + + /** + * Internal lookup table to map values to types. + */ + private final static HashMap<Integer, TYPE> INVERSE_LUT = + new HashMap<Integer, TYPE>(); + + /** + * Initialize the reverse lookup table. + */ + static { + for(TYPE t: TYPE.values()) { + INVERSE_LUT.put(t.getValue(), t); + } + } + + /** + * Create a new record type. + * @param value The binary value of this type. + */ + private TYPE(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this type. + * @return The binary value. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic type of the binary value. + * @param value The binary type value. + * @return The symbolic tpye. + */ + public static TYPE getType(int value) { + return INVERSE_LUT.get(value); + } + }; + + /** + * The symbolic class of a DNS record (usually IN for Internet). + */ + public static enum CLASS { + IN(1), + CH(3), + HS(4), + NONE(254), + ANY(255); + + /** + * Internal reverse lookup table to map binary class values to symbolic + * names. + */ + private final static HashMap<Integer, CLASS> INVERSE_LUT = + new HashMap<Integer, CLASS>(); + + /** + * Initialize the interal reverse lookup table. + */ + static { + for(CLASS c: CLASS.values()) { + INVERSE_LUT.put(c.getValue(), c); + } + } + + /** + * The binary value of this dns class. + */ + private final int value; + + /** + * Create a new DNS class based on a binary value. + * @param value The binary value of this DNS class. + */ + private CLASS(int value) { + this.value = value; + } + + /** + * Retrieve the binary value of this DNS class. + * @return The binary value of this DNS class. + */ + public int getValue() { + return value; + } + + /** + * Retrieve the symbolic DNS class for a binary class value. + * @param value The binary DNS class value. + * @return The symbolic class instance. + */ + public static CLASS getClass(int value) { + return INVERSE_LUT.get(value); + } + + } + + /** + * The generic name of this record. + */ + protected String name; + + /** + * The type (and payload type) of this record. + */ + protected TYPE type; + + /** + * The record class (usually CLASS.IN). + */ + protected CLASS clazz; + + /** + * The ttl of this record. + */ + protected long ttl; + + /** + * The payload object of this record. + */ + protected Data payloadData; + + /** + * MDNS defines the highest bit of the class as the unicast query bit. + */ + protected boolean unicastQuery; + + /** + * Parse a given record based on the full message data and the current + * stream position. + * @param dis The DataInputStream positioned at the first record byte. + * @param data The full message data. + * @throws IOException In case of malformed replies. + */ + public void parse(DataInputStream dis, byte[] data) throws IOException { + this.name = NameUtil.parse(dis, data); + this.type = TYPE.getType(dis.readUnsignedShort()); + int clazzValue = dis.readUnsignedShort(); + this.clazz = CLASS.getClass(clazzValue & 0x7fff); + this.unicastQuery = (clazzValue & 0x8000) > 0; + if (this.clazz == null) { + LOGGER.log(Level.FINE, "Unknown class " + clazzValue); + } + this.ttl = (((long)dis.readUnsignedShort()) << 32) + + dis.readUnsignedShort(); + int payloadLength = dis.readUnsignedShort(); + switch (this.type) { + case SRV: + this.payloadData = new SRV(); + break; + case AAAA: + this.payloadData = new AAAA(); + break; + case A: + this.payloadData = new A(); + break; + case NS: + this.payloadData = new NS(); + break; + case CNAME: + this.payloadData = new CNAME(); + break; + case PTR: + this.payloadData = new PTR(); + break; + case TXT: + this.payloadData = new TXT(); + break; + default: + LOGGER.log(Level.FINE, "Unparsed type " + type); + this.payloadData = null; + for (int i = 0; i < payloadLength; i++) { + dis.readByte(); + } + break; + } + if (this.payloadData != null) { + this.payloadData.parse(dis, data, payloadLength); + } + } + + /** + * Retrieve a textual representation of this resource record. + * @return String + */ + @Override + public String toString() { + if (payloadData == null) { + return "RR " + type + "/" + clazz; + } + return "RR " + type + "/" + clazz + ": " + payloadData.toString(); + }; + + /** + * Check if this record answers a given query. + * @param q The query. + * @return True if this record is a valid answer. + */ + public boolean isAnswer(Question q) { + return ((q.getType() == type) || (q.getType() == TYPE.ANY)) && + ((q.getClazz() == clazz) || (q.getClazz() == CLASS.ANY)) && + (q.getName().equals(name)); + } + + /** + * See if this query/response was a unicast query (highest class bit set). + * @return True if it is a unicast query/response record. + */ + public boolean isUnicastQuery() { + return unicastQuery; + } + + /** + * The generic record name, e.g. "measite.de". + * @return The record name. + */ + public String getName() { + return name; + } + + /** + * The payload data, usually a subclass of data (A, AAAA, CNAME, ...). + * @return The payload data. + */ + public Data getPayload() { + return payloadData; + } + + /** + * Retrieve the record ttl. + * @return The record ttl. + */ + public long getTtl() { + return ttl; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/A.java b/libs/minidns/src/main/java/de/measite/minidns/record/A.java new file mode 100644 index 00000000..4311c651 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/A.java @@ -0,0 +1,43 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +/** + * A record payload (ip pointer). + */ +public class A implements Data { + + /** + * Target IP. + */ + private byte[] ip; + + @Override + public TYPE getType() { + return TYPE.A; + } + + @Override + public byte[] toByteArray() { + return ip; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException { + ip = new byte[4]; + dis.readFully(ip); + } + + @Override + public String toString() { + return Integer.toString(ip[0] & 0xff) + "." + + Integer.toString(ip[1] & 0xff) + "." + + Integer.toString(ip[2] & 0xff) + "." + + Integer.toString(ip[3] & 0xff); + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/AAAA.java b/libs/minidns/src/main/java/de/measite/minidns/record/AAAA.java new file mode 100644 index 00000000..e4fd5ecf --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/AAAA.java @@ -0,0 +1,49 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +/** + * AAAA payload (an ipv6 pointer). + */ +public class AAAA implements Data { + + /** + * The ipv6 address. + */ + private byte[] ip; + + @Override + public TYPE getType() { + return TYPE.AAAA; + } + + @Override + public byte[] toByteArray() { + return ip; + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException { + ip = new byte[16]; + dis.readFully(ip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ip.length; i += 2) { + if (i != 0) { + sb.append(':'); + } + sb.append(Integer.toHexString( + ((ip[i] & 0xff) << 8) + (ip[i + 1] & 0xff) + )); + } + return sb.toString(); + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/CNAME.java b/libs/minidns/src/main/java/de/measite/minidns/record/CNAME.java new file mode 100644 index 00000000..1ac27814 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/CNAME.java @@ -0,0 +1,46 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * CNAME payload (pointer to another domain / address). + */ +public class CNAME implements Data { + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public byte[] toByteArray() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + this.name = NameUtil.parse(dis, data); + } + + @Override + public TYPE getType() { + return TYPE.CNAME; + } + + @Override + public String toString() { + return "to \"" + name + "\""; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/Data.java b/libs/minidns/src/main/java/de/measite/minidns/record/Data.java new file mode 100644 index 00000000..7f2db03a --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/Data.java @@ -0,0 +1,34 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; + +/** + * Generic payload class. + */ +public interface Data { + + /** + * The payload type. + * @return The payload type. + */ + TYPE getType(); + + /** + * Binary representation of this payload. + * @return The binary representation of this payload. + */ + byte[] toByteArray(); + + /** + * Parse this payload. + * @param dis The input stream. + * @param data The plain data (needed for name cross references). + * @param length The payload length. + * @throws IOException on io error (read past paket boundary). + */ + void parse(DataInputStream dis, byte data[], int length) throws IOException; + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/NS.java b/libs/minidns/src/main/java/de/measite/minidns/record/NS.java new file mode 100644 index 00000000..8ac2d4c3 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/NS.java @@ -0,0 +1,15 @@ +package de.measite.minidns.record; + +import de.measite.minidns.Record.TYPE; + +/** + * Nameserver record. + */ +public class NS extends CNAME { + + @Override + public TYPE getType() { + return TYPE.NS; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/PTR.java b/libs/minidns/src/main/java/de/measite/minidns/record/PTR.java new file mode 100644 index 00000000..6e200655 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/PTR.java @@ -0,0 +1,19 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * A PTR record is handled like a CNAME + */ +public class PTR extends CNAME { + + @Override + public TYPE getType() { + return TYPE.PTR; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/SRV.java b/libs/minidns/src/main/java/de/measite/minidns/record/SRV.java new file mode 100644 index 00000000..707bf3f5 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/SRV.java @@ -0,0 +1,124 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * SRV record payload (service pointer). + */ +public class SRV implements Data { + + /** + * The priority of this service. + */ + protected int priority; + + /** + * The weight of this service. + */ + protected int weight; + + /** + * The target port. + */ + protected int port; + + /** + * The target server. + */ + protected String name; + + /** + * The priority of this service. Lower values mean higher priority. + * @return The priority. + */ + public int getPriority() { + return priority; + } + + /** + * Set the priority of this service entry. Lower values have higher priority. + * @param priority The new priority. + */ + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * The weight of this service. Services with the same priority should be + * balanced based on weight. + * @return The weight of this service. + */ + public int getWeight() { + return weight; + } + + /** + * Set the weight of this service. + * @param weight The new weight of this service. + */ + public void setWeight(int weight) { + this.weight = weight; + } + + /** + * The target port of this service. + * @return The target port of this service. + */ + public int getPort() { + return port; + } + + /** + * Set the target port of this service. + * @param port The new target port. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * The name of the target server. + * @return The target servers name. + */ + public String getName() { + return name; + } + + /** + * Set the name of the target server. + * @param name The new target servers name. + */ + public void setName(String name) { + this.name = name; + } + + @Override + public byte[] toByteArray() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + this.priority = dis.readUnsignedShort(); + this.weight = dis.readUnsignedShort(); + this.port = dis.readUnsignedShort(); + this.name = NameUtil.parse(dis, data); + } + + @Override + public String toString() { + return "SRV " + name + ":" + port + " p:" + priority + " w:" + weight; + } + + @Override + public TYPE getType() { + return TYPE.SRV; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/record/TXT.java b/libs/minidns/src/main/java/de/measite/minidns/record/TXT.java new file mode 100644 index 00000000..03e73040 --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/record/TXT.java @@ -0,0 +1,65 @@ +package de.measite.minidns.record; + +import java.io.DataInputStream; +import java.io.IOException; + +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.util.NameUtil; + +/** + * TXT record (actually a binary blob with wrappers for text content). + */ +public class TXT implements Data { + + protected byte[] blob; + + public byte[] getBlob() { + return blob; + } + + public void setBlob(byte[] blob) { + this.blob = blob; + } + + public String getText() { + try { + return (new String(blob, "UTF-8")).intern(); + } catch (Exception e) { + /* Can't happen for UTF-8 unless it's really a blob */ + return null; + } + } + + public void setText(String text) { + try { + this.blob = text.getBytes("UTF-8"); + } catch (Exception e) { + /* Can't happen, UTF-8 IS supported */ + throw new RuntimeException("UTF-8 not supported", e); + } + } + + @Override + public byte[] toByteArray() { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public void parse(DataInputStream dis, byte[] data, int length) + throws IOException + { + blob = new byte[length]; + dis.readFully(blob); + } + + @Override + public TYPE getType() { + return TYPE.TXT; + } + + @Override + public String toString() { + return "\"" + getText() + "\""; + } + +} diff --git a/libs/minidns/src/main/java/de/measite/minidns/util/NameUtil.java b/libs/minidns/src/main/java/de/measite/minidns/util/NameUtil.java new file mode 100644 index 00000000..7ae373bc --- /dev/null +++ b/libs/minidns/src/main/java/de/measite/minidns/util/NameUtil.java @@ -0,0 +1,129 @@ +package de.measite.minidns.util; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.IDN; +import java.util.HashSet; +import java.util.Arrays; + +/** + * Utilities related to internationalized domain names and dns name handling. + */ +public class NameUtil { + + /** + * Retrieve the rough binary length of a string + * (length + 2 bytes length prefix). + * @param name The name string. + * @return The binary size of the string (length + 2). + */ + public static int size(String name) { + return name.length() + 2; + } + + /** + * Check if two internationalized domain names are equal, possibly causing + * a serialization of both domain names. + * @param name1 The first domain name. + * @param name2 The second domain name. + * @return True if both domain names are the same. + */ + public static boolean idnEquals(String name1, String name2) { + if (name1 == name2) return true; // catches null, null + if (name1 == null) return false; + if (name2 == null) return false; + if (name1.equals(name2)) return true; + + try { + return Arrays.equals(toByteArray(name1),toByteArray(name2)); + } catch (IOException e) { + return false; // impossible + } + } + + /** + * Serialize a domain name under IDN rules. + * @param name The domain name. + * @return The binary domain name representation. + * @throws IOException Should never happen. + */ + public static byte[] toByteArray(String name) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(64); + DataOutputStream dos = new DataOutputStream(baos); + for (String s: name.split("[.\u3002\uFF0E\uFF61]")) { + byte[] buffer = IDN.toASCII(s).getBytes(); + dos.writeByte(buffer.length); + dos.write(buffer); + } + dos.writeByte(0); + dos.flush(); + return baos.toByteArray(); + } + + /** + * 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 String 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 ""; + } + byte b[] = new byte[c]; + dis.readFully(b); + String s = IDN.toUnicode(new String(b)); + String t = parse(dis, data); + if (t.length() > 0) { + s = s + "." + t; + } + return s; + } + + /** + * 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. + */ + public static String parse( + byte data[], + int offset, + HashSet<Integer> jumps + ) { + 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 ""; + } + String s = new String(data,offset + 1, c); + String t = parse(data, offset + 1 + c, jumps); + if (t.length() > 0) { + s = s + "." + t; + } + return s; + } + +} |