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    package com.sun.enterprise.admin.cli;
041    
042    import com.sun.enterprise.util.OS;
043    import com.sun.enterprise.util.io.FileUtils;
044    import java.io.*;
045    import java.net.*;
046    import java.util.*;
047    import java.security.KeyStore;
048    
049    import org.glassfish.api.admin.CommandException;
050    import com.sun.enterprise.admin.cli.remote.RemoteCommand;
051    import com.sun.enterprise.security.store.PasswordAdapter;
052    import com.sun.enterprise.universal.i18n.LocalStringsImpl;
053    import com.sun.enterprise.universal.io.SmartFile;
054    import com.sun.enterprise.universal.process.Jps;
055    import com.sun.enterprise.universal.process.ProcessUtils;
056    import com.sun.enterprise.universal.xml.MiniXmlParser;
057    import com.sun.enterprise.universal.xml.MiniXmlParserException;
058    import com.sun.enterprise.util.HostAndPort;
059    import com.sun.enterprise.util.io.ServerDirs;
060    
061    /**
062     * A class that's supposed to capture all the behavior common to operation
063     * on a "local" server.
064     * It's getting fairly complicated thus the "section headers" comments.
065     * This class plays two roles, <UL><LI>a place for putting common code - which
066     * are final methods.  A parent class that is communicating with its own unknown
067     * sub-classes.  These are non-final methods
068     *
069     * @author Byron Nevins
070     */
071    public abstract class LocalServerCommand extends CLICommand {
072        ////////////////////////////////////////////////////////////////
073        /// Section:  protected methods that are OK to override
074        ////////////////////////////////////////////////////////////////
075        /**
076         * Override this method and return false to turn-off the file validation.
077         * E.g. it demands that config/domain.xml be present.  In special cases like
078         * Synchronization -- this is how you turn off the testing.
079         * @return true - do the checks, false - don't do the checks
080         */
081        protected boolean checkForSpecialFiles() {
082            return true;
083        }
084    
085        ////////////////////////////////////////////////////////////////
086        /// Section:  protected methods that are notOK to override.
087        ////////////////////////////////////////////////////////////////
088        /**
089         * Returns the admin address of the local domain. Note that this method
090         * should be called only when you own the domain that is available on
091         * an accessible file system.
092         *
093         * @return HostAndPort object with admin server address
094         * @throws CommandException in case of parsing errors
095         */
096        protected final HostAndPort getAdminAddress() throws CommandException {
097            // default:  DAS which always has the name "server"
098            return getAdminAddress("server");
099        }
100    
101        /**
102         * Returns the admin address of a particular server. Note that this method
103         * should be called only when you own the server that is available on
104         * an accessible file system.
105         *
106         * @return HostAndPort object with admin server address
107         * @throws CommandException in case of parsing errors
108         */
109        protected final HostAndPort getAdminAddress(String serverName)
110                throws CommandException {
111    
112            try {
113                MiniXmlParser parser = new MiniXmlParser(getDomainXml(), serverName);
114                List<HostAndPort> addrSet = parser.getAdminAddresses();
115    
116                if (addrSet.size() > 0)
117                    return addrSet.get(0);
118                else
119                    throw new CommandException(strings.get("NoAdminPort"));
120            }
121            catch (MiniXmlParserException ex) {
122                throw new CommandException(strings.get("NoAdminPortEx", ex), ex);
123            }
124        }
125    
126        protected final void setServerDirs(ServerDirs sd) {
127            serverDirs = sd;
128        }
129    
130        protected final void setLocalPassword() {
131            String pw = serverDirs == null ? null : serverDirs.getLocalPassword();
132    
133            if (ok(pw)) {
134                programOpts.setPassword(pw,
135                        ProgramOptions.PasswordLocation.LOCAL_PASSWORD);
136                logger.finer("Using local password");
137            }
138            else
139                logger.finer("Not using local password");
140        }
141    
142        protected final void unsetLocalPassword() {
143            programOpts.setPassword(null,
144                    ProgramOptions.PasswordLocation.LOCAL_PASSWORD);
145        }
146    
147        protected final void resetServerDirs() throws IOException {
148            serverDirs = serverDirs.refresh();
149        }
150    
151        protected final ServerDirs getServerDirs() {
152            return serverDirs;
153        }
154    
155        protected final File getDomainXml() {
156            return serverDirs.getDomainXml();
157        }
158    
159        /**
160         * Checks if the create-domain was created using --savemasterpassword flag
161         * which obtains security by obfuscation! Returns null in case of failure
162         * of any kind.
163         * @return String representing the password from the JCEKS store named
164         *          master-password in domain folder
165         */
166        protected final String readFromMasterPasswordFile() {
167            File mpf = getMasterPasswordFile();
168            if (mpf == null)
169                return null;   // no master password  saved
170            try {
171                PasswordAdapter pw = new PasswordAdapter(mpf.getAbsolutePath(),
172                        "master-password".toCharArray()); // fixed key
173                return pw.getPasswordForAlias("master-password");
174            }
175            catch (Exception e) {
176                logger.finer("master password file reading error: "
177                        + e.getMessage());
178                return null;
179            }
180        }
181    
182        protected final boolean verifyMasterPassword(String mpv) {
183            //issue : 14971, should ideally use javax.net.ssl.keyStore and
184            //javax.net.ssl.keyStoreType system props here but they are
185            //unavailable to asadmin start-domain hence falling back to
186            //cacerts.jks instead of keystore.jks. Since the truststore
187            //is less-likely to be Non-JKS
188    
189            return loadAndVerifyKeystore(getJKS(),mpv);
190        }
191    
192        protected boolean loadAndVerifyKeystore(File jks,String mpv) {
193            FileInputStream fis = null;
194            try {
195                fis = new FileInputStream(jks);
196                KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
197                ks.load(fis, mpv.toCharArray());
198                return true;
199            }
200            catch (Exception e) {
201                logger.finer(e.getMessage());
202                return false;
203            }
204            finally {
205                try {
206                    if (fis != null)
207                        fis.close();
208                }
209                catch (IOException ioe) {
210                    // ignore, I know ...
211                }
212            }
213        }
214    
215        /**
216         * Get the master password, either from a password file or
217         * by asking the user.
218         */
219        protected final String getMasterPassword() throws CommandException {
220            // Sets the password into the launcher info.
221            // Yes, returning master password as a string is not right ...
222            final int RETRIES = 3;
223            long t0 = System.currentTimeMillis();
224            String mpv = passwords.get(CLIConstants.MASTER_PASSWORD);
225            if (mpv == null) { //not specified in the password file
226                mpv = "changeit";  //optimization for the default case -- see 9592
227                if (!verifyMasterPassword(mpv)) {
228                    mpv = readFromMasterPasswordFile();
229                    if (!verifyMasterPassword(mpv)) {
230                        mpv = retry(RETRIES);
231                    }
232                }
233            }
234            else { // the passwordfile contains AS_ADMIN_MASTERPASSWORD, use it
235                if (!verifyMasterPassword(mpv))
236                    mpv = retry(RETRIES);
237            }
238            long t1 = System.currentTimeMillis();
239            logger.finer("Time spent in master password extraction: "
240                    + (t1 - t0) + " msec");       //TODO
241            return mpv;
242        }
243    
244        /**
245         * See if the server is alive and is the one at the specified directory.
246         *
247         * @return true if it's the DAS at this domain directory
248         */
249        protected final boolean isThisServer(File ourDir, String directoryKey) {
250            if (!ok(directoryKey))
251                throw new NullPointerException();
252    
253            ourDir = getUniquePath(ourDir);
254            logger.finer("Check if server is at location " + ourDir);
255    
256            try {
257                RemoteCommand cmd =
258                        new RemoteCommand("__locations", programOpts, env);
259                Map<String, String> attrs =
260                        cmd.executeAndReturnAttributes(new String[]{"__locations"});
261                String theirDirPath = attrs.get(directoryKey);
262                logger.finer("Remote server has root directory " + theirDirPath);
263    
264                if (ok(theirDirPath)) {
265                    File theirDir = getUniquePath(new File(theirDirPath));
266                    return theirDir.equals(ourDir);
267                }
268                return false;
269            }
270            catch (Exception ex) {
271                return false;
272            }
273        }
274    
275        /**
276         * There is sometimes a need for subclasses to know if a
277         * <code> local domain </code> is running. An example of such a command is
278         * change-master-password command. The stop-domain command also needs to
279         * know if a domain is running <i> without </i> having to provide user
280         * name and password on command line (this is the case when I own a domain
281         * that has non-default admin user and password) and want to stop it
282         * without providing it.
283         * <p>
284         * In such cases, we need to know if the domain is running and this method
285         * provides a way to do that.
286         *
287         * @return boolean indicating whether the server is running
288         */
289        protected final boolean isRunning(String host, int port) {
290            Socket server = null;
291            try {
292                server = new Socket(host, port);
293                return true;
294            }
295            catch (Exception ex) {
296                logger.finer("\nisRunning got exception: " + ex);
297                return false;
298            }
299            finally {
300                if (server != null) {
301                    try {
302                        server.close();
303                    }
304                    catch (IOException ex) {
305                    }
306                }
307            }
308        }
309    
310        /**
311         * convenience method for the local machine
312         */
313        protected final boolean isRunning(int port) {
314            return isRunning(null, port);
315        }
316    
317        /**
318         * Is the server still running?
319         * This is only called when we're hanging around waiting for the server to die.
320         * Byron Nevins, Nov 7, 2010 - Check to see if the process itself is still running
321         * We use OS tools to figure this out.  See ProcessUtils for details.
322         * Failover to the JPS check if necessary
323         */
324        protected boolean isRunning() {
325            int pp = getPrevPid();
326    
327            if (pp < 0)
328                return isRunningByCheckingForPidFile();
329    
330            Boolean b = ProcessUtils.isProcessRunning(pp);
331    
332            if (b == null) // this means it couldn't find out!
333                return isRunningUsingJps();
334            else
335                return b.booleanValue();
336        }
337    
338        protected final void waitForRestart(File pwFile, long oldTimeStamp, long uptimeOldServer) throws CommandException {
339            if (oldTimeStamp <= 0 || !usingLocalPassword())
340                waitForRestartRemote(uptimeOldServer);
341            else
342                waitForRestartLocal(pwFile, oldTimeStamp, uptimeOldServer);
343        }
344    
345        // todo move prevpid to ServerDirs ???
346        protected final int getPrevPid() {
347            try {
348                File prevPidFile = new File(getServerDirs().getPidFile().getPath() + ".prev");
349    
350                if (!prevPidFile.canRead())
351                    return -1;
352    
353                String pids = FileUtils.readSmallFile(prevPidFile).trim();
354                return Integer.parseInt(pids);
355            }
356            catch (Exception ex) {
357                return -1;
358            }
359        }
360    
361        /**
362         * Is the server still running?
363         * This is only called when we're hanging around waiting for the server to die.
364         * Byron Nevins, Nov 7, 2010 - Check to see if the process itself is still running
365         * We use jps to check
366         * If there are any problems fall back to the previous implementation of
367         * isRunning() which looks for the pidfile to get deleted
368         */
369        private final boolean isRunningUsingJps() {
370            int pp = getPrevPid();
371    
372            if (pp < 0)
373                return isRunningByCheckingForPidFile();
374    
375            return Jps.isPid(pp);
376        }
377    
378        /**
379         * Is the server still running?
380         * This is only called when we're hanging around waiting for the server to die.
381         */
382        private boolean isRunningByCheckingForPidFile() {
383            File pf = getServerDirs().getPidFile();
384    
385            if (pf != null) {
386                return pf.exists();
387            }
388            else
389                return isRunning(programOpts.getHost(), // remote case
390                        programOpts.getPort());
391        }
392    
393        /**
394         * Wait for the local server to restart.
395         */
396        private void waitForRestartLocal(File pwFile, long oldTimeStamp, long uptimeOldServer) throws CommandException {
397            // we are using local-password for authentication to the local server.  We need
398            // to use the NEW password that will be soon generated.  After that we can
399            // do Uptime calls to make sure V3 is ready to receive commands
400    
401            if (!usingLocalPassword())
402                throw new CommandException("Internal Error - waitForRestartLocal should "
403                        + "not be called unless using local password authentication.");
404    
405            long end = CLIConstants.WAIT_FOR_DAS_TIME_MS
406                    + System.currentTimeMillis();
407    
408            while (System.currentTimeMillis() < end) {
409                // when the server has restarted the passwordfile will be different
410                // don't waste time reading the file again and again, just look
411                // for the time stamp to change.
412                // Careful -- there is a slice of time where the file does not exist!
413                try {
414                    long newTimeStamp = pwFile.lastModified(); // could be 0L
415                    logger.finer("Checking timestamp of local-password.  "
416                            + "old: " + oldTimeStamp + ", new: " + newTimeStamp);
417    
418                    if (newTimeStamp > oldTimeStamp) {
419                        // Server has restarted but may not be quite ready to handle commands
420                        // automated tests would have issues if we returned right here...
421                        resetServerDirs();
422                        programOpts.setPassword(getServerDirs().getLocalPassword(), ProgramOptions.PasswordLocation.LOCAL_PASSWORD);
423                        waitForRestartRemote(uptimeOldServer);
424                        return;
425                    }
426                    Thread.sleep(CLIConstants.RESTART_CHECK_INTERVAL_MSEC);
427                }
428                catch (Exception e) {
429                    // continue
430                }
431            }
432            // if we get here -- we timed out
433            throw new CommandException(strings.get("restartDomain.noGFStart"));
434        }
435    
436        /**
437         * Wait for the remote server to restart.
438         */
439        private void waitForRestartRemote(long uptimeOldServer) throws CommandException {
440            long end = CLIConstants.WAIT_FOR_DAS_TIME_MS
441                    + System.currentTimeMillis();
442    
443            while (System.currentTimeMillis() < end) {
444                try {
445                    Thread.sleep(CLIConstants.RESTART_CHECK_INTERVAL_MSEC);
446                    long up = getUptime();
447                    logger.finer("oldserver-uptime, newserver-uptime = " + uptimeOldServer + " --- " + up);
448    
449                    if (up > 0 && up < uptimeOldServer) {
450                        return;
451                    }
452                }
453                catch (Exception e) {
454                    // continue
455                }
456            }
457            // if we get here -- we timed out
458            throw new CommandException(strings.get("restartDomain.noGFStart"));
459        }
460    
461        /**
462         * Get uptime from the server.
463         */
464        protected final long getUptime() throws CommandException {
465            RemoteCommand cmd = new RemoteCommand("uptime", programOpts, env);
466            String up = cmd.executeAndReturnOutput("uptime", "--milliseconds").trim();
467            long up_ms = parseUptime(up);
468    
469            if (up_ms <= 0) {
470                throw new CommandException(strings.get("restart.dasNotRunning"));
471            }
472    
473            logger.finer("server uptime: " + up_ms);
474            return up_ms;
475        }
476        /**
477         * See if the server is restartable
478         * As of March 2011 -- this only returns false if a passwordfile argument was given
479         * when the server started -- but it is no longer available - i.e. the user
480         * deleted it or made it unreadable.
481         */
482        protected final boolean isRestartable() throws CommandException {
483            // false negative is worse than false positive.
484            // there is one and only one case where we return false
485            RemoteCommand cmd = new RemoteCommand("_get-runtime-info", programOpts, env);
486            Map<String, String> atts = cmd.executeAndReturnAttributes("_get-runtime-info");
487    
488            if (atts != null) {
489                String val = atts.get("restartable_value");
490    
491                if (ok(val) && val.equals("false"))
492                    return false;
493            }
494            return true;
495        }
496    
497        ////////////////////////////////////////////////////////////////
498        /// Section:  private methods
499        ////////////////////////////////////////////////////////////////
500        /**
501         * The remote uptime command returns a string like:
502         * Uptime: 10 minutes, 53 seconds, Total milliseconds: 653859\n
503         * We find that last number and extract it.
504         * XXX - this is pretty gross, and fragile
505         */
506        private long parseUptime(String up) {
507            try {
508                return Long.parseLong(up);
509            }
510            catch (Exception e) {
511                return 0;
512            }
513        }
514    
515        private boolean usingLocalPassword() {
516            return programOpts.getPasswordLocation() == ProgramOptions.PasswordLocation.LOCAL_PASSWORD;
517        }
518    
519        private final File getJKS() {
520            if (serverDirs == null)
521                return null;
522    
523            File mp = new File(new File(serverDirs.getServerDir(), "config"), "cacerts.jks");
524            if (!mp.canRead())
525                return null;
526            return mp;
527        }
528    
529        protected File getMasterPasswordFile() {
530    
531            if (serverDirs == null)
532                return null;
533    
534            File mp = new File(serverDirs.getServerDir(), "master-password");
535            if (!mp.canRead())
536                return null;
537    
538            return mp;
539        }
540    
541        private String retry(int times) throws CommandException {
542            String mpv;
543            // prompt times times
544            for (int i = 0; i < times; i++) {
545                // XXX - I18N
546                String prompt = strings.get("mp.prompt", (times - i));
547                mpv = super.readPassword(prompt);
548                if (mpv == null)
549                    throw new CommandException(strings.get("no.console"));
550                // ignore retries :)
551                if (verifyMasterPassword(mpv))
552                    return mpv;
553                if (i < (times - 1))
554                    logger.info(strings.get("retry.mp"));
555                // make them pay for typos?
556                //Thread.currentThread().sleep((i+1)*10000);
557            }
558            throw new CommandException(strings.get("mp.giveup", times));
559        }
560    
561        private File getUniquePath(File f) {
562            try {
563                f = f.getCanonicalFile();
564            }
565            catch (IOException ioex) {
566                f = SmartFile.sanitize(f);
567            }
568            return f;
569        }
570        ////////////////////////////////////////////////////////////////
571        /// Section:  private variables
572        ////////////////////////////////////////////////////////////////
573        private ServerDirs serverDirs;
574        private static final LocalStringsImpl strings =
575                new LocalStringsImpl(LocalDomainCommand.class);
576    }