/*
 * Copyright (C) 2021 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.pm.verify.domain.proxy;

import static android.os.PowerWhitelistManager.REASON_DOMAIN_VERIFICATION_V1;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.BroadcastOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainVerificationInfo;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationState;
import android.os.Process;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.pm.verify.domain.DomainVerificationCollector;
import com.android.server.pm.verify.domain.DomainVerificationDebug;
import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
import com.android.server.pm.verify.domain.DomainVerificationMessageCodes;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

public class DomainVerificationProxyV1 implements DomainVerificationProxy {

    private static final String TAG = "DomainVerificationProxyV1";

    private static final boolean DEBUG_BROADCASTS = DomainVerificationDebug.DEBUG_BROADCASTS;

    @NonNull
    private final Context mContext;

    @NonNull
    private final Connection mConnection;

    @NonNull
    private final ComponentName mVerifierComponent;

    @NonNull
    private final DomainVerificationManagerInternal mManager;

    @NonNull
    private final DomainVerificationCollector mCollector;

    @NonNull
    private final Object mLock = new Object();

    @NonNull
    @GuardedBy("mLock")
    private final ArrayMap<Integer, Pair<UUID, String>> mRequests = new ArrayMap<>();

    @GuardedBy("mLock")
    private int mVerificationToken = 0;

    public DomainVerificationProxyV1(@NonNull Context context,
            @NonNull DomainVerificationManagerInternal manager,
            @NonNull DomainVerificationCollector collector, @NonNull Connection connection,
            @NonNull ComponentName verifierComponent) {
        mContext = context;
        mConnection = connection;
        mVerifierComponent = verifierComponent;
        mManager = manager;
        mCollector = collector;
    }

    public static void queueLegacyVerifyResult(@NonNull Context context,
            @NonNull DomainVerificationProxyV1.Connection connection, int verificationId,
            int verificationCode, @Nullable List<String> failedDomains, int callingUid) {
        context.enforceCallingOrSelfPermission(
                Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT,
                "Only the intent filter verification agent can verify applications");

        connection.schedule(DomainVerificationMessageCodes.LEGACY_ON_INTENT_FILTER_VERIFIED,
                new Response(callingUid, verificationId, verificationCode, failedDomains));
    }

    @Override
    public void sendBroadcastForPackages(@NonNull Set<String> packageNames) {
        synchronized (mLock) {
            int size = mRequests.size();
            for (int index = size - 1; index >= 0; index--) {
                Pair<UUID, String> pair = mRequests.valueAt(index);
                if (packageNames.contains(pair.second)) {
                    mRequests.removeAt(index);
                }
            }
        }
        mConnection.schedule(DomainVerificationMessageCodes.LEGACY_SEND_REQUEST, packageNames);
    }

    @SuppressWarnings("deprecation")
    @Override
    public boolean runMessage(int messageCode, Object object) {
        switch (messageCode) {
            case DomainVerificationMessageCodes.LEGACY_SEND_REQUEST:
                @SuppressWarnings("unchecked") Set<String> packageNames = (Set<String>) object;
                if (DEBUG_BROADCASTS) {
                    Slog.d(TAG, "Requesting domain verification for " + packageNames);
                }

                ArrayMap<Integer, Pair<UUID, String>> newRequests = new ArrayMap<>(
                        packageNames.size());
                synchronized (mLock) {
                    for (String packageName : packageNames) {
                        UUID domainSetId = mManager.getDomainVerificationInfoId(packageName);
                        if (domainSetId == null) {
                            continue;
                        }

                        newRequests.put(mVerificationToken++,
                                Pair.create(domainSetId, packageName));
                    }
                    mRequests.putAll(newRequests);
                }

                sendBroadcasts(newRequests);
                return true;
            case DomainVerificationMessageCodes.LEGACY_ON_INTENT_FILTER_VERIFIED:
                Response response = (Response) object;

                Pair<UUID, String> pair = mRequests.get(response.verificationId);
                if (pair == null) {
                    return true;
                }

                UUID domainSetId = pair.first;
                String packageName = pair.second;
                DomainVerificationInfo info;
                try {
                    info = mManager.getDomainVerificationInfo(packageName);
                } catch (PackageManager.NameNotFoundException ignored) {
                    return true;
                }

                if (info == null) {
                    return true;
                }

                if (!Objects.equals(domainSetId, info.getIdentifier())) {
                    return true;
                }

                AndroidPackage pkg = mConnection.getPackage(packageName);
                if (pkg == null) {
                    return true;
                }

                ArraySet<String> failedDomains = new ArraySet<>(response.failedDomains);
                Map<String, Integer> hostToStateMap = info.getHostToStateMap();
                Set<String> hostKeySet = hostToStateMap.keySet();
                ArraySet<String> successfulDomains = new ArraySet<>(hostKeySet);
                successfulDomains.removeAll(failedDomains);

                // v1 doesn't handle wildcard domains, so check them here for the verifier
                int size = successfulDomains.size();
                for (int index = size - 1; index >= 0; index--) {
                    String domain = successfulDomains.valueAt(index);
                    if (domain.startsWith("*.")) {
                        String nonWildcardDomain = domain.substring(2);
                        if (failedDomains.contains(nonWildcardDomain)) {
                            failedDomains.add(domain);
                            successfulDomains.removeAt(index);

                            // It's possible to declare a wildcard without declaring its
                            // non-wildcard equivalent, so if it wasn't originally declared,
                            // remove the transformed domain from the failed set. Otherwise the
                            // manager will not accept the failed set as it contains an undeclared
                            // domain.
                            if (!hostKeySet.contains(nonWildcardDomain)) {
                                failedDomains.remove(nonWildcardDomain);
                            }
                        }
                    }
                }

                int callingUid = response.callingUid;
                if (!successfulDomains.isEmpty()) {
                    try {
                        if (mManager.setDomainVerificationStatusInternal(callingUid, domainSetId,
                                successfulDomains, DomainVerificationState.STATE_SUCCESS)
                                != DomainVerificationManager.STATUS_OK) {
                            Slog.e(TAG, "Failure reporting successful domains for " + packageName);
                        }
                    } catch (Exception e) {
                        Slog.e(TAG, "Failure reporting successful domains for " + packageName, e);
                    }
                }

                if (!failedDomains.isEmpty()) {
                    try {
                        if (mManager.setDomainVerificationStatusInternal(callingUid, domainSetId,
                                failedDomains, DomainVerificationState.STATE_LEGACY_FAILURE)
                                != DomainVerificationManager.STATUS_OK) {
                            Slog.e(TAG, "Failure reporting failed domains for " + packageName);
                        }
                    } catch (Exception e) {
                        Slog.e(TAG, "Failure reporting failed domains for " + packageName, e);
                    }
                }

                return true;
            default:
                return false;
        }
    }

    @Override
    public boolean isCallerVerifier(int callingUid) {
        return mConnection.isCallerPackage(callingUid, mVerifierComponent.getPackageName());
    }

    @SuppressWarnings("deprecation")
    private void sendBroadcasts(@NonNull ArrayMap<Integer, Pair<UUID, String>> verifications) {
        final long allowListTimeout = mConnection.getPowerSaveTempWhitelistAppDuration();
        mConnection.getDeviceIdleInternal().addPowerSaveTempWhitelistApp(Process.myUid(),
                mVerifierComponent.getPackageName(), allowListTimeout,
                UserHandle.USER_SYSTEM, true, REASON_DOMAIN_VERIFICATION_V1,
                "domain verification agent");

        int size = verifications.size();
        for (int index = 0; index < size; index++) {
            int verificationId = verifications.keyAt(index);
            String packageName = verifications.valueAt(index).second;
            AndroidPackage pkg = mConnection.getPackage(packageName);

            String hostsString = buildHostsString(pkg);

            Intent intent = new Intent(Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION)
                    .setComponent(mVerifierComponent)
                    .putExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID,
                            verificationId)
                    .putExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME,
                            IntentFilter.SCHEME_HTTPS)
                    .putExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS,
                            hostsString)
                    .putExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME,
                            packageName)
                    .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);

            final BroadcastOptions options = BroadcastOptions.makeBasic();
            options.setTemporaryAppAllowlist(allowListTimeout,
                    TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                    REASON_DOMAIN_VERIFICATION_V1, "");
            mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null, options.toBundle());
        }
    }

    @NonNull
    private String buildHostsString(@NonNull AndroidPackage pkg) {
        // The collector itself handles the v1 vs v2 behavior, which is based on targetSdkVersion,
        // not the version of the verification agent on device.
        ArraySet<String> domains = mCollector.collectValidAutoVerifyDomains(pkg);

        // v1 doesn't handle wildcard domains, so transform them here to the root
        StringBuilder builder = new StringBuilder();
        int size = domains.size();
        for (int index = 0; index < size; index++) {
            if (index > 0) {
                builder.append(" ");
            }
            String domain = domains.valueAt(index);
            if (domain.startsWith("*.")) {
                domain = domain.substring(2);
            }
            builder.append(domain);
        }
        return builder.toString();
    }

    @NonNull
    @Override
    public ComponentName getComponentName() {
        return mVerifierComponent;
    }

    private static class Response {
        public final int callingUid;
        public final int verificationId;
        public final int verificationCode;
        @NonNull
        public final List<String> failedDomains;

        private Response(int callingUid, int verificationId, int verificationCode,
                @Nullable List<String> failedDomains) {
            this.callingUid = callingUid;
            this.verificationId = verificationId;
            this.verificationCode = verificationCode;
            this.failedDomains = failedDomains == null ? Collections.emptyList() : failedDomains;
        }
    }

    public interface Connection extends BaseConnection {

        @Nullable
        AndroidPackage getPackage(@NonNull String packageName);
    }
}
