001    /*
002     * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003     *
004     * Copyright (c) 1997-2011 Oracle and/or its affiliates. All rights reserved.
005     *
006     * The contents of this file are subject to the terms of either the GNU
007     * General Public License Version 2 only ("GPL") or the Common Development
008     * and Distribution License("CDDL") (collectively, the "License").  You
009     * may not use this file except in compliance with the License.  You can
010     * obtain a copy of the License at
011     * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
012     * or packager/legal/LICENSE.txt.  See the License for the specific
013     * language governing permissions and limitations under the License.
014     *
015     * When distributing the software, include this License Header Notice in each
016     * file and include the License file at packager/legal/LICENSE.txt.
017     *
018     * GPL Classpath Exception:
019     * Oracle designates this particular file as subject to the "Classpath"
020     * exception as provided by Oracle in the GPL Version 2 section of the License
021     * file that accompanied this code.
022     *
023     * Modifications:
024     * If applicable, add the following below the License Header, with the fields
025     * enclosed by brackets [] replaced by your own identifying information:
026     * "Portions Copyright [year] [name of copyright owner]"
027     *
028     * Contributor(s):
029     * If you wish your version of this file to be governed by only the CDDL or
030     * only the GPL Version 2, indicate your decision by adding "[Contributor]
031     * elects to include this software in this distribution under the [CDDL or GPL
032     * Version 2] license."  If you don't indicate a single choice of license, a
033     * recipient has the option to distribute your version of this file under
034     * either the CDDL, the GPL Version 2 or to extend the choice of license to
035     * its licensees as provided above.  However, if you add GPL Version 2 code
036     * and therefore, elected the GPL Version 2 license, then the option applies
037     * only if the new code is made subject to such option by the copyright
038     * holder.
039     */
040    
041    package com.sun.enterprise.admin.cli.remote;
042    
043    import java.io.*;
044    import java.net.*;
045    import java.util.*;
046    import java.util.logging.Level;
047    import java.util.logging.Logger;
048    
049    import org.jvnet.hk2.component.*;
050    import com.sun.enterprise.module.*;
051    import com.sun.enterprise.module.single.StaticModulesRegistry;
052    
053    import org.glassfish.api.admin.*;
054    import org.glassfish.api.admin.CommandModel.ParamModel;
055    import org.glassfish.common.util.admin.ManPageFinder;
056    
057    import com.sun.appserv.management.client.prefs.LoginInfo;
058    import com.sun.appserv.management.client.prefs.LoginInfoStore;
059    import com.sun.appserv.management.client.prefs.LoginInfoStoreFactory;
060    import com.sun.appserv.management.client.prefs.StoreException;
061    import com.sun.enterprise.universal.i18n.LocalStringsImpl;
062    import com.sun.enterprise.admin.remote.RemoteAdminCommand;
063    import com.sun.enterprise.admin.cli.*;
064    import com.sun.enterprise.admin.cli.ProgramOptions.PasswordLocation;
065    import com.sun.enterprise.admin.util.*;
066    import com.sun.enterprise.admin.util.CommandModelData.ParamModelData;
067    import com.sun.enterprise.util.SystemPropertyConstants;
068    
069    /**
070     * A remote command handled by the asadmin CLI.
071     */
072    public class RemoteCommand extends CLICommand {
073    
074        private static final LocalStringsImpl   strings =
075                new LocalStringsImpl(RemoteCommand.class);
076    
077        // return output string rather than printing it
078        private boolean                     returnOutput = false;
079        private String                      output;
080        private boolean                     returnAttributes = false;
081        private Map<String, String>         attrs;
082        private String                      usage;
083    
084        private String                      responseFormatType;
085        private OutputStream                userOut;
086        private File                        outputDir;
087    
088        private CLIRemoteAdminCommand       rac;
089    
090        /**
091         * A special RemoteAdminCommand that overrides methods so that
092         * we can handle the interactive requirements of a CLI command.
093         */
094        private class CLIRemoteAdminCommand extends RemoteAdminCommand {
095            /**
096             * Construct a new remote command object.  The command and arguments
097             * are supplied later using the execute method in the superclass.
098             */
099            public CLIRemoteAdminCommand(String name, String host, int port,
100                    boolean secure, String user, String password, Logger logger,
101                    String authToken)
102                    throws CommandException {
103                super(name, host, port, secure, user, password, logger, authToken, true /* prohibitDirectoryUploads */);
104            }
105    
106            /**
107             * If we're interactive, prompt for a new username and password.
108             * Return true if we're successful in collecting new information
109             * (and thus the caller should try the request again).
110             */
111            @Override
112            protected boolean updateAuthentication() {
113                Console cons;
114                if (programOpts.isInteractive() &&
115                        (cons = System.console()) != null) {
116                    // if appropriate, tell the user why authentication failed
117                    PasswordLocation pwloc = programOpts.getPasswordLocation();
118                    if (pwloc == PasswordLocation.PASSWORD_FILE) {
119                        logger.fine(strings.get("BadPasswordFromFile",
120                                                    programOpts.getPasswordFile()));
121                    } else if (pwloc == PasswordLocation.LOGIN_FILE) {
122                        try {
123                            LoginInfoStore store =
124                                LoginInfoStoreFactory.getDefaultStore();
125                            logger.fine(strings.get("BadPasswordFromLogin",
126                                            store.getName()));
127                        } catch (StoreException ex) {
128                            // ignore it
129                        }
130                    }
131    
132                    String user = null;
133                    // only prompt for a user name if the user name is set to
134                    // the default.  otherwise, assume the user specified the
135                    // correct username to begin with and all we need is the
136                    // password.
137                    if (programOpts.getUser() == null) {
138                        cons.printf("%s ", strings.get("AdminUserPrompt"));
139                        user = cons.readLine();
140                        if (user == null)
141                            return false;
142                    }
143                    String password;
144                    String puser = ok(user) ? user : programOpts.getUser();
145                    if (ok(puser))
146                        password = readPassword(
147                                    strings.get("AdminUserPasswordPrompt", puser));
148                    else
149                        password = readPassword(strings.get("AdminPasswordPrompt"));
150                    if (password == null)
151                        return false;
152                    if (ok(user)) {      // if none entered, don't change
153                        programOpts.setUser(user);
154                        this.user = user;
155                    }
156                    programOpts.setPassword(password, PasswordLocation.USER);
157                    this.password = password;
158                    return true;
159                }
160                return false;
161            }
162    
163            /**
164             * Get from environment.
165             */
166            @Override
167            protected String getFromEnvironment(String name) {
168                return env.getStringOption(name);
169            }
170    
171            /**
172             * Called when a non-secure connection attempt fails and it appears
173             * that the server requires a secure connection.
174             * Tell the user that we're retrying.
175             */
176            @Override
177            protected boolean retryUsingSecureConnection(String host, int port) {
178                String msg = strings.get("ServerMaybeSecure", host, port + "");
179                logger.info(msg);
180                return true;
181            }
182    
183            /**
184             * Return the error message to be used in the AuthenticationException.
185             * Subclasses can override to provide a more detailed message, for
186             * example, indicating the source of the password that failed.
187             */
188            @Override
189            protected String reportAuthenticationException() {
190                String msg = null;
191                PasswordLocation pwloc =
192                    programOpts.getPasswordLocation();
193                if (pwloc == PasswordLocation.PASSWORD_FILE) {
194                    msg = strings.get("InvalidCredentialsFromFile",
195                                        programOpts.getUser(),
196                                        programOpts.getPasswordFile());
197                } else if (pwloc == PasswordLocation.LOGIN_FILE) {
198                    try {
199                        LoginInfoStore store =
200                            LoginInfoStoreFactory.getDefaultStore();
201                        msg = strings.get("InvalidCredentialsFromLogin",
202                                            programOpts.getUser(),
203                                            store.getName());
204                    } catch (StoreException ex) {
205                        // ignore it
206                    }
207                }
208    
209                if (msg == null)
210                    msg = strings.get("InvalidCredentials", programOpts.getUser());
211                return msg;
212            }
213        }
214    
215        /**
216         * A class loader for the "modules" directory.
217         */
218        private static ClassLoader moduleClassLoader;
219    
220        /**
221         * A habitat just for finding man pages.
222         */
223        private static Habitat manHabitat;
224    
225        /**
226         * Construct a new remote command object.  The command and arguments
227         * are supplied later using the execute method in the superclass.
228         */
229        public RemoteCommand() throws CommandException {
230            super();
231        }
232    
233        /**
234         * Construct a new remote command object.  The command and arguments
235         * are supplied later using the execute method in the superclass.
236         */
237        public RemoteCommand(String name, ProgramOptions po, Environment env)
238                throws CommandException {
239            super(name, po, env);
240        }
241    
242        /**
243         * Construct a new remote command object.  The command and arguments
244         * are supplied later using the execute method in the superclass.
245         * This variant is used by the RemoteDeploymentFacility class to
246         * control and capture the output.
247         */
248        public RemoteCommand(String name, ProgramOptions po, Environment env,
249                String responseFormatType, OutputStream userOut)
250                throws CommandException {
251            this(name, po, env);
252            this.responseFormatType = responseFormatType;
253            this.userOut = userOut;
254        }
255    
256        /**
257         * Set the directory in which any returned files will be stored.
258         * The default is the user's home directory.
259         */
260        public void setFileOutputDirectory(File dir) {
261            outputDir = dir;
262        }
263    
264        @Override
265        protected void prepare()
266                throws CommandException, CommandValidationException  {
267            try {
268                processProgramOptions();
269    
270                initializeAuth();
271    
272                /*
273                 * Now we have all the information we need to create
274                 * the remote admin command object.
275                 */
276                initializeRemoteAdminCommand();
277    
278                if (responseFormatType != null)
279                    rac.setResponseFormatType(responseFormatType);
280                if (userOut != null)
281                    rac.setUserOut(userOut);
282    
283                /*
284                 * If this is a help request, we don't need the command
285                 * metadata and we throw away all the other options and
286                 * fake everything else.
287                 */
288                if (programOpts.isHelp()) {
289                    commandModel = helpModel();
290                    rac.setCommandModel(commandModel);
291                    return;
292                }
293    
294                /*
295                 * Find the metadata for the command.
296                 */
297                commandModel = rac.getCommandModel();
298            } catch (CommandException cex) {
299                logger.finer("RemoteCommand.prepare throws " + cex);
300                throw cex;
301            } catch (Exception e) {
302                logger.finer("RemoteCommand.prepare throws " + e);
303                throw new CommandException(e.getMessage());
304            }
305        }
306    
307        /**
308         * If it's a help request, don't prompt for any missing options.
309         */
310        @Override
311        protected void validate()
312                throws CommandException, CommandValidationException  {
313            if (programOpts.isHelp())
314                return;
315            super.validate();
316        }
317    
318        /**
319         * We do all our help processing in executeCommand.
320         */
321        @Override
322        protected boolean checkHelp()
323                throws CommandException, CommandValidationException {
324            return false;
325        }
326    
327        /**
328         * Runs the command using the specified arguments.
329         */
330        @Override
331        protected int executeCommand()
332                throws CommandException, CommandValidationException {
333            try {
334                options.set("DEFAULT", operands);
335                output = rac.executeCommand(options);
336                if (returnAttributes)
337                    attrs = rac.getAttributes();
338                else if (!returnOutput) {
339                    if (output.length() > 0) {
340                        logger.info(output);
341                    }
342                }
343            } catch (CommandException ex) {
344                // if a --help request failed, try to emulate it locally
345                if (programOpts.isHelp()) {
346                    Reader r = getLocalManPage();
347                    if (r != null) {
348                        try {
349                            BufferedReader br = new BufferedReader(r);
350                            PrintWriter pw = new PrintWriter(System.out);
351                            char[] buf = new char[8192];
352                            int cnt;
353                            while ((cnt = br.read(buf)) > 0)
354                                pw.write(buf, 0, cnt);
355                            pw.flush();
356                            return SUCCESS;
357                        } catch (IOException ioex2) {
358                            // ignore it and throw original exception
359                        } finally {
360                            try {
361                                r.close();
362                            } catch (IOException ioex3) {
363                                // ignore it
364                            }
365                        }
366                    }
367                }
368                throw ex;
369            }
370            final Map<String,String> racAttrs = rac.getAttributes();
371            String returnVal = racAttrs != null ? racAttrs.get("exit-code") : null;
372            if(returnVal != null && "WARNING".equals(returnVal))
373                return WARNING;
374            return SUCCESS;
375        }
376    
377    
378        /**
379         * Execute the command and return the output as a string
380         * instead of writing it out.
381         */
382        public String executeAndReturnOutput(String... args)
383                throws CommandException, CommandValidationException {
384            /*
385             * Tell the low level output processing to just save the output
386             * string instead of writing it out.  Yes, this is pretty gross.
387             */
388            returnOutput = true;
389            execute(args);
390            returnOutput = false;
391            return output;
392        }
393    
394        /**
395         * Execute the command and return the main attributes from the manifest
396         * instead of writing out the output.
397         */
398        public Map<String, String> executeAndReturnAttributes(String... args)
399                throws CommandException, CommandValidationException {
400            /*
401             * Tell the low level output processing to just save the attributes
402             * instead of writing out the output.  Yes, this is pretty gross.
403             */
404            returnAttributes = true;
405            execute(args);
406            returnAttributes = false;
407            return attrs;
408        }
409    
410        /**
411         * Get the usage text.
412         * If we got usage information from the server, use it.
413         *
414         * @return usage text
415         */
416        public String getUsage() {
417            if (usage == null) {
418                if (rac == null) {
419                    /*
420                     * We weren't able to initialize the RemoteAdminCommand
421                     * object, probably because we failed to parse the program
422                     * options.  With no ability to contact the remote server,
423                     * we can't provide any command-specific usage information.
424                     * Sigh.
425                     */
426                    return strings.get("Usage.asadmin.full", getName());
427                }
428                usage = rac.getUsage();
429            }
430            if (usage == null)
431                return super.getUsage();
432    
433            StringBuilder usageText = new StringBuilder();
434            usageText.append(strings.get("Usage", strings.get("Usage.asadmin")));
435            usageText.append(" ");
436            usageText.append(usage);
437            return usageText.toString();
438        }
439    
440        /**
441         * Get the man page from the server.  If the man page isn't
442         * available, e.g., because the server is down, try to find
443         * it locally by looking in the modules directory.
444         */
445        public BufferedReader getManPage() {
446            try {
447                initializeRemoteAdminCommand();
448                rac.setCommandModel(helpModel());
449                ParameterMap params = new ParameterMap();
450                params.set("help", "true");
451                String manpage = rac.executeCommand(params);
452                return new BufferedReader(new StringReader(manpage));
453            } catch (CommandException cex) {
454                // ignore
455            }
456    
457            /*
458             * Can't find the man page remotely, try to find it locally.
459             * XXX - maybe should only do this on connection failure
460             */
461            BufferedReader r = getLocalManPage();
462            return r != null ? r : super.getManPage();
463        }
464    
465        /**
466         * Return a CommandModel that only includes the --help option.
467         */
468        private CommandModel helpModel() {
469            CommandModelData cm = new CommandModelData(name);
470            cm.add(new ParamModelData("help", boolean.class, true, "false", "?"));
471            return cm;
472        }
473    
474        /**
475         * Try to find a local version of the man page for this command.
476         */
477        private BufferedReader getLocalManPage() {
478            logger.fine(strings.get("NoRemoteManPage"));
479            String cmdClass = getCommandClass(getName());
480            ClassLoader mcl = getModuleClassLoader();
481            if (cmdClass != null && mcl != null) {
482                return ManPageFinder.getCommandManPage(getName(), cmdClass,
483                                                Locale.getDefault(), mcl, logger);
484            }
485            return null;
486        }
487    
488        private void initializeRemoteAdminCommand() throws CommandException {
489            if (rac == null) {
490                rac = new CLIRemoteAdminCommand(name,
491                    programOpts.getHost(), programOpts.getPort(),
492                    programOpts.isSecure(), programOpts.getUser(),
493                    programOpts.getPassword(), logger, programOpts.getAuthToken());
494                rac.setFileOutputDirectory(outputDir);
495                rac.setInteractive(programOpts.isInteractive());;
496            }
497        }
498    
499        private void initializeAuth() throws CommandException {
500            LoginInfo li = null;
501    
502            try {
503                LoginInfoStore store = LoginInfoStoreFactory.getDefaultStore();
504                li = store.read(programOpts.getHost(), programOpts.getPort());
505                if (li == null)
506                    return;
507            } catch (StoreException se) {
508                logger.finer(
509                        "Login info could not be read from ~/.asadminpass file");
510                return;
511            }
512    
513            /*
514             * If we don't have a user name, initialize it from .asadminpass.
515             * In that case, also initialize the password unless it was
516             * already specified (overriding what's in .asadminpass).
517             *
518             * If we already have a user name, and it's the same as what's
519             * in .asadminpass, and we don't have a password, use the password
520             * from .asadminpass.
521             */
522            if (programOpts.getUser() == null) {
523                // not on command line and in .asadminpass
524                logger.finer("Getting user name from ~/.asadminpass: " +
525                                            li.getUser());
526                programOpts.setUser(li.getUser());
527                if (programOpts.getPassword() == null) {
528                    // not in passwordfile and in .asadminpass
529                    logger.finer(
530                        "Getting password from ~/.asadminpass");
531                    programOpts.setPassword(li.getPassword(),
532                        ProgramOptions.PasswordLocation.LOGIN_FILE);
533                }
534            } else if (programOpts.getUser().equals(li.getUser())) {
535                if (programOpts.getPassword() == null) {
536                    // not in passwordfile and in .asadminpass
537                    logger.finer(
538                        "Getting password from ~/.asadminpass");
539                    programOpts.setPassword(li.getPassword(),
540                        ProgramOptions.PasswordLocation.LOGIN_FILE);
541                }
542            }
543        }
544    
545        /**
546         * Given a command name, return the name of the class that implements
547         * that command in the server.
548         */
549        private static String getCommandClass(String cmdName) {
550            Habitat h = getManHabitat();
551            String cname = "org.glassfish.api.admin.AdminCommand";
552            for (Inhabitant<?> command : h.getInhabitantsByContract(cname)) {
553                for (String name : Inhabitants.getNamesFor(command, cname)) {
554                    if (name.equals(cmdName))
555                        return command.typeName();
556                }
557            }
558            return null;
559        }
560    
561        /**
562         * Return a Habitat used just for reading man pages from the
563         * modules in the modules directory.
564         */
565        private static Habitat getManHabitat() {
566            if (manHabitat != null)
567                return manHabitat;
568            ModulesRegistry registry =
569                    new StaticModulesRegistry(getModuleClassLoader());
570            manHabitat = registry.createHabitat("default");
571            return manHabitat;
572        }
573    
574        /**
575         * Return a ClassLoader that loads classes from all the modules
576         * (jar files) in the <INSTALL_ROOT>/modules directory.
577         */
578        private static ClassLoader getModuleClassLoader() {
579            if (moduleClassLoader != null)
580                return moduleClassLoader;
581            try {
582                File installDir = new File(System.getProperty(
583                                    SystemPropertyConstants.INSTALL_ROOT_PROPERTY));
584                File modulesDir = new File(installDir, "modules");
585                moduleClassLoader = new DirectoryClassLoader(modulesDir,
586                                                CLICommand.class.getClassLoader());
587                return moduleClassLoader;
588            } catch (IOException ioex) {
589                return null;
590            }
591        }
592    }