/******************************************************************************* * * Copyright (c) 2004-2009 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi * * *******************************************************************************/ package hudson.os; import hudson.Launcher.LocalLauncher; import hudson.Util; import hudson.model.Computer; import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.remoting.Launcher; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; import hudson.remoting.Which; import hudson.slaves.Channels; import hudson.util.ArgumentListBuilder; import org.eclipse.hudson.jna.NativeAccessException; import org.eclipse.hudson.jna.NativeUtils; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.Collections; /** * Executes {@link Callable} as the super user, by forking a new process and * executing the closure in there if necessary. * * <p> A best effort is made to execute the closure as root, but we may still * end up exeucting the closure in the non-root privilege, so the closure should * expect that and handle it gracefully. * * <p> Still very much experimental. Subject to change. <b>Don't use it.</b> * * @author Kohsuke Kawaguchi */ public abstract class SU { private SU() { // not meant to be instantiated } /** * Returns a {@link VirtualChannel} that's connected to the * priviledge-escalated environment. * * @return Never null. This may represent a channel to a separate JVM, or * just {@link LocalChannel}. Close this channel and the SU environment will * be shut down. */ public static VirtualChannel start(final TaskListener listener, final String rootUsername, final String rootPassword) throws IOException, InterruptedException { // on Windows if (File.pathSeparatorChar == ';') { return newLocalChannel(); // TODO: perhaps use RunAs to run as an Administrator? } String os = Util.fixNull(System.getProperty("os.name")); if (os.equals("Linux")) { return new UnixSu() { protected String sudoExe() { return "sudo"; } protected Process sudoWithPass(ArgumentListBuilder args) throws IOException { args.prepend(sudoExe(), "-S"); listener.getLogger().println("$ " + Util.join(args.toList(), " ")); ProcessBuilder pb = new ProcessBuilder(args.toCommandArray()); Process p = pb.start(); // TODO: use -p to detect prompt // TODO: detect if the password didn't work PrintStream ps = new PrintStream(p.getOutputStream()); ps.println(rootPassword); ps.println(rootPassword); ps.println(rootPassword); return p; } }.start(listener, rootPassword); } if (os.equals("SunOS")) { return new UnixSu() { protected String sudoExe() { return "/usr/bin/pfexec"; } protected Process sudoWithPass(ArgumentListBuilder args) throws IOException { listener.getLogger().println("Running with embedded_su"); ProcessBuilder pb = new ProcessBuilder(args.prepend(sudoExe()).toCommandArray()); return EmbeddedSu.startWithSu(rootUsername, rootPassword, pb); } // in solaris, pfexec never asks for a password, so username==null means // we won't be using password. this helps disambiguate empty password }.start(listener, rootUsername == null ? null : rootPassword); } // TODO: Mac? // unsupported platform, take a chance return newLocalChannel(); } private static LocalChannel newLocalChannel() { return new LocalChannel(Computer.threadPoolForRemoting); } /** * Starts a new priviledge-escalated environment, execute a closure, and * shut it down. */ public static <V, T extends Throwable> V execute(TaskListener listener, String rootUsername, String rootPassword, final Callable<V, T> closure) throws T, IOException, InterruptedException { VirtualChannel ch = start(listener, rootUsername, rootPassword); try { return ch.call(closure); } finally { ch.close(); ch.join(3000); // give some time for orderly shutdown, but don't block forever. } } private static abstract class UnixSu { protected abstract String sudoExe(); protected abstract Process sudoWithPass(ArgumentListBuilder args) throws IOException; VirtualChannel start(TaskListener listener, String rootPassword) throws IOException, InterruptedException { final int uid; try { uid = NativeUtils.getInstance().getEuid(); } catch (NativeAccessException exc) { // TODO: Added to avoid adding NativeExecutionException to throws clause throw new IOException(exc); } // already running as root if (uid == 0) { return newLocalChannel(); } String javaExe = System.getProperty("java.home") + "/bin/java"; File slaveJar = Which.jarFile(Launcher.class); ArgumentListBuilder args = new ArgumentListBuilder().add(javaExe); if (slaveJar.isFile()) { args.add("-jar").add(slaveJar); } else // in production code this never happens, but during debugging this is convenientud { args.add("-cp").add(slaveJar).add(hudson.remoting.Launcher.class.getName()); } if (rootPassword == null) { // try sudo, in the hope that the user has the permission to do so without password return new LocalLauncher(listener).launchChannel( args.prepend(sudoExe()).toCommandArray(), listener.getLogger(), null, Collections.<String, String>emptyMap()); } else { // try sudo with the given password. Also run in pfexec so that we can elevate the privileges Process proc = sudoWithPass(args); return Channels.forProcess(args.toStringWithQuote(), Computer.threadPoolForRemoting, proc, listener.getLogger()); } } } }