/*
 * Copyright (C) 2019 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.ipsec.ike;

import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE;
import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP;
import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4;
import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6;
import static android.net.ipsec.ike.IkeSessionParams.IKE_NATT_KEEPALIVE_DELAY_SEC_MAX;
import static android.net.ipsec.ike.IkeSessionParams.IKE_NATT_KEEPALIVE_DELAY_SEC_MIN;

import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.IpSecManager;
import android.net.Network;
import android.net.ipsec.ike.exceptions.IkeException;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.CloseGuard;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.ipsec.ike.IkeSessionStateMachine;

import java.io.PrintWriter;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * This class represents an IKE Session management object that allows for keying and management of
 * {@link android.net.IpSecTransform}s.
 *
 * <p>An IKE/Child Session represents an IKE/Child SA as well as its rekeyed successors. A Child
 * Session is bounded by the lifecycle of the IKE Session under which it is set up. Closing an IKE
 * Session implicitly closes any remaining Child Sessions under it.
 *
 * <p>An IKE procedure is one or multiple IKE message exchanges that are used to create, delete or
 * rekey an IKE Session or Child Session.
 *
 * <p>This class provides methods for initiating IKE procedures, such as the Creation and Deletion
 * of a Child Session, or the Deletion of the IKE session. All procedures (except for IKE deletion)
 * will be initiated sequentially after IKE Session is set up.
 *
 * @see <a href="https://tools.ietf.org/html/rfc7296">RFC 7296, Internet Key Exchange Protocol
 *     Version 2 (IKEv2)</a>
 */
public final class IkeSession implements AutoCloseable {
    private final CloseGuard mCloseGuard = new CloseGuard();
    private final Context mContext;

    /**
     * Attribution tag for IWLAN callers
     *
     * @hide
     */
    public static final String CONTEXT_ATTRIBUTION_TAG_IWLAN = "IWLAN";

    /**
     * Attribution tag for VCN callers
     *
     * @hide
     */
    public static final String CONTEXT_ATTRIBUTION_TAG_VCN = "VCN";

    /**
     * Attribution tag for VPN callers
     *
     * @hide
     */
    public static final String CONTEXT_ATTRIBUTION_TAG_VPN = "VPN_MANAGER";

    @VisibleForTesting final IkeSessionStateMachine mIkeSessionStateMachine;

    /**
     * Constructs a new IKE session.
     *
     * <p>This method will immediately return an instance of {@link IkeSession} and asynchronously
     * initiate the setup procedure of {@link IkeSession} as well as its first Child Session.
     * Callers will be notified of these two setup results via the callback arguments.
     *
     * <p>FEATURE_IPSEC_TUNNELS is required for setting up a tunnel mode Child SA.
     *
     * @param context a valid {@link Context} instance.
     * @param ikeSessionParams the {@link IkeSessionParams} that contains a set of valid {@link
     *     IkeSession} configurations.
     * @param firstChildSessionParams the {@link ChildSessionParams} that contains a set of valid
     *     configurations for the first Child Session.
     * @param userCbExecutor the {@link Executor} upon which all callbacks will be posted. For
     *     security and consistency, the callbacks posted to this executor MUST be executed serially
     *     and in the order they were posted, as guaranteed by executors such as {@link
     *     java.util.concurrent.Executors#newSingleThreadExecutor()}
     * @param ikeSessionCallback the {@link IkeSessionCallback} interface to notify callers of state
     *     changes within the {@link IkeSession}.
     * @param firstChildSessionCallback the {@link ChildSessionCallback} interface to notify callers
     *     of state changes within the first Child Session.
     * @return an instance of {@link IkeSession}.
     */
    public IkeSession(
            @NonNull Context context,
            @NonNull IkeSessionParams ikeSessionParams,
            @NonNull ChildSessionParams firstChildSessionParams,
            @NonNull Executor userCbExecutor,
            @NonNull IkeSessionCallback ikeSessionCallback,
            @NonNull ChildSessionCallback firstChildSessionCallback) {
        this(
                context,
                (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE),
                ikeSessionParams,
                firstChildSessionParams,
                userCbExecutor,
                ikeSessionCallback,
                firstChildSessionCallback);
    }

    /** Package private */
    @VisibleForTesting
    IkeSession(
            Context context,
            IpSecManager ipSecManager,
            IkeSessionParams ikeSessionParams,
            ChildSessionParams firstChildSessionParams,
            Executor userCbExecutor,
            IkeSessionCallback ikeSessionCallback,
            ChildSessionCallback firstChildSessionCallback) {
        this(
                IkeThreadHolder.IKE_WORKER_THREAD.getLooper(),
                context,
                ipSecManager,
                ikeSessionParams,
                firstChildSessionParams,
                userCbExecutor,
                ikeSessionCallback,
                firstChildSessionCallback);
    }

    /** Package private */
    @VisibleForTesting
    IkeSession(
            Looper looper,
            Context context,
            IpSecManager ipSecManager,
            IkeSessionParams ikeSessionParams,
            ChildSessionParams firstChildSessionParams,
            Executor userCbExecutor,
            IkeSessionCallback ikeSessionCallback,
            ChildSessionCallback firstChildSessionCallback) {
        mContext = context;

        if (firstChildSessionParams instanceof TunnelModeChildSessionParams) {
            checkTunnelFeatureOrThrow(mContext);
        }

        mIkeSessionStateMachine =
                new IkeSessionStateMachine(
                        looper,
                        context,
                        ipSecManager,
                        ikeSessionParams,
                        firstChildSessionParams,
                        userCbExecutor,
                        ikeSessionCallback,
                        firstChildSessionCallback);
        mIkeSessionStateMachine.openSession();

        mCloseGuard.open("open");
    }

    /** @hide */
    @Override
    public void finalize() {
        if (mCloseGuard != null) {
            mCloseGuard.warnIfOpen();
        }
    }

    private void checkTunnelFeatureOrThrow(Context context) {
        // TODO(b/157754168): Also check if OP_MANAGE_IPSEC_TUNNELS is granted when it is exposed
        if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) {
            throw new IllegalStateException(
                    "Cannot set up tunnel mode Child SA due to FEATURE_IPSEC_TUNNELS missing");
        }
    }

    /** Initialization-on-demand holder */
    private static class IkeThreadHolder {
        static final HandlerThread IKE_WORKER_THREAD;

        static {
            IKE_WORKER_THREAD = new HandlerThread("IkeWorkerThread");
            IKE_WORKER_THREAD.start();
        }
    }

    // TODO: b/133340675 Destroy the worker thread when there is no more alive {@link IkeSession}.

    /**
     * Request a new Child Session.
     *
     * <p>Users MUST provide a unique {@link ChildSessionCallback} instance for each new Child
     * Session.
     *
     * <p>Upon setup, {@link ChildSessionCallback#onOpened(ChildSessionConfiguration)} will be
     * fired.
     *
     * <p>FEATURE_IPSEC_TUNNELS is required for setting up a tunnel mode Child SA.
     *
     * @param childSessionParams the {@link ChildSessionParams} that contains the Child Session
     *     configurations to negotiate.
     * @param childSessionCallback the {@link ChildSessionCallback} interface to notify users the
     *     state changes of the Child Session. It will be posted to the callback {@link Executor} of
     *     this {@link IkeSession}.
     * @throws IllegalArgumentException if the ChildSessionCallback is already in use.
     */
    // The childSessionCallback will be called on the same executor as was passed in the constructor
    // for security reasons.
    @SuppressLint("ExecutorRegistration")
    public void openChildSession(
            @NonNull ChildSessionParams childSessionParams,
            @NonNull ChildSessionCallback childSessionCallback) {
        if (childSessionParams instanceof TunnelModeChildSessionParams) {
            checkTunnelFeatureOrThrow(mContext);
        }

        mIkeSessionStateMachine.openChildSession(childSessionParams, childSessionCallback);
    }

    /**
     * Delete a Child Session.
     *
     * <p>Upon closure, {@link ChildSessionCallback#onClosed()} will be fired.
     *
     * @param childSessionCallback The {@link ChildSessionCallback} instance that uniquely identify
     *     the Child Session.
     * @throws IllegalArgumentException if no Child Session found bound with this callback.
     */
    // The childSessionCallback will be called on the same executor as was passed in the constructor
    // for security reasons.
    @SuppressLint("ExecutorRegistration")
    public void closeChildSession(@NonNull ChildSessionCallback childSessionCallback) {
        mIkeSessionStateMachine.closeChildSession(childSessionCallback);
    }

    /**
     * Close the IKE session gracefully.
     *
     * <p>Implements {@link AutoCloseable#close()}
     *
     * <p>Upon closure, {@link IkeSessionCallback#onClosed()} or {@link
     * IkeSessionCallback#onClosedWithException(IkeException)} will be fired.
     *
     * <p>Closing an IKE Session implicitly closes any remaining Child Sessions negotiated under it.
     * Users SHOULD stop all outbound traffic that uses these Child Sessions ({@link
     * android.net.IpSecTransform} pairs) before calling this method. Otherwise IPsec packets will
     * be dropped due to the lack of a valid {@link android.net.IpSecTransform}.
     *
     * <p>Closure of an IKE session will take priority over, and cancel other procedures waiting in
     * the queue (but will wait for ongoing locally initiated procedures to complete). After sending
     * the Delete request, the IKE library will wait until a Delete response is received or
     * retransmission timeout occurs.
     */
    @Override
    public void close() {
        mCloseGuard.close();
        mIkeSessionStateMachine.closeSession();
    }

    /**
     * Terminate (forcibly close) the IKE session.
     *
     * <p>Upon closing, {@link IkeSessionCallback#onClosed()} will be fired.
     *
     * <p>Closing an IKE Session implicitly closes any remaining Child Sessions negotiated under it.
     * Users SHOULD stop all outbound traffic that uses these Child Sessions ({@link
     * android.net.IpSecTransform} pairs) before calling this method. Otherwise IPsec packets will
     * be dropped due to the lack of a valid {@link android.net.IpSecTransform}.
     *
     * <p>Forcible closure of an IKE session will take priority over, and cancel other procedures
     * waiting in the queue. It will also interrupt any ongoing locally initiated procedure.
     */
    public void kill() {
        mCloseGuard.close();
        mIkeSessionStateMachine.killSession();
    }

    /**
     * Update the IkeSession's underlying Network to use the specified Network.
     *
     * @see #setNetwork(Network, int, int)
     * @hide
     */
    @SystemApi
    public void setNetwork(@NonNull Network network) {
        setNetwork(network, IkeSessionParams.ESP_IP_VERSION_AUTO,
                IkeSessionParams.ESP_ENCAP_TYPE_AUTO,
                IkeSessionParams.NATT_KEEPALIVE_INTERVAL_AUTO);
    }

    /**
     * Update the IkeSession's underlying Network, protocol preference and keepalive delay.
     *
     * <p>Updating the IkeSession's Network also updates the Network for any Child Sessions created
     * with this IkeSession. To perform the update, callers must implement:
     *
     * <ul>
     *   <li>{@link IkeSessionCallback#onIkeSessionConnectionInfoChanged(IkeSessionConnectionInfo)}:
     *       This call will be triggered once the IKE Session has been updated. The implementation
     *       MUST migrate all IpSecTunnelInterface instances associated with this IkeSession via
     *       {@link android.net.IpSecManager.IpSecTunnelInterface#setUnderlyingNetwork(Network)}
     *   <li>{@link ChildSessionCallback#onIpSecTransformsMigrated(android.net.IpSecTransform,
     *       android.net.IpSecTransform)}: This call will be triggered once a Child Session has been
     *       updated. The implementation MUST re-apply the migrated transforms to the {@link
     *       android.net.IpSecManager.IpSecTunnelInterface} associated with this
     *       ChildSessionCallback, via {@link android.net.IpSecManager#applyTunnelModeTransform(
     *       android.net.IpSecManager.IpSecTunnelInterface, int, android.net.IpSecTransform)}.
     * </ul>
     *
     * <p>In order for Network migration to be possible, the following must be true:
     *
     * <ul>
     *   <li>the {@link IkeSessionParams} for this IkeSession must be configured with {@link
     *       IkeSessionParams#IKE_OPTION_MOBIKE} (set via {@link
     *       IkeSessionParams.Builder#addIkeOption(int)}), and
     *   <li>the IkeSession must have been started with the Network specified via {@link
     *       IkeSessionParams.Builder#setNetwork(Network)}.
     * </ul>
     *
     * <p>As MOBIKE support is negotiated, callers are advised to check for MOBIKE support in {@link
     * IkeSessionConfiguration} before calling this method to update the network. Failure to do so
     * may cause this call to be ignored.
     *
     * @see <a href="https://tools.ietf.org/html/rfc4555">RFC 4555, IKEv2 Mobility and Multihoming
     *     Protocol (MOBIKE)</a>
     * @param network the Network to use for this IkeSession
     * @param ipVersion the IP version to use for ESP packets
     * @param encapType the encapsulation type to use for ESP packets
     * @param keepaliveDelaySeconds the keepalive delay in seconds, or NATT_KEEPALIVE_INTERVAL_AUTO
     *                              to choose the value automatically based on the network.
     * @throws IllegalStateException if {@link IkeSessionParams#IKE_OPTION_MOBIKE} is not configured
     *     in IkeSessionParams, or if the Network was not specified in IkeSessionParams.
     * @throws UnsupportedOperationException if the provided option is not supported.
     * @see IkeSessionParams#getNattKeepAliveDelaySeconds()
     * @hide
     */
    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
    public void setNetwork(
            @NonNull Network network,
            @IkeSessionParams.EspIpVersion int ipVersion,
            @IkeSessionParams.EspEncapType int encapType,
            // Is there a way to specify an intrange + a sentinel value ?
            @IntRange(
                    from = IKE_NATT_KEEPALIVE_DELAY_SEC_MIN,
                    to = IKE_NATT_KEEPALIVE_DELAY_SEC_MAX)
            int keepaliveDelaySeconds) {
        if ((ipVersion ==  ESP_IP_VERSION_IPV4 && encapType == ESP_ENCAP_TYPE_NONE)
                || (ipVersion == ESP_IP_VERSION_IPV6 && encapType == ESP_ENCAP_TYPE_UDP)) {
            throw new UnsupportedOperationException("Sending packets with IPv4 ESP or IPv6 UDP"
                    + " are not supported");
        }

        mIkeSessionStateMachine.setNetwork(Objects.requireNonNull(network),
                ipVersion, encapType, keepaliveDelaySeconds);
    }

    /**
     * Inform the session that it is used to supply the passed network.
     *
     * This will be used by the session when it needs to perform actions that depend on what
     * network this session is underpinning. In particular, this can be used to turn off
     * keepalives when there are no connections open on the underpinned network, if the
     * {@link IkeSessionParams#IKE_OPTION_AUTOMATIC_KEEPALIVE_ON_OFF} option is turned on.
     *
     * @param underpinnedNetwork the network underpinned by this session.
     * @hide
     */
    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
    public void setUnderpinnedNetwork(@NonNull Network underpinnedNetwork) {
        mIkeSessionStateMachine.setUnderpinnedNetwork(Objects.requireNonNull(underpinnedNetwork));
    }

    /**
     * Request to check liveness of peer.
     *
     * <p>This method returns immediately and asynchronously,
     *
     * <p>The liveness check determines whether a peer is alive by executing a new on-demand DPD
     * task or joining an existing running task depending on the situation.
     *
     * <ul>
     *   <li>If there is no running task when a liveness check request is called, a new on-demand
     *       DPD task is started. The on-demand DPD (Dead Peer Detection) is used for checking
     *       liveness of peer in this case. This method adds an on-demand DPD request to the work
     *       queue to check that the peer is alive. The on-demand DPD uses retransmit timeouts from
     *       {@link IkeSessionParams#getLivenessRetransmissionTimeoutsMillis}.
     *   <li>If any IKE message is already in progress when a client requests a liveness check, the
     *       liveness check request is joined to an existing running task. And then the liveness
     *       check runs in the background. When a running task receives a valid IKE message packet
     *       from a peer, it can verify that the peer is alive in the background without triggering
     *       an on-demand DPD task. A running task uses retransmit timeouts from {@link
     *       IkeSessionParams#getRetransmissionTimeoutsMillis}.
     * </ul>
     *
     * <p>The client is notified of the progress or result statuses of the liveness check via {@link
     * IkeSessionCallback#onLivenessStatusChanged}. These statuses are notified after a liveness
     * check request is started. By notifying {@link
     * IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_SUCCESS} or {@link
     * IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_FAILURE}, the liveness check request is
     * done and no further status notification is made until the next {@link
     * IkeSession#requestLivenessCheck}. The status notifications to the client are as follows.
     *
     * <ul>
     *   <li>{@link IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_ON_DEMAND_STARTED}: This
     *       status is called when liveness checking is started with a new on-demand DPD task.
     *   <li>{@link IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_ON_DEMAND_ONGOING}: This
     *       status is called when liveness checking is already running in an on-demand DPD task.
     *   <li>{@link IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_BACKGROUND_STARTED}: This
     *       status is called when liveness checking is started in the background and has joined an
     *       existing running task.
     *   <li>{@link IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_BACKGROUND_ONGOING}: This
     *       status is called when liveness checking is already running in the background by joining
     *       an existing running task.
     *   <li>{@link IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_SUCCESS}: This status is
     *       called when the peer's liveness is proven. Once this status is notified, the liveness
     *       check request is done and no further status notification is made until the next {@link
     *       IkeSession#requestLivenessCheck}.
     *   <li>{@link IkeSessionCallback.LivenessStatus#LIVENESS_STATUS_FAILURE}: This state is called
     *       when the peer is determined as dead for a liveness check request. After this status is
     *       notified, the IkeSession will be closed immediately by calling {@link
     *       IkeSessionCallback#onClosedWithException} with {@link
     *       android.net.ipsec.ike.exceptions.IkeTimeoutException} in the {@link
     *       IkeException#getCause()}.
     * </ul>
     *
     * <p>If a valid IKE message response is received from the peer, the IkeSession remains as
     * connected and periodic DPD reschedules by {@link IkeSessionParams#getDpdDelaySeconds}
     *
     * <p>If the liveness check request couldn't get any a peer's valid response in retransmission
     * timeout, The IkeSession will be closed. Session closing is also notified to {@link
     * IkeSessionCallback#onClosedWithException} with {@link
     * android.net.ipsec.ike.exceptions.IkeTimeoutException} cause.
     *
     * @hide
     */
    @SystemApi
    @FlaggedApi("com.android.ipsec.flags.liveness_check_api")
    public void requestLivenessCheck() {
        mIkeSessionStateMachine.requestLivenessCheck();
    }

    /**
     * Dumps the state of {@link IkeSession} information for the clients
     *
     * @param pw Print writer
     */
    @FlaggedApi("com.android.ipsec.flags.dumpsys_api")
    public void dump(@NonNull PrintWriter pw) {
        // TODO(b/336409878): Add @RequiresPermission annotation.
        mContext.enforceCallingOrSelfPermission(
                android.Manifest.permission.DUMP, mContext.getAttributionTag());

        // Please make sure that the dump is thread-safe
        // so the client won't get a crash or exception when adding codes to the dump.
        pw.println();
        pw.println("IkeSession:");
        pw.println("------------------------------");
        // Dump ike state machine.
        if (mIkeSessionStateMachine != null) {
            pw.println();
            mIkeSessionStateMachine.dump(pw);
        }
        pw.println("------------------------------");
    }
}