
/*
 * @(#)Dns.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.dns;

import com.jscape.inet.dns.record.DnsRecord;
import com.jscape.inet.util.TimedSocket;

import java.io.*;
import java.net.Socket;
import java.util.StringTokenizer;

/**
 * Implements the basic functionality of a DNS client.
 * <P>
 * Example Usage:
 * <P>
 * <pre>
 * // create new instance
 * Dns dns = new Dns();
 *  // creates DNS request with nameserver and domain as arguments
 * DnsRequest request = new DnsRequest("ns.myserver.com","myserver.com");
 * // retrieve only MX (mail exchanger) records
 * request.setRecordType(Dns.TYPE_MX);
 *  // gets DNS response
 * DnsResponse response = dns.getResponse(request);
 *  // print out records for response to console
 *  Enumeration e = response.getAnswers();
 * while(e.hasMoreElements()) {
 *   DnsRecord record = (DnsRecord) e.nextElement();
 *   System.out.println(record);
 * }
 * </pre>
 * </p>
 */
public final class Dns {
    /**
     * Default Dns port.
     */
    public static final int DEFAULT_PORT = 53;

    /**
     * Indicates a host address record.
     */
    public static final int TYPE_A = 1;

    /**
     * Indicates an authoritative name server record.
     */
    public static final int TYPE_NS = 2;

    /**
     * Indicates a mail destination record. Obsolete use mail exchange record
     * <code>Dns.TYPE_MX</code>.
     */
    public static final int TYPE_MD = 3;

    /**
     * Indicates a mail forwarder record. Obsolete use mail exchange record
     * <code>Dns.TYPE_MX</code>.
     */
    public static final int TYPE_MF = 4;

    /**
     * Indicates a canonical name for an alias record.
     */
    public static final int TYPE_CNAME = 5;

    /**
     * Indicates a start of authority record.
     */
    public static final int TYPE_SOA = 6;

    /**
     * Indicates a mailbox domain record. Experimental per RFC 1035.
     */
    public static final int TYPE_MB = 7;

    /**
     * Indicates a mail group member record. Experimental per RFC 1035.
     */
    public static final int TYPE_MG = 8;

    /**
     * Indicates a mail rename domain name record. Experimental per RFC 1035.
     */
    public static final int TYPE_MR = 9;

    /**
     * Indicates a null record. Experimental per RFC 1035.
     */
    public static final int TYPE_NULL = 10;

    /**
     * Indicates a well known service description record.
     */
    public static final int TYPE_WKS = 11;

    /**
     * Indicates a domain name pointer record.
     */
    public static final int TYPE_PTR = 12;

    /**
     * Indicates a host information record.
     */
    public static final int TYPE_HINFO = 13;

    /**
     * Indicates a mailbox or list information record.
     */
    public static final int TYPE_MINFO = 14;

    /**
     * Indicates a mail exchange record.
     */
    public static final int TYPE_MX = 15;

    /**
     * Indicates a text record.
     */
    public static final int TYPE_TXT = 16;

    /**
     * Indicates a request for incremental zone transfer record.
     */
    public static final int TYPE_IXFR = 251;

    /**
     * Indicates a request for zone transfer record.
     */
    public static final int TYPE_AXFR = 252;

    /**
     * Indicates a request for mailbox-related records record.
     */
    public static final int TYPE_MAILB = 253;

    /**
     * Indicates a request for mail agent record. Obsolete use mail exchange
     * record <code>Dns.TYPE_MX</code>.
     */
    public static final int TYPE_MAILA = 254;

    /**
     * Indicates Any record.
     */
    public static final int TYPE_ANY = 255;

    /**
     * Indicates Internet domain records.
     */
    public static final int CLASS_IN = 1;

    /**
     * Indicates CSNET domain records. Obsolete - used only for examples in
     * some obsolete RFCs.
     */
    public static final int CLASS_CS = 2;

    /**
     * Indicates CHAOS domain records.
     */
    public static final int CLASS_CH = 3;

    /**
     * Indicates HESIOD domain records.
     */
    public static final int CLASS_HS = 4;

    /**
     * Indicates any Domain Records.
     */
    public static final int CLASS_ANY = 255;
    public static final int SHIFT_QUERY = 15;
    public static final int SHIFT_OPCODE = 11;

    /**
     * Indicates that response is an authoritative answer.
     */
    public static final int SHIFT_AUTHORITATIVE = 10;

    /**
     * Indicates that response is truncated.
     */
    public static final int SHIFT_TRUNCATED = 9;

    /**
     * Indicates that recursion is requested. Recursion is requested if the
     * query should be forwarded to another nameserver.
     */
    public static final int SHIFT_RECURSE_PLEASE = 8;

    /**
     * Indicates that recursion is available.
     */
    public static final int SHIFT_RECURSE_AVAILABLE = 7;
    public static final int SHIFT_RESERVED = 4;

    /**
     * Indicates that no error occured in query.
     */
    public static final int SHIFT_RESPONSE_CODE = 0;

    /**
     * Indicates a Query operation.
     */
    public static final int OPCODE_QUERY = 0;

    /**
     * Indicates an Internet Query operation.
     */
    public static final int OPCODE_IQUERY = 1;

    /**
     * Indicates a Status Query operation.
     */
    public static final int OPCODE_STATUS = 2;

    private static final int DEFAULT_PROXY_PORT = 1080;
    private static final String DEFAULT_PROXY_TYPE = "SOCKS5";
    private String proxyHostname = null;
    private int proxyPort = DEFAULT_PROXY_PORT;
    private String proxyUsername = null;
    private String proxyPassword = null;
    private String proxyType = DEFAULT_PROXY_TYPE;

    /**
     * Maps record types to name.
     */
    private static final Class[] TYPE_NAMES = {
            com.jscape.inet.dns.record.Address.class,
            com.jscape.inet.dns.record.NameServer.class,
            com.jscape.inet.dns.record.MailDomain.class,
            com.jscape.inet.dns.record.MailForwarder.class,
            com.jscape.inet.dns.record.CanonicalName.class,
            com.jscape.inet.dns.record.StartOfAuthority.class,
            com.jscape.inet.dns.record.MailBox.class,
            com.jscape.inet.dns.record.MailGroup.class,
            com.jscape.inet.dns.record.MailRename.class,
            com.jscape.inet.dns.record.Null.class,
            com.jscape.inet.dns.record.WellKnownServices.class,
            com.jscape.inet.dns.record.Pointer.class,
            com.jscape.inet.dns.record.HostInfo.class,
            com.jscape.inet.dns.record.MailInfo.class,
            com.jscape.inet.dns.record.MailExchanger.class,
            com.jscape.inet.dns.record.Text.class
    };

    /**
     * Maps error codes to descriptive names.
     */
    private static final String[] CODE_NAMES = {
            "Format error", "Server failure", "Name not known",
            "Not implemented", "Refused"
    };

    /**
     * Constructs a new <code>Dns</code> instance.
     */
    public Dns() {
        
    }

    /**
     * Gets descriptive name for a given Dns record <code>type</code>.
     *
     * @param type a DNS record type
     *
     * @return a <code>DnsRecord</code>
     *
     * @see DnsRecord
     */
    public static Class typeName(int type) {
        return ((type >= 1) && (type <= 16)) ? TYPE_NAMES[type - 1] : null;
    }

    /**
     * Gets descriptive name for a DNS error code.
     *
     * @param code a DNS error code
     *
     * @return error description
     */
    private static String codeName(int code) {
        return ((code >= 1) && (code <= 5)) ? CODE_NAMES[code - 1]
                                            : "Unknown error";
    }

    /**
     * Sets the username and password to use when for authentication with
     * proxy server.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.proxyUsername = proxyUsername;
        this.proxyPassword = proxyPassword;
    }

    // PROXY CONTROL

    /**
     * Sets the proxy hostname and port for this connection.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.proxyHostname = proxyHostname;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the proxy type will be used for this connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }

    /**
     * Gets response from a DNS server based on arguments defined in
     * <code>request</code>.
     *
     * @param request      a <code>DnsRequest</code>
     * @param ignoreLength <code>true</code> if length field of DnsResponse should be ignored and read until EOF, <code>false</code> otherwise, for performance reasons
     *                     this parameter should be set to true only when returning very large DNS responses.
     *
     * @return a <code>DnsResponse</code>
     *
     * @throws DnsException if an I/O or DNS related error occurs
     * @see DnsResponse
     * @see DnsRequest
     */
    public DnsResponse getResponse(DnsRequest request, boolean ignoreLength)
            throws DnsException {
        synchronized (this) {
            DnsResponse response = null;
            Socket con = null;
            try {
                if (this.proxyHostname == null) {
                    con = TimedSocket.getSocket(request.getNameserver(),
                                                Dns.DEFAULT_PORT, request.getTimeout());
                } else {
                    con = TimedSocket.getSocket(request.getNameserver(),
                                                Dns.DEFAULT_PORT, request.getTimeout(),
                                                this.proxyHostname, this.proxyPort, this.proxyUsername,
                                                this.proxyPassword, this.proxyType);
                }
                con.setSoTimeout(request.getTimeout());

                // send query
                BufferedOutputStream bufferedOut = new BufferedOutputStream(con.getOutputStream());
                DataOutputStream dataOut = new DataOutputStream(bufferedOut);
                byte[] queryData = getRequestData(request);

                dataOut.writeShort(queryData.length);
                dataOut.write(queryData);
                dataOut.flush();

                // get response stream
                DataInputStream dataIn = new DataInputStream(con.getInputStream());

                // get response length
                int len = dataIn.readUnsignedShort();


                // some large responses exceed length of unsigned int, corrupting response length
                if (!ignoreLength) {
                    byte[] buffer = new byte[len];

                    // read response
                    dataIn.readFully(buffer);

                    // create response
                    response = getResponse(request, buffer, buffer.length);
                } else {
                    byte[] buffer = new byte[1024];

                    ByteArrayOutputStream bout = new ByteArrayOutputStream();
                    int i = 0;
                    while ((i = dataIn.read(buffer)) != -1) {
                        bout.write(buffer, 0, i);
                    }
                    byte[] responseData = bout.toByteArray();
                    response = getResponse(request, responseData, responseData.length);
                }

            } catch (IOException ioe) {
                throw new DnsException(ioe.getMessage(), ioe);
            } finally {
                try {
                    con.close();
                } catch (Exception e) {
                    // ignore
                }

            }
            return response;
        }
    }

    /**
     * Gets response from a DNS server based on arguments defined in
     * <code>request</code>.
     *
     * @param request a <code>DnsRequest</code>
     *
     * @return a <code>DnsResponse</code>
     *
     * @throws DnsException if an I/O or DNS related error occurs
     * @see DnsResponse
     * @see DnsRequest
     */
    public DnsResponse getResponse(DnsRequest request)
            throws DnsException {
        return getResponse(request, false);
    }

    private byte[] getRequestData(DnsRequest request) {
        ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
        DataOutputStream dataOut = new DataOutputStream(byteArrayOut);

        synchronized (this) {
            try {
                dataOut.writeShort(request.getQueryId());
                dataOut.writeShort((0 << Dns.SHIFT_QUERY) | (Dns.OPCODE_QUERY << Dns.SHIFT_OPCODE) | (1 << Dns.SHIFT_RECURSE_PLEASE));
                dataOut.writeShort(1); // # queries
                dataOut.writeShort(0); // # answers
                dataOut.writeShort(0); // # authorities
                dataOut.writeShort(0); // # additional

                StringTokenizer labels = new StringTokenizer(request.getDomain(),
                                                             ".");

                while (labels.hasMoreTokens()) {
                    String label = labels.nextToken();

                    dataOut.writeByte(label.length());
                    dataOut.writeBytes(label);
                }

                dataOut.writeByte(0);
                dataOut.writeShort(request.getRecordType());
                dataOut.writeShort(request.getClassType());
            } catch (IOException ignored) {
                // todo: ignore?
            }

            return byteArrayOut.toByteArray();
        }
    }

    /**
     * Parses Dns query response.
     *
     * @param request a DnsRequest
     * @param data    the Dns query response data
     * @param length  the length of response data
     *
     * @return a DnsResponse
     *
     * @throws IOException if an I/O or Dns parsing error occurs
     */
    private DnsResponse getResponse(DnsRequest request, byte[] data, int length)
            throws IOException {
        DnsResponse response = new DnsResponse();

        synchronized (this) {
            DnsInputStream inputStream = new DnsInputStream(data, 0, length);
            int id = inputStream.readShort();

            if (id != request.getQueryId()) {
                throw new IOException("ID does not match request");
            }

            int flags = inputStream.readShort();

            decodeFlags(response, flags);

            int numQueries = inputStream.readShort();
            int numAnswers = inputStream.readShort();
            int numAuthorities = inputStream.readShort();
            int numAdditional = inputStream.readShort();

            while (numQueries-- > 0) { // discard questions

                String queryName = inputStream.readDomainName();
                int queryType = inputStream.readShort();
                int queryClass = inputStream.readShort();
            }

            try {
                while (numAnswers-- > 0) {
                    DnsRecord record = inputStream.readRecord();

                    if (record != null) {
                        response.answers.addElement(record);
                    }
                }

                while (numAuthorities-- > 0) {
                    DnsRecord record = inputStream.readRecord();

                    if (record != null) {
                        response.authorities.addElement(record);
                    }
                }

                while (numAdditional-- > 0) {
                    DnsRecord record = inputStream.readRecord();

                    if (record != null) {
                        response.additional.addElement(record);
                    }
                }
            } catch (EOFException ex) {
                if (!response.truncated) {
                    throw ex;
                }
            }
        }

        return response;
    }

    /**
     * Decodes response.
     *
     * @param response a DnsResponse
     * @param flags    DNS flags
     *
     * @throws IOException if an I/O error occurs
     */
    private void decodeFlags(DnsResponse response, int flags)
            throws IOException {
        synchronized (this) {
            boolean isResponse = ((flags >> Dns.SHIFT_QUERY) & 1) != 0;

            if (!isResponse) {
                throw new IOException("Response flag not set");
            }

            int opcode = (flags >> Dns.SHIFT_OPCODE) & 15;

            // could check opcode
            response.authoritative = ((flags >> Dns.SHIFT_AUTHORITATIVE) & 1) != 0;
            response.truncated = ((flags >> Dns.SHIFT_TRUNCATED) & 1) != 0;

            boolean recurseRequest = ((flags >> Dns.SHIFT_RECURSE_PLEASE) & 1) != 0;

            // could check recurse request
            response.recursive = ((flags >> Dns.SHIFT_RECURSE_AVAILABLE) & 1) != 0;

            int code = (flags >> Dns.SHIFT_RESPONSE_CODE) & 15;

            if (code != 0) {
                throw new IOException(Dns.codeName(code) + " (" + code + ")");
            }
        }
    }
}
