![JAR search and dependency download from the Maven repository](/logo.png)
com.sun.enterprise.admin.servermgmt.cli.LocalServerCommand Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// Portions Copyright [2016-2023] [Payara Foundation and/or affiliates]
package com.sun.enterprise.admin.servermgmt.cli;
import com.sun.enterprise.admin.cli.CLICommand;
import com.sun.enterprise.admin.cli.CLIConstants;
import com.sun.enterprise.admin.cli.ProgramOptions;
import com.sun.enterprise.admin.cli.remote.RemoteCLICommand;
import com.sun.enterprise.admin.servermgmt.domain.DomainConstants;
import com.sun.enterprise.security.store.PasswordAdapter;
import com.sun.enterprise.universal.i18n.LocalStringsImpl;
import com.sun.enterprise.universal.io.SmartFile;
import com.sun.enterprise.universal.process.Jps;
import com.sun.enterprise.universal.process.ProcessUtils;
import com.sun.enterprise.universal.xml.MiniXmlParser;
import com.sun.enterprise.universal.xml.MiniXmlParserException;
import com.sun.enterprise.util.HostAndPort;
import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.util.SystemPropertyConstants;
import com.sun.enterprise.util.io.FileUtils;
import com.sun.enterprise.util.io.ServerDirs;
import org.glassfish.api.ActionReport;
import org.glassfish.api.admin.CommandException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.XMLEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Stream;
import static com.sun.enterprise.admin.servermgmt.domain.DomainConstants.MASTERPASSWORD_FILE;
/**
* A class that's supposed to capture all the behavior common to operation on a
* "local" server. It's getting fairly complicated thus the "section headers"
* comments. This class plays two roles,
*
* - a place for putting common code - which are final methods. A parent class
* that is communicating with its own unknown sub-classes. These are non-final
* methods
*
* @author Byron Nevins
*/
public abstract class LocalServerCommand extends CLICommand {
////////////////////////////////////////////////////////////////
/// Section: private variables
////////////////////////////////////////////////////////////////
private ServerDirs serverDirs;
private static final LocalStringsImpl STRINGS = new LocalStringsImpl(LocalDomainCommand.class);
private final static int IS_RUNNING_DEFAULT_TIMEOUT = 2000;
////////////////////////////////////////////////////////////////
/// Section: protected variables
////////////////////////////////////////////////////////////////
protected static final String DEFAULT_MASTER_PASSWORD = "changeit";
////////////////////////////////////////////////////////////////
/// Section: protected methods that are OK to override
////////////////////////////////////////////////////////////////
/**
* Override this method and return false to turn-off the file validation. E.g.
* it demands that config/domain.xml be present. In special cases like
* Synchronization -- this is how you turn off the testing.
*
* @return true - do the checks, false - don't do the checks
*/
protected boolean checkForSpecialFiles() {
return true;
}
////////////////////////////////////////////////////////////////
/// Section: protected methods that are notOK to override.
////////////////////////////////////////////////////////////////
/**
* Returns the admin address of the local domain. Note that this method should
* be called only when you own the domain that is available on an accessible
* file system.
*
* @return HostAndPort object with admin server address
* @throws CommandException in case of parsing errors
*/
protected final HostAndPort getAdminAddress() throws CommandException {
// default: DAS which always has the name "server"
return getAdminAddress("server");
}
/**
* Returns the admin address of a particular server. Note that this method
* should be called only when you own the server that is available on an
* accessible file system.
*
* @param serverName the server name
* @return HostAndPort object with admin server address
* @throws CommandException in case of parsing errors
*/
protected final HostAndPort getAdminAddress(String serverName) throws CommandException {
try {
MiniXmlParser parser = new MiniXmlParser(getDomainXml(), serverName);
List
addrSet = parser.getAdminAddresses();
if (!addrSet.isEmpty()) {
return addrSet.get(0);
} else {
throw new CommandException(STRINGS.get("NoAdminPort"));
}
} catch (MiniXmlParserException ex) {
throw new CommandException(STRINGS.get("NoAdminPortEx", ex), ex);
}
}
protected final void setServerDirs(ServerDirs sd) {
serverDirs = sd;
}
protected final boolean isLocal() {
return serverDirs != null && serverDirs.getServerName() != null;
}
protected final boolean isRemote() {
return !isLocal();
}
private void resetLocalPassword() throws IOException {
resetServerDirs();
setLocalPassword();
}
protected final void setLocalPassword() {
String pw = serverDirs == null ? null : serverDirs.getLocalPassword();
if (ok(pw)) {
programOpts.setPassword(pw != null ? pw.toCharArray() : null,
ProgramOptions.PasswordLocation.LOCAL_PASSWORD);
logger.finer("Using local password");
} else
logger.finer("Not using local password");
}
protected final void unsetLocalPassword() {
programOpts.setPassword(null, ProgramOptions.PasswordLocation.LOCAL_PASSWORD);
}
protected final void resetServerDirs() throws IOException {
if (serverDirs == null)
throw new RuntimeException(Strings.get("NoServerDirs"));
serverDirs = serverDirs.refresh();
}
protected final ServerDirs getServerDirs() {
return serverDirs;
}
protected final File getDomainXml() {
if (serverDirs == null)
throw new RuntimeException(Strings.get("NoServerDirs"));
return serverDirs.getDomainXml();
}
/**
* Checks if the create-domain was created using --savemasterpassword flag which
* obtains security by obfuscation! Returns null in case of failure of any kind.
*
* @return String representing the password from the JCEKS store named
* master-password in domain folder
*/
protected final String readFromMasterPasswordFile() {
File mpf = getMasterPasswordFile();
if (mpf == null)
return null; // no master password saved
try {
PasswordAdapter pw = new PasswordAdapter(mpf.getAbsolutePath(), MASTERPASSWORD_FILE.toCharArray()); // fixed
// key
return pw.getPasswordForAlias(MASTERPASSWORD_FILE);
} catch (Exception e) {
logger.log(Level.FINER, "master password file reading error: {0}", e.getMessage());
return null;
}
}
protected final boolean verifyMasterPassword(String mpv) {
// issue : 14971, should ideally use javax.net.ssl.keyStore and
// javax.net.ssl.keyStoreType system props here but they are
// unavailable to asadmin start-domain hence falling back to
// cacerts.p12 instead of keystore.p12. Since the truststore
// is less-likely to be Non-JKS
return loadAndVerifyKeystore(getKeyStoreFile(), mpv);
}
protected boolean loadAndVerifyKeystore(File jks, String mpv) {
try {
// try to load the keystore with the provided keystore password
KeyStore.getInstance(jks, mpv.toCharArray());
return true;
} catch (Exception e) {
if (logger.isLoggable(Level.FINER))
logger.finer(e.getMessage());
return false;
}
}
/**
* Get the master password, either from a password file or by asking the user.
*
* @return the actual master password
*/
protected final String getMasterPassword() throws CommandException {
// Sets the password into the launcher info.
// Yes, returning master password as a string is not right ...
final int RETRIES = 3;
long t0 = now();
String mpv = passwords.get(CLIConstants.MASTER_PASSWORD);
if (mpv == null) { // not specified in the password file
mpv = DEFAULT_MASTER_PASSWORD; // optimization for the default case -- see 9592
if (!verifyMasterPassword(mpv)) {
mpv = readFromMasterPasswordFile();
if (!verifyMasterPassword(mpv)) {
mpv = retry(RETRIES);
}
}
} else { // the passwordfile contains AS_ADMIN_MASTERPASSWORD, use it
if (!verifyMasterPassword(mpv))
mpv = retry(RETRIES);
}
long t1 = now();
logger.log(Level.FINER, "Time spent in master password extraction: {0} msec", (t1 - t0));
return mpv;
}
/**
* See if the server is alive and is the one at the specified directory.
*
* @param ourDir the directory to check if the server is alive agains
* @param directoryKey the key for the directory
* @return true if it's the DAS at this domain directory
*/
protected final boolean isThisServer(File ourDir, String directoryKey) {
if (!ok(directoryKey))
throw new NullPointerException();
ourDir = getUniquePath(ourDir);
logger.log(Level.FINER, "Check if server is at location {0}", ourDir);
try {
programOpts.setHostAndPort(getAdminAddress());
RemoteCLICommand cmd = new RemoteCLICommand("__locations", programOpts, env);
ActionReport report = cmd.executeAndReturnActionReport("__locations");
String theirDirPath = report.findProperty(directoryKey);
logger.log(Level.FINER, "Remote server has root directory {0}", theirDirPath);
if (ok(theirDirPath)) {
File theirDir = getUniquePath(new File(theirDirPath));
return theirDir.equals(ourDir);
}
return false;
} catch (Exception ex) {
return false;
}
}
protected final int getServerPid() {
try {
return Integer.parseInt(new RemoteCLICommand("__locations", programOpts, env)
.executeAndReturnActionReport("__locations").findProperty("Pid"));
} catch (Exception e) {
return -1;
}
}
/**
* There is sometimes a need for subclasses to know if a
* local domain
is running.An example of such a command is
* change-master-password command.The stop-domain command also needs to know if
* a domain is running without having to provide user name and password
* on command line (this is the case when I own a domain that has non-default
* admin user and password) and want to stop it without providing it.
*
* In such cases, we need to know if the domain is running and this method
* provides a way to do that.
*
* @param host the host to check
* @param port the port to check agains
* @return boolean indicating whether the server is running
*/
protected final boolean isRunning(String host, int port) {
try (Socket server = new Socket()) {
if (host == null) {
host = InetAddress.getByName(null).getHostName();
}
server.connect(new InetSocketAddress(host, port), IS_RUNNING_DEFAULT_TIMEOUT);
return true;
} catch (Exception ex) {
logger.log(Level.FINER, "\nisRunning got exception: {0}", ex);
return false;
}
}
/**
* Is the server still running? This is only called when we're hanging around
* waiting for the server to die. Byron Nevins, Nov 7, 2010 - Check to see if
* the process itself is still running We use OS tools to figure this out. See
* ProcessUtils for details. Failover to the JPS check if necessary
*
* bnevins, May 2013
* http://serverfault.com/questions/181015/how-do-you-free-up-a-port-being-held-open-by-dead-process
* In WIndows the admin port may be held open for a while -- if there happens to
* be an attached running child process. This is the key message from the url:
*
* If your program spawned any processes while it was running, try killing them.
* That should cause its process record to be freed and the TCP port to be
* cleaned up. Apparently windows does this when the record is released not when
* the process exits as I would have expected.
*
* @return
*/
protected boolean isRunning() {
int pp = getPrevPid();
if (pp < 0) {
return isRunningByCheckingForPidFile();
}
Boolean b = ProcessUtils.isProcessRunning(pp);
if (b == null) { // this means it couldn't find out!
return isRunningUsingJps();
} else {
return b;
}
}
protected final void waitForRestart(final int oldServerPid) throws CommandException {
waitForRestart(oldServerPid, CLIConstants.WAIT_FOR_DAS_TIME_MS);
}
/**
* Byron Nevins Says: We have quite a historical assortment of ways to determine
* if a server has restarted. There are little teeny timing issues with all of
* them. I'm confident that this new technique will clear them all up. Here we
* are just monitoring the PID of the new server and comparing it to the pid of
* the old server. The oldServerPid is guaranteed to be either the PID of the
* "old" server or -1 if we couldn't get it -- or it isn't running. If it is -1
* then we make the assumption that once we DO get a valid pid that the server
* has started. If the old pid is valid we simply poll until we get a different
* pid. Notice that we will never get a valid pid back unless the server is
* officially up and running and "STARTED" Created April 2013
*
* @param oldServerPid The pid of the server which is being restarted.
* @throws CommandException if we time out.
*/
protected final void waitForRestart(final int oldServerPid, long timeout) throws CommandException {
long end = getEndTime(timeout);
while (now() < end) {
try {
if (isLocal())
resetLocalPassword();
int newServerPid = getServerPid();
if (newServerPid > 0 && newServerPid != oldServerPid) {
logger.log(Level.FINER, "oldserver-pid, newserver-pid = {0} --- {1}",
new Object[]{oldServerPid, newServerPid});
return;
}
Thread.sleep(CLIConstants.RESTART_CHECK_INTERVAL_MSEC);
} catch (Exception e) {
// continue
}
}
// if we get here -- we timed out
throw new CommandException(STRINGS.get("restartDomain.noGFStart"));
}
// todo move prevpid to ServerDirs ???
protected final int getPrevPid() {
try {
File prevPidFile = new File(getServerDirs().getPidFile().getPath() + ".prev");
if (!prevPidFile.canRead())
return -1;
String pids = FileUtils.readSmallFile(prevPidFile).trim();
return Integer.parseInt(pids);
} catch (Exception ex) {
return -1;
}
}
/**
* Is the server still running? This is only called when we're hanging around
* waiting for the server to die. Byron Nevins, Nov 7, 2010 - Check to see if
* the process itself is still running We use jps to check If there are any
* problems fall back to the previous implementation of isRunning() which looks
* for the pidfile to get deleted
*/
private boolean isRunningUsingJps() {
int pp = getPrevPid();
if (pp < 0)
return isRunningByCheckingForPidFile();
return Jps.isPid(pp);
}
/**
* Is the server still running? This is only called when we're hanging around
* waiting for the server to die.
*/
private boolean isRunningByCheckingForPidFile() {
File pf = getServerDirs().getPidFile();
if (pf != null) {
return pf.exists();
} else
return isRunning(programOpts.getHost(), // remote case
programOpts.getPort());
}
/**
* Get uptime from the server.
*
* @return uptime in milliseconds
* @throws CommandException if the server is not running
*/
protected final long getUptime() throws CommandException {
RemoteCLICommand cmd = new RemoteCLICommand("uptime", programOpts, env);
String up = cmd.executeAndReturnOutput("uptime", "--milliseconds").trim();
long uptimeMillis = parseUptime(up);
if (uptimeMillis <= 0) {
throw new CommandException(STRINGS.get("restart.dasNotRunning"));
}
logger.log(Level.FINER, "server uptime: {0}", uptimeMillis);
return uptimeMillis;
}
/**
* See if the server is restartable As of March 2011 -- this only returns false
* if a passwordfile argument was given when the server started -- but it is no
* longer available - i.e.the user deleted it or made it unreadable.
*
* @return true if the server is restartable
* @throws CommandException
*/
protected final boolean isRestartable() throws CommandException {
// false negative is worse than false positive.
// there is one and only one case where we return false
RemoteCLICommand cmd = new RemoteCLICommand("_get-runtime-info", programOpts, env);
ActionReport report = cmd.executeAndReturnActionReport("_get-runtime-info");
if (report != null) {
String val = report.findProperty("restartable_value");
return !ok(val) || !val.equals("false");
}
return true;
}
////////////////////////////////////////////////////////////////
/// Section: private methods
////////////////////////////////////////////////////////////////
/**
* The remote uptime command returns a string like: Uptime: 10 minutes, 53
* seconds, Total milliseconds: 653859\n We find that last number and extract
* it. XXX - this is pretty gross, and fragile
*/
private long parseUptime(String up) {
try {
return Long.parseLong(up);
} catch (Exception e) {
return 0;
}
}
/**
* Load KeyStore. By default, it is cacerts.p12. If not found, search for cacerts.jks.
*
* @return File of keystore (p12 or jks) in config directory, null if none is found
*/
protected File getKeyStoreFile() {
if (serverDirs == null) {
return null;
}
File configDir = serverDirs.getConfigDir();
File mp = Stream.of(new File(configDir, DomainConstants.TRUSTSTORE_FILE),
new File(configDir, DomainConstants.TRUSTSTORE_JKS_FILE))
.filter(f -> f.canRead())
.findFirst()
.orElse(null);
return mp;
}
protected File getMasterPasswordFile() {
if (serverDirs == null)
return null;
File mp = new File(serverDirs.getConfigDir(), MASTERPASSWORD_FILE);
if (!mp.canRead())
return null;
return mp;
}
private String retry(int times) throws CommandException {
String mpv;
// prompt times times
for (int i = 0; i < times; i++) {
// XXX - I18N
String prompt = STRINGS.get("mp.prompt", (times - i));
char[] mpvArr = super.readPassword(prompt);
mpv = mpvArr != null ? new String(mpvArr) : null;
if (mpv == null)
throw new CommandException(STRINGS.get("no.console"));
// ignore retries :)
if (verifyMasterPassword(mpv))
return mpv;
if (i < (times - 1))
logger.info(STRINGS.get("retry.mp"));
// make them pay for typos?
// Thread.currentThread().sleep((i+1)*10000);
}
throw new CommandException(STRINGS.get("mp.giveup", times));
}
private File getUniquePath(File f) {
try {
f = f.getCanonicalFile();
} catch (IOException ioex) {
f = SmartFile.sanitize(f);
}
return f;
}
private long now() {
// it's just *so* ugly to call this directly!
return System.currentTimeMillis();
}
private long getEndTime(long timeout) {
return timeout + now();
}
protected boolean dataGridEncryptionEnabled() throws IOException, XMLStreamException {
// We can't access config beans from this invocation due to it being CLI vs.
// ASAdmin command - it's not
// executing against a running server. This means we need to read directly from
// the domain.xml.
XMLEventReader xmlReader = XMLInputFactory.newInstance()
.createXMLEventReader(new FileInputStream(getDomainXml()));
while (xmlReader.hasNext()) {
XMLEvent event = xmlReader.nextEvent();
if (event.isStartElement()
&& event.asStartElement().getName().getLocalPart().equals("hazelcast-runtime-configuration")) {
Attribute attribute = event.asStartElement()
.getAttributeByName(new QName("datagrid-encryption-enabled"));
if (attribute == null) {
return false;
}
return Boolean.parseBoolean(attribute.getValue());
}
}
logger.warning("Could not determine if data grid encryption is enabled - "
+ "you will need to regenerate the encryption key if it is");
return false;
}
/**
* Gets the GlassFish installation root (using property
* com.sun.aas.installRoot), first from asenv.conf. If that's not available,
* then from java.lang.System.
*
* @return path of GlassFish install root
* @throws CommandException if the GlassFish install root is not found
*/
protected String getInstallRootPath() throws CommandException {
String installRootPath = getSystemProperty(SystemPropertyConstants.INSTALL_ROOT_PROPERTY);
if (!StringUtils.ok(installRootPath)) {
installRootPath = System.getProperty(SystemPropertyConstants.INSTALL_ROOT_PROPERTY);
}
if (!StringUtils.ok(installRootPath)) {
throw new CommandException("noInstallDirPath");
}
return installRootPath;
}
protected void checkAdditionalTrustAndKeyStores() throws IOException, XMLStreamException {
HashMap additionalTrustandKeyStores = new HashMap<>();
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(getDomainXml());
NodeList jvmOptionsNodes = document.getElementsByTagName("jvm-options");
for (int i = 0; i < jvmOptionsNodes.getLength(); i++) {
String jvmOption = jvmOptionsNodes.item(i).getTextContent();
if (jvmOption.startsWith("-Dfish.payara.ssl.additionalKeyStores")) {
String additionalKeyStores = jvmOption.split("=")[1];
additionalTrustandKeyStores.compute("additionalKeyStores", (key, value) -> value == null ? additionalKeyStores : value.concat(", " + additionalKeyStores));
continue;
}
if (jvmOption.startsWith("-Dfish.payara.ssl.additionalTrustStores")) {
String additionalTrustStores = jvmOption.split("=")[1];
additionalTrustandKeyStores.compute("additionalTrustStores", (key, value) -> value == null ? additionalTrustStores : value.concat(", " + additionalTrustStores));
continue;
}
}
if (additionalTrustandKeyStores.containsKey("additionalKeyStores")) {
logger.log(Level.INFO,
"The passwords of additional KeyStores {0} have not been changed - please update these manually to continue using them.",
Arrays.toString(additionalTrustandKeyStores.get("additionalKeyStores").split(File.pathSeparator)));
}
if (additionalTrustandKeyStores.containsKey("additionalTrustStores")) {
logger.log(Level.INFO,
"The passwords of additional TrustStores {0} have not been changed - please update these manually to continue using them.",
Arrays.toString(additionalTrustandKeyStores.get("additionalTrustStores").split(File.pathSeparator)));
}
} catch (ParserConfigurationException | SAXException exception) {
logger.warning(
"Could not determine if there were additional Key Stores or Trust stores, if the master-password has been updated, the password for the additional stores need updating in order to continue using them.");
}
}
}