/******************************************************************************* * * Copyright (c) 2004-2012, 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, Winston Prakash * *******************************************************************************/ package hudson.cli; import hudson.AbortException; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.OptionHandlerExtension; import hudson.cli.handlers.RequiresAuthenticationOptionHandler; import hudson.model.Hudson; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.ChannelProperty; import hudson.security.CliAuthenticator; import hudson.security.SecurityRealm; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.Type; import java.util.List; import java.util.Locale; import java.util.logging.Logger; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.tiger_types.Types; import org.kohsuke.args4j.ClassParser; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.spi.OptionHandler; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; /** * Base class for Hudson CLI. * * <h2>How does a CLI command work</h2> <p> The users starts * {@linkplain CLI the "CLI agent"} on a remote system, by specifying arguments, * like <tt>"java -jar hudson-cli.jar command arg1 arg2 arg3"</tt>. The CLI * agent creates a remoting channel with the server, and it sends the entire * arguments to the server, along with the remoted stdin/out/err. * * <p> The Hudson master then picks the right {@link CLICommand} to execute, * clone it, and calls * {@link #main(List, Locale, InputStream, PrintStream, PrintStream)} method. * * <h2>Note for CLI command implementor</h2> Start with <a * href="http://wiki.hudson-ci.org/display/HUDSON/Writing+CLI+commands">this * document</a> to get the general idea of CLI. * * <ul> <li> Put {@link Extension} on your implementation to have it discovered * by Hudson. * * <li> Use <a href="http://java.net/projects/args4j/">args4j</a> annotation on * your implementation to define options and arguments (however, if you don't * like that, you could override the * {@link #main(List, Locale, InputStream, PrintStream, PrintStream)} method * directly. * * <li> stdin, stdout, stderr are remoted, so proper buffering is necessary for * good user experience. * * <li> Send {@link Callable} to a CLI agent by using {@link #channel} to get * local interaction, such as uploading a file, asking for a password, etc. * * </ul> * * @author Kohsuke Kawaguchi * @since 1.302 * @see CLIMethod */ @LegacyInstancesAreScopedToHudson public abstract class CLICommand implements ExtensionPoint, Cloneable { /** * Connected to stdout and stderr of the CLI agent that initiated the * session. IOW, if you write to these streams, the person who launched the * CLI command will see the messages in his terminal. * * <p> (In contrast, calling {@code System.out.println(...)} would print out * the message to the server log file, which is probably not what you want. */ public transient PrintStream stdout, stderr; /** * Connected to stdin of the CLI agent. * * <p> This input stream is buffered to hide the latency in the remoting. */ public transient InputStream stdin; /** * {@link Channel} that represents the CLI JVM. You can use this to execute * {@link Callable} on the CLI JVM, among other things. */ public transient Channel channel; /** * The locale of the client. Messages should be formatted with this * resource. */ public transient Locale locale; /** * Gets the command name. * * <p> For example, if the CLI is invoked as <tt>java -jar cli.jar foo arg1 * arg2 arg4</tt>, on the server side {@link CLICommand} that returns "foo" * from {@link #getName()} will be invoked. * * <p> By default, this method creates "foo-bar-zot" from * "FooBarZotCommand". */ public String getName() { String name = getClass().getName(); name = name.substring(name.lastIndexOf('.') + 1); // short name name = name.substring(name.lastIndexOf('$') + 1); if (name.endsWith("Command")) { name = name.substring(0, name.length() - 7); // trim off the command } // convert "FooBarZot" into "foo-bar-zot" // Locale is fixed so that "CreateInstance" always become "create-instance" no matter where this is run. return name.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase(Locale.ENGLISH); } /** * Gets the quick summary of what this command does. Used by the help * command to generate the list of commands. */ public abstract String getShortDescription(); private void parseArguments(CmdLineParser p, List<String> args, boolean isAuthenticated) throws CmdLineException { RequiresAuthenticationOptionHandler.setIsAuthenticated(isAuthenticated); p.parseArgument(args.toArray(new String[args.size()])); } public int main(List<String> args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) { this.stdin = new BufferedInputStream(stdin); this.stdout = stdout; this.stderr = stderr; this.locale = locale; this.channel = Channel.current(); registerOptionHandlers(); CmdLineParser p = new CmdLineParser(this); if (!Hudson.getInstance().allowCli()){ stderr.println("\n\nCommand Line access is disabled. Ask your administrator to enable CLI in the System Configuration\n\n"); return -1; } // add options from the authenticator SecurityContext sc = SecurityContextHolder.getContext(); Authentication old = sc.getAuthentication(); CliAuthenticator authenticator = HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getSecurityRealm().createCliAuthenticator(this); new ClassParser().parse(authenticator, p); try { parseArguments(p, args, false); Authentication auth = authenticator.authenticate(); if (auth == Hudson.ANONYMOUS) { auth = loadStoredAuthentication(); } sc.setAuthentication(auth); // run the CLI with the right credential // Re-authenticate to make sure the user still exists (see bug 454550) authenticator.authenticate(); // parse again to deal with arguments that require authentication parseArguments(p, args, true); if (!(this instanceof LoginCommand || this instanceof HelpCommand)) { Hudson.getInstance().checkPermission(Hudson.READ); } return run(); } catch (CmdLineException e) { stderr.println(e.getMessage()); printUsage(stderr, p); return -1; } catch (AbortException e) { // signals an error without stack trace stderr.println(e.getMessage()); return -1; } catch (Exception e) { e.printStackTrace(stderr); return -1; } finally { sc.setAuthentication(old); // restore } } /** * Loads the persisted authentication information from * {@link ClientAuthenticationCache}. */ protected Authentication loadStoredAuthentication() throws InterruptedException { try { return new ClientAuthenticationCache(channel).get(); } catch (IOException e) { stderr.println("Failed to access the stored credential"); e.printStackTrace(stderr); // recover return Hudson.ANONYMOUS; } } /** * Determines if the user authentication is attempted through CLI before * running this command. * * <p> If your command doesn't require any authentication whatsoever, and if * you don't even want to let the user authenticate, then override this * method to always return false — doing so will result in all the * commands running as anonymous user credential. * * <p> Note that even if this method returns true, the user can still skip * aut * * @param auth Always non-null. If the underlying transport had already * performed authentication, this object is something other than * {@link Hudson#ANONYMOUS}. */ protected boolean shouldPerformAuthentication(Authentication auth) { return auth == Hudson.ANONYMOUS; } /** * Returns the identity of the client as determined at the CLI transport * level. * * <p> When the CLI connection to the server is tunneled over HTTP, that * HTTP connection can authenticate the client, just like any other HTTP * connections to the server can authenticate the client. This method * returns that information, if one is available. By generalizing it, this * method returns the identity obtained at the transport-level * authentication. * * <p> For example, imagine if the current {@link SecurityRealm} is doing * Kerberos authentication, then this method can return a valid identity of * the client. * * <p> If the transport doesn't do authentication, this method returns * {@link Hudson#ANONYMOUS}. */ public Authentication getTransportAuthentication() { Authentication a = channel.getProperty(TRANSPORT_AUTHENTICATION); if (a == null) { a = Hudson.ANONYMOUS; } return a; } /** * Executes the command, and return the exit code. * * @return 0 to indicate a success, otherwise an error code. * @throws AbortException If the processing should be aborted. Hudson will * report the error message without stack trace, and then exits this * command. * @throws Exception All the other exceptions cause the stack trace to be * dumped, and then the command exits with an error code. */ protected abstract int run() throws Exception; protected void printUsage(PrintStream stderr, CmdLineParser p) { stderr.println("java -jar hudson-cli.jar " + getName() + " args..."); printUsageSummary(stderr); p.printUsage(stderr); } /** * Called while producing usage. This is a good method to override to render * the general description of the command that goes beyond a single-line * summary. */ protected void printUsageSummary(PrintStream stderr) { stderr.println(getShortDescription()); } /** * Convenience method for subtypes to obtain the system property of the * client. */ protected String getClientSystemProperty(String name) throws IOException, InterruptedException { return channel.call(new GetSystemProperty(name)); } private static final class GetSystemProperty implements Callable<String, IOException> { private final String name; private GetSystemProperty(String name) { this.name = name; } public String call() throws IOException { return System.getProperty(name); } private static final long serialVersionUID = 1L; } /** * Convenience method for subtypes to obtain environment variables of the * client. */ protected String getClientEnvironmentVariable(String name) throws IOException, InterruptedException { return channel.call(new GetEnvironmentVariable(name)); } private static final class GetEnvironmentVariable implements Callable<String, IOException> { private final String name; private GetEnvironmentVariable(String name) { this.name = name; } public String call() throws IOException { return System.getenv(name); } private static final long serialVersionUID = 1L; } /** * Creates a clone to be used to execute a command. */ protected CLICommand createClone() { try { return getClass().newInstance(); } catch (IllegalAccessException e) { throw new AssertionError(e); } catch (InstantiationException e) { throw new AssertionError(e); } } /** * Auto-discovers {@link OptionHandler}s and add them to the given command * line parser. */ protected void registerOptionHandlers() { try { for (Class c : Index.list(OptionHandlerExtension.class, Hudson.getInstance().pluginManager.uberClassLoader, Class.class)) { Type t = Types.getBaseClass(c, OptionHandler.class); CmdLineParser.registerHandler(Types.erasure(Types.getTypeArgument(t, 0)), c); } } catch (IOException e) { throw new Error(e); } } /** * Returns all the registered {@link CLICommand}s. */ public static ExtensionList<CLICommand> all() { return Hudson.getInstance().getExtensionList(CLICommand.class); } /** * Obtains a copy of the command for invocation. */ public static CLICommand clone(String name) { for (CLICommand cmd : all()) { if (name.equals(cmd.getName())) { return cmd.createClone(); } } return null; } private static final Logger LOGGER = Logger.getLogger(CLICommand.class.getName()); /** * Key for {@link Channel#getProperty(Object)} that links to the * {@link Authentication} object which captures the identity of the client * given by the transport layer. */ public static final ChannelProperty<Authentication> TRANSPORT_AUTHENTICATION = new ChannelProperty<Authentication>(Authentication.class, "transportAuthentication"); private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<CLICommand>(); /*package*/ static CLICommand setCurrent(CLICommand cmd) { CLICommand old = getCurrent(); CURRENT_COMMAND.set(cmd); return old; } /** * If the calling thread is in the middle of executing a CLI command, return * it. Otherwise null. */ public static CLICommand getCurrent() { return CURRENT_COMMAND.get(); } /*package*/ static void removeCurrent() { CURRENT_COMMAND.remove(); } }