/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed 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 android.net.dhcp;

import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE;
import static com.android.server.util.NetworkStackConstants.IPV4_MAX_MTU;
import static com.android.server.util.NetworkStackConstants.IPV4_MIN_MTU;

import static java.lang.Integer.toUnsignedLong;

import android.net.IpPrefix;
import android.net.LinkAddress;
import android.util.ArraySet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.net.module.util.Inet4AddressUtils;

import java.net.Inet4Address;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Parameters used by the DhcpServer to serve requests.
 *
 * <p>Instances are immutable. Use {@link DhcpServingParams.Builder} to instantiate.
 * @hide
 */
public class DhcpServingParams {
    public static final int MTU_UNSET = 0;
    public static final int MIN_PREFIX_LENGTH = 16;
    public static final int MAX_PREFIX_LENGTH = 30;

    /** Server inet address and prefix to serve */
    @NonNull
    public final LinkAddress serverAddr;

    /**
     * Default routers to be advertised to DHCP clients. May be empty.
     * This set is provided by {@link DhcpServingParams.Builder} and is immutable.
     */
    @NonNull
    public final Set<Inet4Address> defaultRouters;

    /**
     * DNS servers to be advertised to DHCP clients. May be empty.
     * This set is provided by {@link DhcpServingParams.Builder} and is immutable.
     */
    @NonNull
    public final Set<Inet4Address> dnsServers;

    /**
     * Excluded addresses that the DHCP server is not allowed to assign to clients.
     * This set is provided by {@link DhcpServingParams.Builder} and is immutable.
     */
    @NonNull
    public final Set<Inet4Address> excludedAddrs;

    // DHCP uses uint32. Use long for clearer code, and check range when building.
    public final long dhcpLeaseTimeSecs;
    public final int linkMtu;

    /**
     * Indicates whether the DHCP server should send the ANDROID_METERED vendor-specific option.
     */
    public final boolean metered;

    /**
     * Client inet address. This will be the only address offered by DhcpServer if set.
     */
    @Nullable
    public final Inet4Address singleClientAddr;

    /**
     * Indicates whether the DHCP server should request a new prefix from IpServer when receiving
     * DHCPDECLINE message in certain particular link (e.g. there is only one downstream USB
     * tethering client). If it's false, process DHCPDECLINE message as RFC2131#4.3.3 suggests.
     */
    public final boolean changePrefixOnDecline;

    /**
     * Checked exception thrown when some parameters used to build {@link DhcpServingParams} are
     * missing or invalid.
     */
    public static class InvalidParameterException extends Exception {
        public InvalidParameterException(String message) {
            super(message);
        }
    }

    private DhcpServingParams(@NonNull LinkAddress serverAddr,
            @NonNull Set<Inet4Address> defaultRouters,
            @NonNull Set<Inet4Address> dnsServers, @NonNull Set<Inet4Address> excludedAddrs,
            long dhcpLeaseTimeSecs, int linkMtu, boolean metered, Inet4Address singleClientAddr,
            boolean changePrefixOnDecline) {
        this.serverAddr = serverAddr;
        this.defaultRouters = defaultRouters;
        this.dnsServers = dnsServers;
        this.excludedAddrs = excludedAddrs;
        this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
        this.linkMtu = linkMtu;
        this.metered = metered;
        this.singleClientAddr = singleClientAddr;
        this.changePrefixOnDecline = changePrefixOnDecline;
    }

    /**
     * Create parameters from a stable AIDL-compatible parcel.
     * @throws InvalidParameterException The parameters parcelable is null or invalid.
     */
    public static DhcpServingParams fromParcelableObject(@Nullable DhcpServingParamsParcel parcel)
            throws InvalidParameterException {
        if (parcel == null) {
            throw new InvalidParameterException("Null serving parameters");
        }
        final LinkAddress serverAddr = new LinkAddress(
                intToInet4AddressHTH(parcel.serverAddr),
                parcel.serverAddrPrefixLength);
        Inet4Address clientAddr = null;
        if (parcel.singleClientAddr != 0) {
            clientAddr = intToInet4AddressHTH(parcel.singleClientAddr);
        }

        return new Builder()
                .setServerAddr(serverAddr)
                .setDefaultRouters(toInet4AddressSet(parcel.defaultRouters))
                .setDnsServers(toInet4AddressSet(parcel.dnsServers))
                .setExcludedAddrs(toInet4AddressSet(parcel.excludedAddrs))
                .setDhcpLeaseTimeSecs(parcel.dhcpLeaseTimeSecs)
                .setLinkMtu(parcel.linkMtu)
                .setMetered(parcel.metered)
                .setSingleClientAddr(clientAddr)
                .setChangePrefixOnDecline(parcel.changePrefixOnDecline)
                .build();
    }

    private static Set<Inet4Address> toInet4AddressSet(@Nullable int[] addrs) {
        if (addrs == null) {
            return new HashSet<>(0);
        }

        final HashSet<Inet4Address> res = new HashSet<>();
        for (int addr : addrs) {
            res.add(intToInet4AddressHTH(addr));
        }
        return res;
    }

    @NonNull
    public Inet4Address getServerInet4Addr() {
        return (Inet4Address) serverAddr.getAddress();
    }

    /**
     * Get the served prefix mask as an IPv4 address.
     *
     * <p>For example, if the served prefix is 192.168.42.0/24, this will return 255.255.255.0.
     */
    @NonNull
    public Inet4Address getPrefixMaskAsAddress() {
        return getPrefixMaskAsInet4Address(serverAddr.getPrefixLength());
    }

    /**
     * Get the server broadcast address.
     *
     * <p>For example, if the server {@link LinkAddress} is 192.168.42.1/24, this will return
     * 192.168.42.255.
     */
    @NonNull
    public Inet4Address getBroadcastAddress() {
        return Inet4AddressUtils.getBroadcastAddress(
                getServerInet4Addr(), serverAddr.getPrefixLength());
    }

    /**
     * Utility class to create new instances of {@link DhcpServingParams} while checking validity
     * of the parameters.
     */
    public static class Builder {
        private LinkAddress mServerAddr;
        private Set<Inet4Address> mDefaultRouters;
        private Set<Inet4Address> mDnsServers;
        private Set<Inet4Address> mExcludedAddrs;
        private long mDhcpLeaseTimeSecs;
        private int mLinkMtu = MTU_UNSET;
        private boolean mMetered;
        private Inet4Address mClientAddr;
        private boolean mChangePrefixOnDecline;

        /**
         * Set the server address and served prefix for the DHCP server.
         *
         * <p>This parameter is required.
         */
        public Builder setServerAddr(@NonNull LinkAddress serverAddr) {
            this.mServerAddr = serverAddr;
            return this;
        }

        /**
         * Set the default routers to be advertised to DHCP clients.
         *
         * <p>Each router must be inside the served prefix. This may be an empty set, but it must
         * always be set explicitly before building the {@link DhcpServingParams}.
         */
        public Builder setDefaultRouters(@NonNull Set<Inet4Address> defaultRouters) {
            this.mDefaultRouters = defaultRouters;
            return this;
        }

        /**
         * Set the default routers to be advertised to DHCP clients.
         *
         * <p>Each router must be inside the served prefix. This may be an empty list of routers,
         * but it must always be set explicitly before building the {@link DhcpServingParams}.
         */
        public Builder setDefaultRouters(@NonNull Inet4Address... defaultRouters) {
            return setDefaultRouters(makeArraySet(defaultRouters));
        }

        /**
         * Convenience method to build the parameters with no default router.
         *
         * <p>Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address.
         */
        public Builder withNoDefaultRouter() {
            return setDefaultRouters();
        }

        /**
         * Set the DNS servers to be advertised to DHCP clients.
         *
         * <p>This may be an empty set, but it must always be set explicitly before building the
         * {@link DhcpServingParams}.
         */
        public Builder setDnsServers(@NonNull Set<Inet4Address> dnsServers) {
            this.mDnsServers = dnsServers;
            return this;
        }

        /**
         * Set the DNS servers to be advertised to DHCP clients.
         *
         * <p>This may be an empty list of servers, but it must always be set explicitly before
         * building the {@link DhcpServingParams}.
         */
        public Builder setDnsServers(@NonNull Inet4Address... dnsServers) {
            return setDnsServers(makeArraySet(dnsServers));
        }

        /**
         * Convenience method to build the parameters with no DNS server.
         *
         * <p>Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address.
         */
        public Builder withNoDnsServer() {
            return setDnsServers();
        }

        /**
         * Set excluded addresses that the DHCP server is not allowed to assign to clients.
         *
         * <p>This parameter is optional. DNS servers and default routers are always excluded
         * and do not need to be set here.
         */
        public Builder setExcludedAddrs(@NonNull Set<Inet4Address> excludedAddrs) {
            this.mExcludedAddrs = excludedAddrs;
            return this;
        }

        /**
         * Set excluded addresses that the DHCP server is not allowed to assign to clients.
         *
         * <p>This parameter is optional. DNS servers and default routers are always excluded
         * and do not need to be set here.
         */
        public Builder setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) {
            return setExcludedAddrs(makeArraySet(excludedAddrs));
        }

        /**
         * Set the lease time for leases assigned by the DHCP server.
         *
         * <p>This parameter is required.
         */
        public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) {
            this.mDhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
            return this;
        }

        /**
         * Set the link MTU to be advertised to DHCP clients.
         *
         * <p>If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter
         * is optional and defaults to {@link #MTU_UNSET}.
         */
        public Builder setLinkMtu(int linkMtu) {
            this.mLinkMtu = linkMtu;
            return this;
        }

        /**
         * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option.
         *
         * <p>If not set, the default value is false.
         */
        public Builder setMetered(boolean metered) {
            this.mMetered = metered;
            return this;
        }

        /**
         * Set the client address.
         *
         * <p>If not set, the default value is null.
         */
        public Builder setSingleClientAddr(@Nullable Inet4Address clientAddr) {
            this.mClientAddr = clientAddr;
            return this;
        }

        /**
         * Set whether the DHCP server should request a new prefix from IpServer when receiving
         * DHCPDECLINE message in certain particular link.
         *
         * <p>If not set, the default value is false.
         */
        public Builder setChangePrefixOnDecline(boolean changePrefixOnDecline) {
            this.mChangePrefixOnDecline = changePrefixOnDecline;
            return this;
        }

        /**
         * Create a new {@link DhcpServingParams} instance based on parameters set in the builder.
         *
         * <p>This method has no side-effects. If it does not throw, a valid
         * {@link DhcpServingParams} is returned.
         * @return The constructed parameters.
         * @throws InvalidParameterException At least one parameter is missing or invalid.
         */
        @NonNull
        public DhcpServingParams build() throws InvalidParameterException {
            if (mServerAddr == null) {
                throw new InvalidParameterException("Missing serverAddr");
            }
            if (mDefaultRouters == null) {
                throw new InvalidParameterException("Missing defaultRouters");
            }
            if (mDnsServers == null) {
                // Empty set is OK, but enforce explicitly setting it
                throw new InvalidParameterException("Missing dnsServers");
            }
            if (mDhcpLeaseTimeSecs <= 0 || mDhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) {
                throw new InvalidParameterException("Invalid lease time: " + mDhcpLeaseTimeSecs);
            }
            if (mLinkMtu != MTU_UNSET && (mLinkMtu < IPV4_MIN_MTU || mLinkMtu > IPV4_MAX_MTU)) {
                throw new InvalidParameterException("Invalid link MTU: " + mLinkMtu);
            }
            if (!mServerAddr.isIpv4()) {
                throw new InvalidParameterException("serverAddr must be IPv4");
            }
            if (mServerAddr.getPrefixLength() < MIN_PREFIX_LENGTH
                    || mServerAddr.getPrefixLength() > MAX_PREFIX_LENGTH) {
                throw new InvalidParameterException("Prefix length is not in supported range");
            }

            final IpPrefix prefix = makeIpPrefix(mServerAddr);
            for (Inet4Address addr : mDefaultRouters) {
                if (!prefix.contains(addr)) {
                    throw new InvalidParameterException(String.format(
                            "Default router %s is not in server prefix %s", addr, mServerAddr));
                }
            }

            final Set<Inet4Address> excl = new HashSet<>();
            if (mExcludedAddrs != null) {
                excl.addAll(mExcludedAddrs);
            }
            excl.add((Inet4Address) mServerAddr.getAddress());
            excl.addAll(mDefaultRouters);
            excl.addAll(mDnsServers);

            return new DhcpServingParams(mServerAddr,
                    Collections.unmodifiableSet(new HashSet<>(mDefaultRouters)),
                    Collections.unmodifiableSet(new HashSet<>(mDnsServers)),
                    Collections.unmodifiableSet(excl),
                    mDhcpLeaseTimeSecs, mLinkMtu, mMetered, mClientAddr, mChangePrefixOnDecline);
        }
    }

    /**
     * Utility method to create an IpPrefix with the address and prefix length of a LinkAddress.
     */
    @NonNull
    static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) {
        return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
    }

    private static <T> ArraySet<T> makeArraySet(T[] elements) {
        final ArraySet<T> set = new ArraySet<>(elements.length);
        set.addAll(Arrays.asList(elements));
        return set;
    }
}
