// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.net.*;
import java.util.regex.*;

/**
 * The Client Subnet EDNS Option, defined in
 * http://tools.ietf.org/html/draft-vandergaast-edns-client-subnet-00
 * ("Client subnet in DNS requests").
 * <p/>
 * The option is used to convey information about the IP address of the
 * originating client, so that an authoritative server can make decisions
 * based on this address, rather than the address of the intermediate
 * caching name server.
 * <p/>
 * The option is transmitted as part of an OPTRecord in the additional section
 * of a DNS message, as defined by RFC 2671 (EDNS0).
 * <p/>
 * An option code has not been assigned by IANA; the value 20730 (used here) is
 * also used by several other implementations.
 * <p/>
 * The wire format of the option contains a 2-byte length field (1 for IPv4, 2
 * for IPv6), a 1-byte source netmask, a 1-byte scope netmask, and an address
 * truncated to the source netmask length (where the final octet is padded with
 * bits set to 0)
 *
 * @author Brian Wellington
 * @author Ming Zhou &lt;mizhou@bnivideo.com&gt;, Beaumaris Networks
 * @see OPTRecord
 */
public class ClientSubnetOption extends EDNSOption {

    private static final long serialVersionUID = -3868158449890266347L;

    private int family;
    private int sourceNetmask;
    private int scopeNetmask;
    private InetAddress address;

    ClientSubnetOption() {
        super(EDNSOption.Code.CLIENT_SUBNET);
    }

    private static int
    checkMaskLength(String field, int family, int val) {
        int max = Address.addressLength(family) * 8;
        if (val < 0 || val > max)
            throw new IllegalArgumentException("\"" + field + "\" " + val +
                    " must be in the range " +
                    "[0.." + max + "]");
        return val;
    }

    /**
     * Construct a Client Subnet option.  Note that the number of significant bits in
     * the address must not be greater than the supplied source netmask.
     * XXX something about Java's mapped addresses
     *
     * @param sourceNetmask The length of the netmask pertaining to the query.
     *                      In replies, it mirrors the same value as in the requests.
     * @param scopeNetmask  The length of the netmask pertaining to the reply.
     *                      In requests, it MUST be set to 0.  In responses, this may or may not match
     *                      the source netmask.
     * @param address       The address of the client.
     */
    public ClientSubnetOption(int sourceNetmask, int scopeNetmask, InetAddress address) {
        super(EDNSOption.Code.CLIENT_SUBNET);

        this.family = Address.familyOf(address);
        this.sourceNetmask = checkMaskLength("source netmask", this.family,
                sourceNetmask);
        this.scopeNetmask = checkMaskLength("scope netmask", this.family,
                scopeNetmask);
        this.address = Address.truncate(address, sourceNetmask);

        if (!address.equals(this.address))
            throw new IllegalArgumentException("source netmask is not " +
                    "valid for address");
    }

    /**
     * Construct a Client Subnet option with scope netmask set to 0.
     *
     * @param sourceNetmask The length of the netmask pertaining to the query.
     *                      In replies, it mirrors the same value as in the requests.
     * @param address       The address of the client.
     * @see ClientSubnetOption
     */
    public ClientSubnetOption(int sourceNetmask, InetAddress address) {
        this(sourceNetmask, 0, address);
    }

    /**
     * Returns the family of the network address.  This will be either IPv4 (1)
     * or IPv6 (2).
     */
    public int
    getFamily() {
        return family;
    }

    /**
     * Returns the source netmask.
     */
    public int
    getSourceNetmask() {
        return sourceNetmask;
    }

    /**
     * Returns the scope netmask.
     */
    public int
    getScopeNetmask() {
        return scopeNetmask;
    }

    /**
     * Returns the IP address of the client.
     */
    public InetAddress
    getAddress() {
        return address;
    }

    void
    optionFromWire(DNSInput in) throws WireParseException {
        family = in.readU16();
        if (family != Address.IPv4 && family != Address.IPv6)
            throw new WireParseException("unknown address family");
        sourceNetmask = in.readU8();
        if (sourceNetmask > Address.addressLength(family) * 8)
            throw new WireParseException("invalid source netmask");
        scopeNetmask = in.readU8();
        if (scopeNetmask > Address.addressLength(family) * 8)
            throw new WireParseException("invalid scope netmask");

        // Read the truncated address
        byte[] addr = in.readByteArray();
        if (addr.length != (sourceNetmask + 7) / 8)
            throw new WireParseException("invalid address");

        // Convert it to a full length address.
        byte[] fulladdr = new byte[Address.addressLength(family)];
        System.arraycopy(addr, 0, fulladdr, 0, addr.length);

        try {
            address = InetAddress.getByAddress(fulladdr);
        } catch (UnknownHostException e) {
            throw new WireParseException("invalid address", e);
        }

        InetAddress tmp = Address.truncate(address, sourceNetmask);
        if (!tmp.equals(address))
            throw new WireParseException("invalid padding");
    }

    void
    optionToWire(DNSOutput out) {
        out.writeU16(family);
        out.writeU8(sourceNetmask);
        out.writeU8(scopeNetmask);
        out.writeByteArray(address.getAddress(), 0, (sourceNetmask + 7) / 8);
    }

    String
    optionToString() {
        StringBuffer sb = new StringBuffer();
        sb.append(address.getHostAddress());
        sb.append("/");
        sb.append(sourceNetmask);
        sb.append(", scope netmask ");
        sb.append(scopeNetmask);
        return sb.toString();
    }

}
