/*
 * Copyright (C) 2010 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.tests.getinfo;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;

/** Crawls /proc to find processes that are running as root. */
class RootProcessScanner {

    /** Processes that are allowed to run as root. */
    private static final Pattern ROOT_PROCESS_WHITELIST_PATTERN = getRootProcessWhitelistPattern(
            "debuggerd",
            "debuggerd64",
            "healthd",
            "init",
            "installd",
            "lmkd",
            "netd",
            "servicemanager",
            "ueventd",
            "vold",
            "watchdogd",
            "zygote"
    );

    /** Combine the individual patterns into one super pattern. */
    private static Pattern getRootProcessWhitelistPattern(String... patterns) {
        StringBuilder rootProcessPattern = new StringBuilder();
        for (int i = 0; i < patterns.length; i++) {
            rootProcessPattern.append(patterns[i]);
            if (i + 1 < patterns.length) {
                rootProcessPattern.append('|');
            }
        }
        return Pattern.compile(rootProcessPattern.toString());
    }

    /** Test that there are no unapproved root processes running on the system. */
    public static String[] getRootProcesses()
            throws FileNotFoundException, MalformedStatMException {
        List<File> rootProcessDirs = getRootProcessDirs();
        String[] rootProcessNames = new String[rootProcessDirs.size()];
        for (int i = 0; i < rootProcessNames.length; i++) {
            rootProcessNames[i] = getProcessName(rootProcessDirs.get(i));
        }
        return rootProcessNames;
    }

    private static List<File> getRootProcessDirs()
            throws FileNotFoundException, MalformedStatMException {
        File proc = new File("/proc");
        if (!proc.exists()) {
            throw new FileNotFoundException(proc + " is missing (man 5 proc)");
        }

        List<File> rootProcesses = new ArrayList<File>();
        File[] processDirs = proc.listFiles();
        if (processDirs != null && processDirs.length > 0) {
            for (File processDir : processDirs) {
                if (isUnapprovedRootProcess(processDir)) {
                    rootProcesses.add(processDir);
                }
            }
        }
        return rootProcesses;
    }

    /**
     * Filters out processes in /proc that are not approved.
     * @throws FileNotFoundException
     * @throws MalformedStatMException
     */
    private static boolean isUnapprovedRootProcess(File pathname)
            throws FileNotFoundException, MalformedStatMException {
        return isPidDirectory(pathname)
                && !isKernelProcess(pathname)
                && isRootProcess(pathname);
    }

    private static boolean isPidDirectory(File pathname) {
        return pathname.isDirectory() && Pattern.matches("\\d+", pathname.getName());
    }

    private static boolean isKernelProcess(File processDir)
            throws FileNotFoundException, MalformedStatMException {
        File statm = getProcessStatM(processDir);
        Scanner scanner = null;
        try {
            scanner = new Scanner(statm);

            boolean allZero = true;
            for (int i = 0; i < 7; i++) {
                if (scanner.nextInt() != 0) {
                    allZero = false;
                }
            }

            if (scanner.hasNext()) {
                throw new MalformedStatMException(processDir
                        + " statm expected to have 7 integers (man 5 proc)");
            }

            return allZero;
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }

    private static File getProcessStatM(File processDir) {
        return new File(processDir, "statm");
    }

    public static class MalformedStatMException extends Exception {
        MalformedStatMException(String detailMessage) {
            super(detailMessage);
        }
    }

    /**
     * Return whether or not this process is running as root without being approved.
     *
     * @param processDir with the status file
     * @return whether or not it is a unwhitelisted root process
     * @throws FileNotFoundException
     */
    private static boolean isRootProcess(File processDir) throws FileNotFoundException {
        File status = getProcessStatus(processDir);
        Scanner scanner = null;
        try {
            scanner = new Scanner(status);

            findToken(scanner, "Name:");
            String name = scanner.next();

            findToken(scanner, "Uid:");
            boolean rootUid = hasRootId(scanner);

            findToken(scanner, "Gid:");
            boolean rootGid = hasRootId(scanner);

            return !ROOT_PROCESS_WHITELIST_PATTERN.matcher(name).matches()
                    && (rootUid || rootGid);
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }

    /**
     * Get the status {@link File} that has name:value pairs.
     * <pre>
     * Name:   init
     * ...
     * Uid:    0       0       0       0
     * Gid:    0       0       0       0
     * </pre>
     */
    private static File getProcessStatus(File processDir) {
        return new File(processDir, "status");
    }

    /**
     * Convenience method to move the scanner's position to the point after the given token.
     *
     * @param scanner to call next() until the token is found
     * @param token to find like "Name:"
     */
    private static void findToken(Scanner scanner, String token) {
        while (true) {
            String next = scanner.next();
            if (next.equals(token)) {
                return;
            }
        }

        // Scanner will exhaust input and throw an exception before getting here.
    }

    /**
     * Uid and Gid lines have four values: "Uid:    0       0       0       0"
     *
     * @param scanner that has just processed the "Uid:" or "Gid:" token
     * @return whether or not any of the ids are root
     */
    private static boolean hasRootId(Scanner scanner) {
        int realUid = scanner.nextInt();
        int effectiveUid = scanner.nextInt();
        int savedSetUid = scanner.nextInt();
        int fileSystemUid = scanner.nextInt();
        return realUid == 0 || effectiveUid == 0 || savedSetUid == 0 || fileSystemUid == 0;
    }

    /** Returns the name of the process corresponding to its process directory in /proc. */
    private static String getProcessName(File processDir) throws FileNotFoundException {
        File status = getProcessStatus(processDir);
        Scanner scanner = new Scanner(status);
        try {
            findToken(scanner, "Name:");
            return scanner.next();
        } finally {
            scanner.close();
        }
    }
}
