/******************************************************************************* * * 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 java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.List; import java.util.ArrayList; import java.util.Collection; import java.util.logging.Logger; /** * Encapsulates the process launch through <tt>su</tt> (embedded_su(1M) to be * exact) * * @author Kohsuke Kawaguchi */ public class EmbeddedSu { /** * Not meant to be instantiated. */ private EmbeddedSu() { } /** * Launches a process as configured by {@link ProcessBuilder}, but under a * separate user priviledge by using <tt>su</tt> functionality. * * @param user The user name in which the new process runs. * @param pwd The password of the user. */ public static Process startWithSu(String user, String pwd, ProcessBuilder pb) throws IOException { if (user == null || pwd == null) { throw new IllegalArgumentException(); } // su only invokes a shell, so the argument has to be packed into the -c option StringBuilder buf = new StringBuilder("exec "); for (String cmd : pb.command()) { buf.append(' '); // to prevent shell from interfering with characters like '$' and '`', // put everything in single-quotes. When we do this, single-quotes in // arguments have to be escaped. buf.append('\'').append(cmd.replaceAll("\'", "'\''")).append('\''); } List<String> cmds = pb.command(); cmds.clear(); cmds.addAll(Arrays.asList("/usr/lib/embedded_su", user, "-c", buf.toString())); // now the command line massage is ready. start a process Process proc = pb.start(); PrintWriter out = new PrintWriter(proc.getOutputStream(), true); InputStream in = proc.getInputStream(); // send initialization block LOGGER.fine("Initiating embedded_su conversation"); out.println("."); /* embedded_su often sends us PAM_ERROR_MSG that's useful, so capture them. alice@opensolaris:~$ /usr/lib/embedded_su root . CONV 1 PAM_PROMPT_ECHO_OFF Password: . root CONV 1 PAM_ERROR_MSG Roles can only be assumed by authorized users . ERROR embedded_su: Sorry . */ List<String> errorMessages = new ArrayList<String>(); while (true) { String line = readLine(in); if (line.startsWith("CONV")) { // how many blocks do we expect? int n = Integer.parseInt(line.substring(5)); for (int i = 0; i < n; i++) { String header = readLine(in); String textBlock = readTextBlock(in); LOGGER.fine("Got " + header + " : " + textBlock); if (header.startsWith("PAM_PROMPT_ECHO_OFF")) { // this is where we want to send the password // if we are asked the password for the 2nd time, it's rather unlikely that // the same value is expected --- instead, its' probably a retry. out.println(pwd); pwd = null; } if (header.startsWith("PAM_PROMPT_ECHO_ON")) { // embedded_su expects some value but this is probably not where we want to send the password out.println(); } if (header.startsWith("PAM_ERROR_MSG")) { errorMessages.add(textBlock); } // ignore the rest } continue; } if (line.startsWith("SUCCESS")) { LOGGER.fine("Authentication successful: " + line); return proc; } if (line.startsWith("ERROR")) { LOGGER.fine("Authentication faied: " + line); throw new SuAuthenticationFailureException(readTextBlock(in) + join(errorMessages)); } LOGGER.fine("Unrecognized response: " + line); throw new IOException("Unrecognized response from embedded_su " + line); } } private static String join(Collection<String> col) { StringBuilder buf = new StringBuilder(); for (String a : col) { buf.append(a); } return buf.toString(); } /** * Reads a line from the given input stream, without any read ahead. * * Line end is '\n', as this is for Solaris. */ private static String readLine(InputStream in) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); int ch; while ((ch = in.read()) != -1) { if (ch == '\n') { return buf.toString(); } buf.write(ch); } throw new EOFException(); } /** * Reads the text block. */ private static String readTextBlock(InputStream in) throws IOException { StringBuilder buf = new StringBuilder(); while (true) { String line = readLine(in); if (line.equals(".")) { return buf.toString(); // end of it } if (line.startsWith("..")) { buf.append(line.substring(1)); } else { buf.append(line); } buf.append('\n'); } } private static final Logger LOGGER = Logger.getLogger(EmbeddedSu.class.getName()); }