org.lantern.MacProxyManager.java Source code

Java tutorial

Introduction

Here is the source code for org.lantern.MacProxyManager.java

Source

/*
 * Copyright 2007 ThoughtWorks, Inc.
 *
 *  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 org.lantern;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.prefs.Preferences;
import java.util.regex.Pattern;

import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//import org.openqa.selenium.server.browserlaunchers.LauncherUtils;

/**
 * Class to manage the proxy server on OS X.  It uses the 'networksetup' tool to do
 * its magic; it also depends on 'scutil' to read some settings we need to interact
 * with 'networksetup.'
 * 
 * <p>'networksetup' seems to come in a great many varieties depending on different
 * versions of OS X (and different architectures: PPC vs Intel), so we've taken
 * some care to write this class very defensively.</p>
 * @author Dan Fabulich
 *
 */
public class MacProxyManager {
    private static final Logger log = LoggerFactory.getLogger(MacProxyManager.class);

    private static final Pattern SCUTIL_LINE = Pattern.compile("^  (\\S+) : (.*)$");
    private static final Pattern NETWORKSETUP_LISTORDER_LINE = Pattern
            .compile("\\(Hardware Port: ([^,]*), Device: ([^\\)]*)\\)");
    private static final Pattern NETWORKSETUP_LINE = Pattern.compile("^([^:]+): (.*)$");

    private static final String BACKUP_READY = "backupready";

    private String sessionId;
    private File customProxyPACDir; // TODO evict this?
    private int port;
    // DGF used to be static/final, but that made it harder to mock out
    private Preferences prefs = Preferences.userNodeForPackage(MacProxyManager.class);
    /** The user defined name of the network service, used as an 
     * argument to 'networksetup', e.g. "Built-in Ethernet" 
     * or "AirPort". */
    private String networkService;

    public MacProxyManager(String sessionId, int port) {
        this.sessionId = sessionId;
        this.port = port;
        prefs = Preferences.userNodeForPackage(MacProxyManager.class);
    }

    public File getCustomProxyPACDir() {
        return customProxyPACDir;
    }

    private boolean prefNodeExists(String key) {
        return null != prefs.get(key, null);
    }

    /** change the network settings to enable use of our proxy */
    public void changeNetworkSettings() throws IOException {
        if (networkService == null) {
            getCurrentNetworkSettings();
        }
        customProxyPACDir = createCustomProfileDir(sessionId);
        if (customProxyPACDir.exists()) {
            try {
                FileUtils.deleteDirectory(customProxyPACDir);
            } catch (IOException e) {
                log.warn("Error deleting directory?", e);
            }
        }
        customProxyPACDir.mkdir();
        log.info("Modifying OS X global network settings...");
        // TODO Disable proxy PAC URL (or, even better, use one!) SRC-364

        runNetworkSetup("-setwebproxy", networkService, "localhost", "" + port);

        runNetworkSetup("-setproxybypassdomains", networkService, "Empty");
    }

    /**
     * creates an empty temp directory for managing a browser profile
     */
    // TODO(simon): Change this back to protected once moved into browserlaunchers
    public static File createCustomProfileDir(String sessionId) {
        final File customProfileDir;

        customProfileDir = customProfileDir(sessionId);
        if (customProfileDir.exists()) {
            try {
                FileUtils.deleteDirectory(customProfileDir);
            } catch (IOException e) {
                log.warn("Error deleting directory?", e);
            }
        }
        customProfileDir.mkdir();
        return customProfileDir;
    }

    /**
     * Return the name of the custom profile directory for a specific seleniumm session
     *
     * @param sessionId Current selenium sesssion id. Cannot be null.
     * @return file path of the custom profile directory for this session.
     */
    public static File customProfileDir(String sessionId) {
        final File tmpDir;
        final String customProfileDirParent;
        final File customProfileDir;

        tmpDir = new File(System.getProperty("java.io.tmpdir"));
        customProfileDirParent = ((tmpDir.exists() && tmpDir.isDirectory()) ? tmpDir.getAbsolutePath() : ".");
        customProfileDir = new File(customProfileDirParent + "/customProfileDir" + sessionId);

        return customProfileDir;
    }

    private String findNetworkSetupBin() {
        String defaultPath = "/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Support/networksetup";
        File defaultLocation = new File(defaultPath);
        if (defaultLocation.exists()) {
            return defaultLocation.getAbsolutePath();
        }
        String networkSetupBin = CommandLine.findExecutable("networksetup");
        if (networkSetupBin != null) {
            return networkSetupBin;
        }
        if (defaultLocation.getParentFile().exists()) {
            String[] files = defaultLocation.getParentFile().list();
            String guess = chooseSuitableNetworkSetup(System.getProperty("os.version"),
                    System.getProperty("os.arch"), files);
            if (guess != null) {
                File guessedLocation = new File(defaultLocation.getParentFile(), guess);
                log.error("Couldn't find 'networksetup' in expected location; we're taking " + "a guess and using "
                        + guessedLocation.getAbsolutePath()
                        + " instead.  Please create a symlink called 'networksetup' to make "
                        + "this warning go away.");
                return guessedLocation.getAbsolutePath();
            }
        }
        throw new MacNetworkSetupException("networksetup couldn't be found in the path!\n"
                + "Please add the directory containing 'networksetup' to your PATH environment\n" + "variable.");
    }

    /** Try to guess which 'networksetup' executable to use */
    private String chooseSuitableNetworkSetup(String osVersion, String osArch, String... files) {
        // DGF we don't technically need to know osArch, but according to comments in SRC-13,
        // sometimes Tiger on PPC looks different from Tiger on Intel, so we'll leave it in, 
        // just in case

        Set<String> candidates = new HashSet<String>();
        for (String file : files) {
            if (file.startsWith("networksetup-")) {
                candidates.add(file);
            }
        }
        if (candidates.isEmpty()) {
            log.trace("No networksetup candidates found");
            return null;
        }
        if (candidates.size() == 1) {
            log.trace("One networksetup candidate found");
            return candidates.iterator().next();
        }
        log.trace("Multiple networksetup candidates found: " + candidates);
        // uh-oh.  There's no 'networksetup' and more than one 'networksetup-*'
        // we'll have to take a guess!
        String[] versionParts = osVersion.split("\\.");
        if (versionParts.length < 2) {
            log.trace("OS version seems to be invalid: " + osVersion);
            return null;
        }
        if (!"10".equals(versionParts[0])) {
            log.trace("OS version doesn't seem to be 10.*: " + osVersion);
            return null;
        }
        CodeName codeName;
        try {
            codeName = CodeName.minorVersion(versionParts[1]);
            String possibleCandidate = "networksetup-" + codeName.name().toLowerCase();
            if (candidates.contains(possibleCandidate)) {
                log.trace("This seems to be " + codeName + ", so we'll use " + possibleCandidate);
                return possibleCandidate;
            }
            log.trace("This seems to be " + codeName + ", but there's no " + possibleCandidate);
        } catch (IllegalArgumentException e) {
            log.trace("Couldn't find code name for OS version " + osVersion);
            return null;
        }
        // DGF when we know there's multiple candidates, but none of them match, should we just pick one?
        return null;
    }

    private enum CodeName {
        PUMA("1"), JAGUAR("2"), PANTHER("3"), TIGER("4"), LEOPARD("5");

        String minorVersion;

        CodeName(String minorVersion) {
            this.minorVersion = minorVersion;
        }

        static CodeName minorVersion(String minorVersion) {
            for (CodeName cn : values()) {
                if (cn.minorVersion.equals(minorVersion)) {
                    return cn;
                }
            }
            throw new IllegalArgumentException("No codename matches minorVersion " + minorVersion);
        }
    }

    private String findScutilBin() {
        String defaultPath = "/usr/sbin/scutil";
        File defaultLocation = new File(defaultPath);
        if (defaultLocation.exists()) {
            return defaultLocation.getAbsolutePath();
        }
        String scutilBin = CommandLine.findExecutable("scutil");
        if (scutilBin != null) {
            return scutilBin;
        }
        throw new MacNetworkSetupException("scutil couldn't be found in the path!\n"
                + "Please add the directory containing 'scutil' to your PATH environment\n" + "variable.");
    }

    /** Acquire current network settings using scutil/networksetup */
    private MacNetworkSettings getCurrentNetworkSettings() {
        getPrimaryNetworkServiceName();
        String output = runNetworkSetup("-getwebproxy", networkService);
        log.trace(output);
        Map<String, String> dictionary = Maps.parseDictionary(output.toString(), NETWORKSETUP_LINE, false);
        String strEnabled = verifyKey("Enabled", dictionary, "networksetup", output);
        boolean enabled = isTrueOrSomething(strEnabled);
        String server = verifyKey("Server", dictionary, "networksetup", output);
        String strPort = verifyKey("Port", dictionary, "networksetup", output);
        int port1;
        try {
            port1 = Integer.parseInt(strPort);
        } catch (NumberFormatException e) {
            throw new MacNetworkSetupException("Port didn't look right: " + output, e);
        }
        String strAuth = verifyKey("Authenticated Proxy Enabled", dictionary, "networksetup", output);
        boolean auth = isTrueOrSomething(strAuth);
        String[] bypassDomains = getCurrentProxyBypassDomains();
        MacNetworkSettings networkSettings = new MacNetworkSettings(networkService, enabled, server, port1, auth,
                bypassDomains);
        return networkSettings;
    }

    private String[] getCurrentProxyBypassDomains() {
        String output = runNetworkSetup("-getproxybypassdomains", networkService);
        log.trace(output);
        if (output == null) {
            throw new MacNetworkSetupException("-getproxybypassdomains had no output");
        }
        String[] lines = output.split("\n");
        int i = 0;
        if (lines.length == i) {
            return new String[] { "" };
        }
        if (lines[i].startsWith("cp: /Library")) { // spurious warning when you don't run as root
            i++;
        }
        if (lines.length == i) {
            return new String[] { "" };
        }
        if (lines[i].startsWith("There aren't any")) {
            return new String[0];
        }
        if (i == 0)
            return lines;
        String[] domains = new String[lines.length - i];
        System.arraycopy(lines, i, domains, 0, lines.length - i);
        return domains;
    }

    private boolean isTrueOrSomething(String value) {
        // networksetup sometimes uses one of these; we don't really care which! 
        String[] matches = { "yes", "1", "true", "on" };
        for (String match : matches) {
            if (match.equalsIgnoreCase(value))
                return true;
        }
        return false;
    }

    private String verifyKey(String key, Map<String, String> dictionary, String executable, String output) {
        if (!dictionary.containsKey(key)) {
            throw new MacNetworkSetupException(
                    "Couldn't find " + key + " in " + executable + "; output: " + output);
        }
        return dictionary.get(key);
    }

    private String getPrimaryNetworkServiceName() {
        // TODO This would be faster (but harder to test?) if we just launched scutil once
        // and communicated with it line-by-line using stdin/stdout
        String output = runScutil("show State:/Network/Global/IPv4");
        log.trace(output);
        Map<String, String> dictionary = Maps.parseDictionary(output.toString(), SCUTIL_LINE, false);
        String primaryInterface = verifyKey("PrimaryInterface", dictionary, "scutil", output);
        output = runNetworkSetup("-listnetworkserviceorder");
        log.trace(output);
        dictionary = Maps.parseDictionary(output.toString(), NETWORKSETUP_LISTORDER_LINE, true);
        String userDefinedName = verifyKey(primaryInterface, dictionary, "networksetup -listnetworksetuporder",
                output);
        networkService = userDefinedName;
        return userDefinedName;

    }

    /** Execute scutil and quit, returning the output */
    protected String runScutil(String arg) {
        CommandLine command = new CommandLine(findScutilBin());
        command.setInput(arg + "\nquit\n");
        command.execute();
        String output = command.getStdOut();
        if (!command.isSuccessful()) {
            throw new RuntimeException("exec return code " + command.getExitCode() + ": " + output);
        }
        return output;
    }

    public String runScript(final String script, final String... args) {
        log.info("Got args: {}", Arrays.asList(args));
        final CommandLine command = new CommandLine(script, args);
        command.execute();
        final String output = command.getStdOut();
        if (!command.isSuccessful()) {
            //throw new RuntimeException("exec return code " + command.getStdOut() + ": " + output);
            log.warn("Command failed!! -- {}", args);
        }
        return output;
    }

    /** Execute networksetup, returning the output */
    public String runNetworkSetup(final String... args) {
        CommandLine command = new CommandLine(findNetworkSetupBin(), args);
        command.execute();
        String output = command.getStdOut();
        if (!command.isSuccessful()) {
            //throw new RuntimeException("exec return code " + command.getStdOut() + ": " + output);
            log.warn("Command failed!! -- {}", args);
        }
        return output;
    }

    public Collection<String> getNetworkServices() {
        final String services = runNetworkSetup("-listallnetworkservices");
        log.info("Services: {}", services);
        final String[] strs = services.split("\n");
        return Arrays.asList(strs);
    }

    @SuppressWarnings("serial")
    static class MacNetworkSetupException extends RuntimeException {
        MacNetworkSetupException(Exception e) {
            super(generateMessage(), e);
        }

        private static String generateMessage() {
            return "Problem while managing OS X network settings, OS Version " + System.getProperty("os.version");
            // TODO more diagnostics re: networksetup?  md5sum? others?
        }

        MacNetworkSetupException(String message) {
            this(new RuntimeException(message));
        }

        MacNetworkSetupException(String message, Throwable e) {
            super(generateMessage() + ": " + message, e);
        }
    }

    /** Copy OS X network settings into Java's per-user persistent preference store 
     * @see Preferences
     * */
    public void backupNetworkSettings() throws IOException {
        // Don't clobber our old backup if we 
        // never got the chance to restore for some reason 
        if (backupIsReady())
            return;
        log.info("Backing up OS X global network settings...");
        MacNetworkSettings networkSettings = getCurrentNetworkSettings();
        writeToPrefs(networkSettings);
        backupReady(true);
    }

    /** Restore OS X network settings back the way thay were */
    public void restoreNetworkSettings() {
        // Backup really should be ready, but if not, skip it 
        if (!backupIsReady())
            return;
        log.info("Restoring OS X global network settings...");
        MacNetworkSettings networkSettings = retrieveFromPrefs();

        runNetworkSetup("-setwebproxy", networkSettings.serviceName, networkSettings.proxyServer,
                "" + networkSettings.port1);
        // DGF Do we need to do anything with authentication?  Let's just leave it alone and hope it doesn't bite us

        if (networkSettings.bypass.length > 0) {
            String[] bypassDomainArgs = new String[networkSettings.bypass.length + 2];
            bypassDomainArgs[0] = "-setproxybypassdomains";
            bypassDomainArgs[1] = networkSettings.serviceName;
            System.arraycopy(networkSettings.bypass, 0, bypassDomainArgs, 2, networkSettings.bypass.length);
            runNetworkSetup(bypassDomainArgs);
        } else {
            runNetworkSetup("-setproxybypassdomains", networkSettings.serviceName, "Empty");
        }

        String enabledArg = networkSettings.enabled ? "on" : "off";

        runNetworkSetup("-setwebproxystate", networkSettings.serviceName, enabledArg);

        backupReady(false);
    }

    /** Extract network data from Java user preferences */
    private MacNetworkSettings retrieveFromPrefs() {
        String serviceName = prefsGetStringOrFail("serviceName");
        String proxyServer = prefsGetStringOrFail("proxyServer");
        String strBypass = prefsGetStringOrFail("bypass");
        String[] bypassEncodedArray, bypass;
        if ("".equals(strBypass)) {
            bypass = new String[0];
        } else {
            bypassEncodedArray = strBypass.split("\t");
            int domains;
            try {
                domains = Integer.parseInt(bypassEncodedArray[0]);
            } catch (NumberFormatException e) {
                throw new RuntimeException("BUG! Couldn't decode bypass preference: " + strBypass);
            }
            bypass = new String[domains];
            if (domains == bypassEncodedArray.length) {
                // DGF blank domain... I assume that only the last domain can be blank?
                if (domains == 1) {
                    bypass = new String[] { "" };
                } else {
                    if (bypassEncodedArray.length != domains - 1) {
                        throw new RuntimeException("BUG! Couldn't decode bypass preference: " + strBypass);
                    }
                    System.arraycopy(bypassEncodedArray, 1, bypass, 0, domains - 1);
                }
            } else {
                if (bypassEncodedArray.length != domains + 1) {
                    throw new RuntimeException("BUG! Couldn't decode bypass preference: " + strBypass);
                }
                System.arraycopy(bypassEncodedArray, 1, bypass, 0, domains);
            }
        }

        int port1 = prefsGetIntOrFail("port");
        boolean enabled = prefsGetBooleanOrFail("enabled");
        boolean authenticated = prefsGetBooleanOrFail("authenticated");
        return new MacNetworkSettings(serviceName, enabled, proxyServer, port1, authenticated, bypass);
    }

    private String prefsGetStringOrFail(String key) {
        String value = prefs.get(key, null);
        if (value == null) {
            throw new RuntimeException("BUG! pref key " + key + " should not be null");
        }
        return value;
    }

    private int prefsGetIntOrFail(String key) {
        prefsGetStringOrFail(key);
        return prefs.getInt(key, 0);
    }

    private boolean prefsGetBooleanOrFail(String key) {
        prefsGetStringOrFail(key);
        return prefs.getBoolean(key, false);
    }

    private void writeToPrefs(MacNetworkSettings networkSettings) {
        prefs.put("serviceName", networkSettings.serviceName);
        prefs.putBoolean("enabled", networkSettings.enabled);
        prefs.put("proxyServer", networkSettings.proxyServer);
        prefs.putInt("port", networkSettings.port1);
        prefs.putBoolean("authenticated", networkSettings.authenticated);
        prefs.put("bypass", networkSettings.bypassAsString());
    }

    private boolean backupIsReady() {
        if (!prefNodeExists(BACKUP_READY))
            return false;
        return prefs.getBoolean(BACKUP_READY, false);
    }

    private void backupReady(boolean backupReady) {
        prefs.putBoolean(BACKUP_READY, backupReady);
    }

    /** Data class to hold network settings */
    class MacNetworkSettings {
        final String serviceName;
        final boolean enabled;
        final String proxyServer;
        final int port1;
        final boolean authenticated;
        final String[] bypass;

        public MacNetworkSettings(String serviceName, boolean enabled, String server, int port,
                boolean authenticated, String[] bypass) {
            this.serviceName = serviceName;
            this.enabled = enabled;
            this.proxyServer = server;
            this.port1 = port;
            this.authenticated = authenticated;
            this.bypass = bypass;
        }

        /** Return bypass domains as tab-delimited string */
        public String bypassAsString() {
            StringBuffer sb = new StringBuffer();
            sb.append(bypass.length).append('\t');
            for (String domain : bypass) {
                sb.append(domain).append('\t');
            }
            return sb.toString();
        }

        @Override
        public String toString() {
            StringBuffer sb = new StringBuffer("{serviceName=");
            sb.append(serviceName).append(", enabled=").append(enabled).append(", proxyServer=").append(proxyServer)
                    .append(", port=").append(port1).append(", authenticated=").append(authenticated)
                    .append(", bypass=").append(Arrays.toString(bypass)).append("}");
            ;
            return sb.toString();
        }
    }

}