/*
 * Copyright (C) 2014 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 com.android.server.net;

import android.net.InetAddresses;
import android.net.IpConfiguration;
import android.net.IpConfiguration.IpAssignment;
import android.net.IpConfiguration.ProxySettings;
import android.net.LinkAddress;
import android.net.ProxyInfo;
import android.net.StaticIpConfiguration;
import android.net.Uri;
import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.ProxyUtils;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;

/**
 * This class provides an API to store and manage L3 network IP configuration.
 */
public class IpConfigStore {
    private static final String TAG = "IpConfigStore";
    private static final boolean DBG = false;

    protected final DelayedDiskWrite mWriter;

    /* IP and proxy configuration keys */
    protected static final String ID_KEY = "id";
    protected static final String IP_ASSIGNMENT_KEY = "ipAssignment";
    protected static final String LINK_ADDRESS_KEY = "linkAddress";
    protected static final String GATEWAY_KEY = "gateway";
    protected static final String DNS_KEY = "dns";
    protected static final String PROXY_SETTINGS_KEY = "proxySettings";
    protected static final String PROXY_HOST_KEY = "proxyHost";
    protected static final String PROXY_PORT_KEY = "proxyPort";
    protected static final String PROXY_PAC_FILE = "proxyPac";
    protected static final String EXCLUSION_LIST_KEY = "exclusionList";
    protected static final String EOS = "eos";

    protected static final int IPCONFIG_FILE_VERSION = 3;

    public IpConfigStore(DelayedDiskWrite writer) {
        mWriter = writer;
    }

    public IpConfigStore() {
        this(new DelayedDiskWrite());
    }

    private static boolean writeConfig(DataOutputStream out, String configKey,
            IpConfiguration config) throws IOException {
        return writeConfig(out, configKey, config, IPCONFIG_FILE_VERSION);
    }

    /**
     *  Write the IP configuration with the given parameters to {@link DataOutputStream}.
     */
    @VisibleForTesting
    public static boolean writeConfig(DataOutputStream out, String configKey,
                                IpConfiguration config, int version) throws IOException {
        boolean written = false;

        try {
            switch (config.getIpAssignment()) {
                case STATIC:
                    out.writeUTF(IP_ASSIGNMENT_KEY);
                    out.writeUTF(config.getIpAssignment().toString());
                    StaticIpConfiguration staticIpConfiguration = config.getStaticIpConfiguration();
                    if (staticIpConfiguration != null) {
                        if (staticIpConfiguration.getIpAddress() != null) {
                            LinkAddress ipAddress = staticIpConfiguration.getIpAddress();
                            out.writeUTF(LINK_ADDRESS_KEY);
                            out.writeUTF(ipAddress.getAddress().getHostAddress());
                            out.writeInt(ipAddress.getPrefixLength());
                        }
                        if (staticIpConfiguration.getGateway() != null) {
                            out.writeUTF(GATEWAY_KEY);
                            out.writeInt(0);  // Default route.
                            out.writeInt(1);  // Have a gateway.
                            out.writeUTF(staticIpConfiguration.getGateway().getHostAddress());
                        }
                        for (InetAddress inetAddr : staticIpConfiguration.getDnsServers()) {
                            out.writeUTF(DNS_KEY);
                            out.writeUTF(inetAddr.getHostAddress());
                        }
                    }
                    written = true;
                    break;
                case DHCP:
                    out.writeUTF(IP_ASSIGNMENT_KEY);
                    out.writeUTF(config.getIpAssignment().toString());
                    written = true;
                    break;
                case UNASSIGNED:
                /* Ignore */
                    break;
                default:
                    loge("Ignore invalid ip assignment while writing");
                    break;
            }

            switch (config.getProxySettings()) {
                case STATIC:
                    ProxyInfo proxyProperties = config.getHttpProxy();
                    String exclusionList = ProxyUtils.exclusionListAsString(
                            proxyProperties.getExclusionList());
                    out.writeUTF(PROXY_SETTINGS_KEY);
                    out.writeUTF(config.getProxySettings().toString());
                    out.writeUTF(PROXY_HOST_KEY);
                    out.writeUTF(proxyProperties.getHost());
                    out.writeUTF(PROXY_PORT_KEY);
                    out.writeInt(proxyProperties.getPort());
                    if (exclusionList != null) {
                        out.writeUTF(EXCLUSION_LIST_KEY);
                        out.writeUTF(exclusionList);
                    }
                    written = true;
                    break;
                case PAC:
                    ProxyInfo proxyPacProperties = config.getHttpProxy();
                    out.writeUTF(PROXY_SETTINGS_KEY);
                    out.writeUTF(config.getProxySettings().toString());
                    out.writeUTF(PROXY_PAC_FILE);
                    out.writeUTF(proxyPacProperties.getPacFileUrl().toString());
                    written = true;
                    break;
                case NONE:
                    out.writeUTF(PROXY_SETTINGS_KEY);
                    out.writeUTF(config.getProxySettings().toString());
                    written = true;
                    break;
                case UNASSIGNED:
                    /* Ignore */
                    break;
                default:
                    loge("Ignore invalid proxy settings while writing");
                    break;
            }

            if (written) {
                out.writeUTF(ID_KEY);
                if (version < 3) {
                    out.writeInt(Integer.valueOf(configKey));
                } else {
                    out.writeUTF(configKey);
                }
            }
        } catch (NullPointerException e) {
            loge("Failure in writing " + config + e);
        }
        out.writeUTF(EOS);

        return written;
    }

    /**
     * @deprecated use {@link #writeIpConfigurations(String, ArrayMap)} instead.
     * New method uses string as network identifier which could be interface name or MAC address or
     * other token.
     */
    @Deprecated
    public void writeIpAndProxyConfigurationsToFile(String filePath,
                                              final SparseArray<IpConfiguration> networks) {
        mWriter.write(filePath, out -> {
            out.writeInt(IPCONFIG_FILE_VERSION);
            for (int i = 0; i < networks.size(); i++) {
                writeConfig(out, String.valueOf(networks.keyAt(i)), networks.valueAt(i));
            }
        });
    }

    /**
     *  Write the IP configuration associated to the target networks to the destination path.
     */
    public void writeIpConfigurations(String filePath,
                                      ArrayMap<String, IpConfiguration> networks) {
        mWriter.write(filePath, out -> {
            out.writeInt(IPCONFIG_FILE_VERSION);
            for (int i = 0; i < networks.size(); i++) {
                writeConfig(out, networks.keyAt(i), networks.valueAt(i));
            }
        });
    }

    /**
     * Read the IP configuration from the destination path to {@link BufferedInputStream}.
     */
    public static ArrayMap<String, IpConfiguration> readIpConfigurations(String filePath) {
        BufferedInputStream bufferedInputStream;
        try {
            bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
        } catch (FileNotFoundException e) {
            // Return an empty array here because callers expect an empty array when the file is
            // not present.
            loge("Error opening configuration file: " + e);
            return new ArrayMap<>(0);
        }
        return readIpConfigurations(bufferedInputStream);
    }

    /** @deprecated use {@link #readIpConfigurations(String)} */
    @Deprecated
    public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(String filePath) {
        BufferedInputStream bufferedInputStream;
        try {
            bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
        } catch (FileNotFoundException e) {
            // Return an empty array here because callers expect an empty array when the file is
            // not present.
            loge("Error opening configuration file: " + e);
            return new SparseArray<>();
        }
        return readIpAndProxyConfigurations(bufferedInputStream);
    }

    /** @deprecated use {@link #readIpConfigurations(InputStream)} */
    @Deprecated
    public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(
            InputStream inputStream) {
        ArrayMap<String, IpConfiguration> networks = readIpConfigurations(inputStream);
        if (networks == null) {
            return null;
        }

        SparseArray<IpConfiguration> networksById = new SparseArray<>();
        for (int i = 0; i < networks.size(); i++) {
            int id = Integer.valueOf(networks.keyAt(i));
            networksById.put(id, networks.valueAt(i));
        }

        return networksById;
    }

    /** Returns a map of network identity token and {@link IpConfiguration}. */
    public static ArrayMap<String, IpConfiguration> readIpConfigurations(
            InputStream inputStream) {
        ArrayMap<String, IpConfiguration> networks = new ArrayMap<>();
        DataInputStream in = null;
        try {
            in = new DataInputStream(inputStream);

            int version = in.readInt();
            if (version != 3 && version != 2 && version != 1) {
                loge("Bad version on IP configuration file, ignore read");
                return null;
            }

            while (true) {
                String uniqueToken = null;
                // Default is DHCP with no proxy
                IpAssignment ipAssignment = IpAssignment.DHCP;
                ProxySettings proxySettings = ProxySettings.NONE;
                StaticIpConfiguration staticIpConfiguration = new StaticIpConfiguration();
                LinkAddress linkAddress = null;
                InetAddress gatewayAddress = null;
                String proxyHost = null;
                String pacFileUrl = null;
                int proxyPort = -1;
                String exclusionList = null;
                String key;
                final List<InetAddress> dnsServers = new ArrayList<>();

                do {
                    key = in.readUTF();
                    try {
                        if (key.equals(ID_KEY)) {
                            if (version < 3) {
                                int id = in.readInt();
                                uniqueToken = String.valueOf(id);
                            } else {
                                uniqueToken = in.readUTF();
                            }
                        } else if (key.equals(IP_ASSIGNMENT_KEY)) {
                            ipAssignment = IpAssignment.valueOf(in.readUTF());
                        } else if (key.equals(LINK_ADDRESS_KEY)) {
                            LinkAddress parsedLinkAddress =
                                    new LinkAddress(
                                            InetAddresses.parseNumericAddress(in.readUTF()),
                                            in.readInt());
                            if (parsedLinkAddress.getAddress() instanceof Inet4Address
                                    && linkAddress == null) {
                                linkAddress = parsedLinkAddress;
                            } else {
                                loge("Non-IPv4 or duplicate address: " + parsedLinkAddress);
                            }
                        } else if (key.equals(GATEWAY_KEY)) {
                            LinkAddress dest = null;
                            InetAddress gateway = null;
                            if (version == 1) {
                                // only supported default gateways - leave the dest/prefix empty
                                gateway = InetAddresses.parseNumericAddress(in.readUTF());
                                if (gatewayAddress == null) {
                                    gatewayAddress = gateway;
                                } else {
                                    loge("Duplicate gateway: " + gateway.getHostAddress());
                                }
                            } else {
                                if (in.readInt() == 1) {
                                    dest =
                                            new LinkAddress(
                                                    InetAddresses.parseNumericAddress(in.readUTF()),
                                                    in.readInt());
                                }
                                if (in.readInt() == 1) {
                                    gateway = InetAddresses.parseNumericAddress(in.readUTF());
                                }
                                // If the destination is a default IPv4 route, use the gateway
                                // address unless already set. If there is no destination, assume
                                // it is default route and use the gateway address in all cases.
                                if (dest == null) {
                                    gatewayAddress = gateway;
                                } else if (dest.getAddress() instanceof Inet4Address
                                        && dest.getPrefixLength() == 0 && gatewayAddress == null) {
                                    gatewayAddress = gateway;
                                } else {
                                    loge("Non-IPv4 default or duplicate route: "
                                            + dest.getAddress());
                                }
                            }
                        } else if (key.equals(DNS_KEY)) {
                            dnsServers.add(InetAddresses.parseNumericAddress(in.readUTF()));
                        } else if (key.equals(PROXY_SETTINGS_KEY)) {
                            proxySettings = ProxySettings.valueOf(in.readUTF());
                        } else if (key.equals(PROXY_HOST_KEY)) {
                            proxyHost = in.readUTF();
                        } else if (key.equals(PROXY_PORT_KEY)) {
                            proxyPort = in.readInt();
                        } else if (key.equals(PROXY_PAC_FILE)) {
                            pacFileUrl = in.readUTF();
                        } else if (key.equals(EXCLUSION_LIST_KEY)) {
                            exclusionList = in.readUTF();
                        } else if (key.equals(EOS)) {
                            break;
                        } else {
                            loge("Ignore unknown key " + key + "while reading");
                        }
                    } catch (IllegalArgumentException e) {
                        loge("Ignore invalid address while reading" + e);
                    }
                } while (true);

                staticIpConfiguration = new StaticIpConfiguration.Builder()
                    .setIpAddress(linkAddress)
                    .setGateway(gatewayAddress)
                    .setDnsServers(dnsServers)
                    .build();

                if (uniqueToken != null) {
                    IpConfiguration config = new IpConfiguration();
                    networks.put(uniqueToken, config);

                    switch (ipAssignment) {
                        case STATIC:
                            config.setStaticIpConfiguration(staticIpConfiguration);
                            config.setIpAssignment(ipAssignment);
                            break;
                        case DHCP:
                            config.setIpAssignment(ipAssignment);
                            break;
                        case UNASSIGNED:
                            loge("BUG: Found UNASSIGNED IP on file, use DHCP");
                            config.setIpAssignment(IpAssignment.DHCP);
                            break;
                        default:
                            loge("Ignore invalid ip assignment while reading.");
                            config.setIpAssignment(IpAssignment.UNASSIGNED);
                            break;
                    }

                    switch (proxySettings) {
                        case STATIC:
                            ProxyInfo proxyInfo = ProxyInfo.buildDirectProxy(proxyHost, proxyPort,
                                    ProxyUtils.exclusionStringAsList(exclusionList));
                            config.setProxySettings(proxySettings);
                            config.setHttpProxy(proxyInfo);
                            break;
                        case PAC:
                            ProxyInfo proxyPacProperties =
                                    ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
                            config.setProxySettings(proxySettings);
                            config.setHttpProxy(proxyPacProperties);
                            break;
                        case NONE:
                            config.setProxySettings(proxySettings);
                            break;
                        case UNASSIGNED:
                            loge("BUG: Found UNASSIGNED proxy on file, use NONE");
                            config.setProxySettings(ProxySettings.NONE);
                            break;
                        default:
                            loge("Ignore invalid proxy settings while reading");
                            config.setProxySettings(ProxySettings.UNASSIGNED);
                            break;
                    }
                } else {
                    if (DBG) log("Missing id while parsing configuration");
                }
            }
        } catch (EOFException ignore) {
        } catch (IOException e) {
            loge("Error parsing configuration: " + e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception e) { }
            }
        }

        return networks;
    }

    protected static void loge(String s) {
        Log.e(TAG, s);
    }

    protected static void log(String s) {
        Log.d(TAG, s);
    }
}
