com.sun.enterprise.admin.remote.RemoteAdminCommand Maven / Gradle / Ivy
Show all versions of payara-micro Show documentation
/*
* 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 [2017-2018] [Payara Foundation and/or its affiliates]
package com.sun.enterprise.admin.remote;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.glassfish.admin.payload.PayloadFilesManager;
import org.glassfish.admin.payload.PayloadImpl;
import org.glassfish.api.admin.AuthenticationException;
import org.glassfish.api.admin.CommandException;
import org.glassfish.api.admin.CommandModel;
import org.glassfish.api.admin.CommandModel.ParamModel;
import org.glassfish.api.admin.CommandValidationException;
import org.glassfish.api.admin.InvalidCommandException;
import org.glassfish.api.admin.ParameterMap;
import org.glassfish.api.admin.Payload;
import org.glassfish.common.util.admin.AuthTokenManager;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.sun.enterprise.admin.util.AdminLoggerInfo;
import com.sun.enterprise.admin.util.AuthenticationInfo;
import com.sun.enterprise.admin.util.CachedCommandModel;
import com.sun.enterprise.admin.util.CommandModelData.ParamModelData;
import com.sun.enterprise.admin.util.HttpConnectorAddress;
import com.sun.enterprise.admin.util.cache.AdminCacheUtils;
import com.sun.enterprise.config.serverbeans.SecureAdmin;
import com.sun.enterprise.universal.i18n.LocalStringsImpl;
import com.sun.enterprise.universal.io.SmartFile;
import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.util.io.FileUtils;
import com.sun.enterprise.util.net.NetUtils;
/**
* Utility class for executing remote admin commands.
* Each instance of RemoteAdminCommand represents a particular
* remote command on a particular remote server accessed using
* particular credentials. The instance can be reused to execute
* the same command multiple times with different arguments.
*
* Arguments to the command are supplied using a ParameterMap
* passed to the executeCommand method.
* ParameterMap is a MultiMap where each key can have multiple
* values, although this class only supports a single value for
* each option. Operands for the command are stored as the option
* named "DEFAULT" and can have multiple values.
*
* Before a command can be executed, the metadata for the command
* (in the form of a CommandModel) is required. The getCommandModel
* method will fetch the metadata from the server, save it, and
* return it. If the CommandModel for a command is known
* independently (e.g., stored in a local cache, or known a priori),
* it can be set using the setCommandModel method. If the
* metadata isn't known when the exectureCommand method is
* called, it will fetch the metadata from the server before executing
* the command.
*
* Any files returned by the command will be stored in the current
* directory. The setFileOutputDirectory method can be used to control
* where returned files are saved.
*
*
* This implementation is now in retention period. All content was migrated
* to RemoteRestAdminCommand. This implementation will be removed just after
* all necessary changes and tests will be done.
*/
public class RemoteAdminCommand {
private static final LocalStringsImpl STRINGS = new LocalStringsImpl(RemoteAdminCommand.class);
private static final String QUERY_STRING_INTRODUCER = "?";
private static final String QUERY_STRING_SEPARATOR = "&";
private static final String ADMIN_URI_PATH = "/__asadmin/";
private static final String COMMAND_NAME_REGEXP =
"^[a-zA-Z_][-a-zA-Z0-9_]*$";
private static final String READ_TIMEOUT = "AS_ADMIN_READTIMEOUT";
public static final String COMMAND_MODEL_MATCH_HEADER = "X-If-Command-Model-Match";
private static final int DEFAULT_READ_TIMEOUT; // read timeout for URL conns
private String responseFormatType = "hk2-agent";
private OutputStream userOut;
// return output string rather than printing it
protected String output;
private Map attrs;
private boolean doUpload = false;
private boolean addedUploadOption = false;
private Payload.Outbound outboundPayload;
private String usage;
private File fileOutputDir;
private StringBuilder passwordOptions;
// constructor parameters
protected String name;
protected String host;
private String canonicalHostCache; //Used by getCanonicalHost() to cache resolved value
protected int port;
protected boolean secure;
protected String user;
protected char[] password;
protected Logger logger;
protected String scope;
protected String authToken = null;
protected boolean prohibitDirectoryUploads = false;
// executeCommand parameters
protected ParameterMap options;
protected List operands;
private CommandModel commandModel;
private boolean commandModelFromCache = false;
private StringBuilder metadataErrors; // XXX
private int readTimeout = DEFAULT_READ_TIMEOUT;
private int connectTimeout = -1;
private boolean interactive = true;
private boolean omitCache = true;
private final List requestHeaders = new ArrayList();
/*
* Set a default read timeout for URL connections.
*/
static {
String rt = System.getProperty(READ_TIMEOUT);
if (rt == null) {
rt = System.getenv(READ_TIMEOUT);
}
if (rt != null) {
DEFAULT_READ_TIMEOUT = Integer.parseInt(rt);
} else {
DEFAULT_READ_TIMEOUT = 10 * 60 * 1000; // 10 minutes
}
}
/**
* content-type used for each file-transfer part of a payload to or from
* the server
*/
private static final String FILE_PAYLOAD_MIME_TYPE =
"application/octet-stream";
/**
* Interface to enable factoring out common HTTP connection management code.
*
* The implementation of this interface must implement
*
* - {@link #prepareConnection} - to perform all pre-connection configuration - set headers, chunking, etc.
* as well as writing any payload to the outbound connection. In short
* anything needed prior to the URLConnection#connect invocation.
*
* The caller will invoke this method after it has invoked {@link URL#openConnection}
* but before it invokes {@link URL#connect}.
*
- {@link #useConnection} - to read from the
* input stream, etc. The caller will invoke this method after it has
* successfully invoked {@link URL#connect}.
*
* Because the caller might have to work with multiple URLConnection objects
* (as it follows redirection, for example) this contract allows the caller
* to delegate to the HttpCommand implementation multiple times to configure
* each of the URLConnections objects, then to invoke useConnection only
* once after it has the "final" URLConnection object. For this reason
* be sure to implement prepareConnection so that it can be invoked
* multiple times.
*
*/
interface HttpCommand {
/**
* Configures the HttpURLConnection (headers, chuncking, etc.) according
* to the needs of this use of the connection and then writes any
* required outbound payload to the connection.
*
* This method might be invoked multiple times before the connection is
* actually connected, so it should be serially reentrant. Note that the
* caller will
* @param urlConnection the connection to be configured
*/
public void prepareConnection(HttpURLConnection urlConnection) throws IOException;
/**
* Uses the configured and connected connection to read
* data, process it, etc.
*
* @param urlConnection the connection to be used
* @throws CommandException
* @throws IOException
*/
public void useConnection(HttpURLConnection urlConnection)
throws CommandException, IOException;
}
public RemoteAdminCommand(String name, String host, int port)
throws CommandException {
this(name, host, port, false, "admin", null, Logger.getAnonymousLogger());
}
public RemoteAdminCommand(String name, String host, int port,
boolean secure, String user, char[] password, Logger logger)
throws CommandException {
this(name, host, port, secure, user, password, logger, null, null, false);
}
/**
* Construct a new remote command object. The command and arguments
* are supplied later using the execute method in the superclass.
*/
public RemoteAdminCommand(String name, String host, int port,
boolean secure, String user, char[] password, Logger logger,
final String scope,
final String authToken,
final boolean prohibitDirectoryUploads)
throws CommandException {
this.name = name;
this.host = host;
this.port = port;
this.secure = secure;
this.user = user;
this.password = password;
this.logger = logger;
this.scope = scope;
this.authToken = authToken;
this.prohibitDirectoryUploads = prohibitDirectoryUploads;
checkName();
}
/**
* Make sure the command name is legitimate and
* won't allow any URL spoofing attacks.
*/
private void checkName() throws CommandException {
if (!name.matches(COMMAND_NAME_REGEXP)) {
throw new CommandException("Illegal command name: " + name);
//todo: XXX - I18N
}
}
/**
* Set the response type used in requests to the server.
* The response type is sent in the User-Agent HTTP header
* and tells the server what format of response to produce.
* @param responseFormatType the user-agent to send. The default is {@code hk2-agent}
*/
public void setResponseFormatType(String responseFormatType) {
this.responseFormatType = responseFormatType;
}
/**
* If set, the raw response from the command is written to the
* specified stream.
* @param userOut the {@link OutputStream} to write to
*/
public void setUserOut(OutputStream userOut) {
this.userOut = userOut;
}
/**
* Set the CommandModel used by this command. Normally the
* CommandModel will be fetched from the server using the
* getCommandModel method, which will also save the CommandModel
* for further use. If the CommandModel is known in advance, it
* can be set with this method and avoid the call to the server.
*/
public void setCommandModel(CommandModel commandModel) {
this.commandModel = commandModel;
this.commandModelFromCache = false;
}
/**
* Set the read timeout for the URLConnection.
* @param readTimeout the timeout in millisedonds
*/
public void setReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
}
public static int getReadTimeout() {
return DEFAULT_READ_TIMEOUT;
}
/**
* Set the connect timeout for the URLConnection.
* @param connectTimeout the timeout in milliseconds
*/
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
/**
* Set the interactive mode for the command. By default, the command is
* interactive.
* @param state whether the command is interactive
*/
public void setInteractive(boolean state) {
this.interactive = state;
}
/**
* Omit local {@code AdminCache} to process command metadata.
* @param omitCache If {@code true} it will download the metadata from remote server.
* Default value is {@code false}
*/
public void setOmitCache(boolean omitCache) {
this.omitCache = omitCache;
}
/**
* Get the CommandModel for the command from the server.
* If the CommandModel hasn't been set, it's fetched from
* the server.
*
* @return the model for the command
* @throws CommandException if the server can't be contacted
*/
public CommandModel getCommandModel() throws CommandException {
if (commandModel == null && !omitCache) {
long startNanos = System.nanoTime();
try {
commandModel = AdminCacheUtils.getCache().get(createCommandCacheKey(), CommandModel.class);
if (commandModel != null) {
this.commandModelFromCache = true;
if (commandModel instanceof CachedCommandModel) {
CachedCommandModel ccm = (CachedCommandModel) commandModel;
this.usage = ccm.getUsage();
addedUploadOption = ccm.isAddedUploadOption();
}
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "Command model for command {0} was successfully loaded from the cache. [Duration: {1} nanos]", new Object[] {name, System.nanoTime() - startNanos});
}
} else {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "Command model for command {0} is not in cache. It must be fatched from server.", name);
}
}
} catch (Exception ex) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "Can not get data from cache under key " + createCommandCacheKey(), ex);
}
}
}
if (commandModel == null) {
fetchCommandModel();
}
return commandModel;
}
/**
* If command model was load from local cache.
* @return true if it was from cache, false otherwise
*/
public boolean isCommandModelFromCache() {
return commandModelFromCache;
}
/**
* Set the directory in which any returned files will be stored.
* The default is the user's home directory.
* @param dir
*/
public void setFileOutputDirectory(File dir) {
fileOutputDir = dir;
}
/**
* Return a modifiable list of headers to be added to the request.
* @return
*/
public List headers() {
return requestHeaders;
}
/**
* Run the command using the specified arguments.
* Return the output of the command.
* @param opts
* @return
* @throws org.glassfish.api.admin.CommandException
*/
public String executeCommand(ParameterMap opts) throws CommandException {
// first, make sure we have the command model
getCommandModel();
// XXX : This is to take care of camel case from ReST calls that
// do not go through usual CLI path
// XXX : This is not clean; this should be handled the same way
// it is handled for incoming CLI commands
options = new ParameterMap();
for (Map.Entry> o : opts.entrySet()) {
String key = o.getKey();
List value = o.getValue();
options.set(key.toLowerCase(Locale.ENGLISH), value);
}
operands = options.get("default"); // "DEFAULT".toLowerCase()
try {
initializeDoUpload();
// if uploading, we need a payload
if (doUpload) {
outboundPayload = PayloadImpl.Outbound.newInstance();
}
StringBuilder uriString = getCommandURI();
ParamModel operandParam = null;
for (ParamModel opt : commandModel.getParameters()) {
if (opt.getParam().primary()) {
operandParam = opt;
continue;
}
String paramName = opt.getName();
List paramValues = new ArrayList(options.get(paramName.toLowerCase(Locale.ENGLISH)));
if (!opt.getParam().alias().isEmpty() &&
!paramName.equalsIgnoreCase(opt.getParam().alias())){
paramValues.addAll(options.get(opt.getParam().alias().toLowerCase(Locale.ENGLISH)));
}
if (!opt.getParam().multiple() && paramValues.size() > 1) {
throw new CommandException(STRINGS.get("tooManyOptions",
paramName));
}
if (paramValues.isEmpty()) {
// perhaps it's set in the environment?
String envValue = getFromEnvironment(paramName);
if (envValue != null) {
paramValues.add(envValue);
}
}
if (paramValues.isEmpty()) {
/*
* Option still not set. Note that we ignore the default
* value and don't send it explicitly on the assumption
* that the server will supply the default value itself.
*
* If the missing option is required, that's an error,
* which should never happen here because validate()
* should check it first.
*/
if (!opt.getParam().optional()) {
throw new CommandException(STRINGS.get("missingOption",
paramName));
}
// optional param not set, skip it
continue;
}
for (String paramValue : paramValues) {
if (opt.getType() == File.class ||
opt.getType() == File[].class) {
addFileOption(uriString, paramName, paramValue);
} else if (opt.getParam().password()) {
addPasswordOption(uriString, paramName, paramValue);
} else {
addStringOption(uriString, paramName, paramValue);
}
}
}
// add operands
for (String operand : operands) {
if (operandParam.getType() == File.class ||
operandParam.getType() == File[].class) {
addFileOption(uriString, "DEFAULT", operand);
} else {
addStringOption(uriString, "DEFAULT", operand);
}
}
// remove the last character, whether it was "?" or "&"
uriString.setLength(uriString.length() - 1);
executeRemoteCommand(uriString.toString());
} catch (IOException ioex) {
// possibly an error caused while reading or writing a file?
throw new CommandException("I/O Error", ioex);
}
return output;
}
/**
* After a successful command execution, the attributes returned
* by the command are saved. This method returns those saved
* attributes.
*/
public Map getAttributes() {
return attrs;
}
/**
* Return true if we're successful in collecting new information
* (and thus the caller should try the request again).
* Subclasses can override to (e.g.) collect updated authentication
* information by prompting the user.
* The implementation in this class returns false, indicating that the
* authentication information was not updated.
* @return true if successful, false otherwise
*/
protected boolean updateAuthentication() {
return false;
}
/**
* Subclasses can override to supply parameter values from environment.
* The implementation in this class returns null, indicating that the
* name is not available in the environment.
*/
protected String getFromEnvironment(String name) {
return null;
}
/**
* Called when a non-secure connection attempt fails and it appears
* that the server requires a secure connection.
* Subclasses can override to indicate that the connection should
* The implementation in this class returns false, indicating that the
* connection should not be retried.
* @param host Host to say we are retrying on
* @param port Port to say we are retrying on
* @return true if the the connection should be retried
*/
protected boolean retryUsingSecureConnection(String host, int port) {
return false;
}
/**
* Return the error message to be used in the AuthenticationException.
* Subclasses can override to provide a more detailed message, for
* example, indicating the source of the password that failed.
* The implementation in this class returns a default error message.
*/
protected String reportAuthenticationException() {
return STRINGS.get("InvalidCredentials", user);
}
/**
* Get the URI for executing the command.
* @return The URI. This will be in the form of {@code /__asadmin/name?}
*/
protected StringBuilder getCommandURI() {
StringBuilder rv = new StringBuilder(ADMIN_URI_PATH);
if (scope != null) rv.append(scope);
rv.append(name).append(QUERY_STRING_INTRODUCER);
return rv;
}
/**
* Actually execute the remote command.
*/
private void executeRemoteCommand(String uri) throws CommandException {
doHttpCommand(uri, chooseRequestMethod(), new HttpCommand() {
@Override
public void prepareConnection(final HttpURLConnection urlConnection) throws IOException {
if (doUpload) {
/*
* If we are uploading anything then set the content-type
* and add the uploaded part(s) to the payload.
*/
urlConnection.setChunkedStreamingMode(0); // use default value
urlConnection.setRequestProperty("Content-Type",
outboundPayload.getContentType());
}
// add any user-specified headers
for (Header h : requestHeaders) {
urlConnection.addRequestProperty(h.getName(), h.getValue());
}
if (doUpload) {
outboundPayload.writeTo(urlConnection.getOutputStream());
}
}
@Override
public void useConnection(final HttpURLConnection urlConnection)
throws CommandException, IOException {
InputStream in = urlConnection.getInputStream();
String responseContentType = urlConnection.getContentType();
Payload.Inbound inboundPayload =
PayloadImpl.Inbound.newInstance(responseContentType, in);
if (inboundPayload == null)
throw new IOException(
STRINGS.get("NoPayloadSupport", responseContentType));
PayloadFilesManager downloadedFilesMgr =
new PayloadFilesManager.Perm(fileOutputDir, null, logger,
new PayloadFilesManager.ActionReportHandler() {
@Override
public void handleReport(InputStream reportStream)
throws Exception {
handleResponse(options, reportStream,
urlConnection.getResponseCode(), userOut);
}
});
try {
downloadedFilesMgr.processParts(inboundPayload);
} catch (CommandException cex) {
throw cex;
} catch (Exception ex) {
throw new CommandException(ex.getMessage(), ex);
}
}
});
}
private void doHttpCommand(String uriString, String httpMethod,
HttpCommand cmd) throws CommandException {
doHttpCommand(uriString, httpMethod, cmd, false /* isForMetadata */);
}
/**
* Set up an HTTP connection, call cmd.prepareConnection so the consumer of
* the connection can further configure it, then open the connection (following
* redirects if needed), then call cmd.useConnection so the consumer of the
* connection can use it.
*
* This method will try to execute the command repeatedly, for example,
* retrying with updated credentials (typically from the interactive user), etc., until the
* command succeeds or there are no more ways to retry that might succeed.
*
* @param uriString the URI to connect to
* @param httpMethod the HTTP method to use for the connection
* @param cmd the HttpCommand object
* @throws CommandException if anything goes wrong
*/
private void doHttpCommand(String uriString, String httpMethod,
HttpCommand cmd, boolean isForMetadata) throws CommandException {
HttpURLConnection urlConnection;
/*
* There are various reasons we might retry the command - an authentication
* challenges from the DAS, shifting from an insecure connection to
* a secure one, etc. So just keep trying as long as it makes sense.
*
* Any exception handling code inside the loop that changes something
* about the connection or the request and wants to retry must set
* shoudTryCommandAgain to true.
*/
boolean shouldTryCommandAgain;
/*
* If the DAS challenges us for credentials and we've already sent
* the caller-provided ones, we might ask the user for a new set
* and use them. But we want to ask only once.
*/
boolean askedUserForCredentials = false;
/*
* On a subsequent retry we might need to use secure, even if the
* caller did not request it.
*/
boolean shouldUseSecure = secure;
/*
* Send the caller-provided credentials (typically from command line
* options or the password file) on the first attempt only if we know
* the connection will
* be secure.
*/
boolean usedCallerProvidedCredentials = secure;
/*
* Note: HttpConnectorAddress will set up SSL/TLS client cert
* handling if the current configuration calls for it.
*/
HttpConnectorAddress url = getHttpConnectorAddress(
host, port, shouldUseSecure);
url.setInteractive(interactive);
do {
/*
* Any code that wants to trigger a retry will say so explicitly.
*/
shouldTryCommandAgain = false;
try {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "URI: {0}", uriString);
logger.log(Level.FINER, "URL: {0}", url);
logger.log(Level.FINER, "URL: {0}", url.toURL(uriString));
logger.log(Level.FINER, "Password options: {0}", passwordOptions);
logger.log(Level.FINER, "Using auth info: User: {0}, Password: {1}",
new Object[]{user, (password != null && password.length > 0) ? "" : ""});
}
final AuthenticationInfo authInfo = authenticationInfo();
if (authInfo != null) {
url.setAuthenticationInfo(authInfo);
}
urlConnection = (HttpURLConnection) url.openConnection(uriString);
urlConnection.setRequestProperty("User-Agent", responseFormatType);
if (passwordOptions != null) {
urlConnection.setRequestProperty("X-passwords", passwordOptions.toString());
}
if (authToken != null) {
/*
* If this request is for metadata then we expect to reuse
* the auth token.
*/
urlConnection.setRequestProperty(
SecureAdmin.Util.ADMIN_ONE_TIME_AUTH_TOKEN_HEADER_NAME,
(isForMetadata ? AuthTokenManager.markTokenForReuse(authToken) : authToken));
}
if (commandModel != null && isCommandModelFromCache() && commandModel instanceof CachedCommandModel) {
urlConnection.setRequestProperty(COMMAND_MODEL_MATCH_HEADER, ((CachedCommandModel) commandModel).getETag());
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "CommandModel ETag: {0}", ((CachedCommandModel) commandModel).getETag());
}
}
urlConnection.setRequestMethod(httpMethod);
urlConnection.setReadTimeout(readTimeout);
if (connectTimeout >= 0)
urlConnection.setConnectTimeout(connectTimeout);
addAdditionalHeaders(urlConnection);
cmd.prepareConnection(urlConnection);
urlConnection.connect();
/*
* We must handle redirection from http to https explicitly
* because, even if the HttpURLConnection's followRedirect is
* set to true, the Java SE implementation does not do so if the
* procotols are different.
*/
String redirection = checkConnect(urlConnection);
if (redirection != null) {
/*
* Log at FINER; at FINE it would appear routinely when used from
* asadmin.
*/
logger.log(Level.FINER, "Following redirection to {0}", redirection);
url = followRedirection(url, redirection);
shouldTryCommandAgain = true;
/*
* Record that, during the retry of this request, we should
* use https.
*/
shouldUseSecure = url.isSecure();
/*
* Record that, if this is a metadata request, the real
* request should use https also.
*/
secure = true;
urlConnection.disconnect();
continue;
}
/*
* No redirection, so we have established the connection.
* Now delegate again to the command processing to use the
* now-created connection.
*/
cmd.useConnection(urlConnection);
processHeaders(urlConnection);
logger.finer("doHttpCommand succeeds");
} catch (AuthenticationException authEx) {
logger.log(Level.FINER, "DAS has challenged for credentials");
/*
* The DAS has challenged us to provide valid credentials.
*
* We might have sent the request without credentials previously
* (because the connection was not secure, typically). In that case,
* retry using the caller provided credentials (if there are any).
*/
if ( ! usedCallerProvidedCredentials) {
logger.log(Level.FINER, "Have not tried caller-supplied credentials yet; will do that next");
usedCallerProvidedCredentials = true;
shouldTryCommandAgain = true;
continue;
}
/*
* We already tried the caller-provided credentials. Try to
* update the credentials if we haven't already done so.
*/
logger.log(Level.FINER, "Already used caller-supplied credentials");
if (askedUserForCredentials) {
/*
* We already updated the credentials once, and the updated
* ones did not work. No recourse.
*/
logger.log(Level.FINER, "Already tried with updated credentials; cannot authenticate");
throw authEx;
}
/*
* Try to update the creds.
*/
logger.log(Level.FINER, "Have not yet tried to update credentials, so will try to update them");
if ( ! updateAuthentication()) {
/*
* No updated credentials are avaiable, so we
* have no more options.
*/
logger.log(Level.FINER, "Could not update credentials; cannot authenticate");
throw authEx;
}
/*
* We have another set of credentials we can try.
*/
logger.log(Level.FINER, "Was able to update the credentials so will retry with the updated ones");
askedUserForCredentials = true;
shouldTryCommandAgain = true;
continue;
} catch (ConnectException ce) {
logger.log(Level.FINER, "doHttpCommand: connect exception", ce);
// this really means nobody was listening on the remote server
// note: ConnectException extends IOException and tells us more!
String msg = STRINGS.get("ConnectException", host, port + "");
throw new CommandException(msg, ce);
} catch (UnknownHostException he) {
logger.log(Level.FINER, "doHttpCommand: host exception", he);
// bad host name
String msg = STRINGS.get("UnknownHostException", host);
throw new CommandException(msg, he);
} catch (SocketException se) {
logger.log(Level.FINER, "doHttpCommand: socket exception", se);
try {
boolean serverAppearsSecure = NetUtils.isSecurePort(host, port);
if (serverAppearsSecure && !shouldUseSecure && (retryUsingSecureConnection(host, port))) {
// retry using secure connection
shouldUseSecure = true;
usedCallerProvidedCredentials = true;
shouldTryCommandAgain = true;
continue;
}
throw new CommandException(se);
} catch(IOException io) {
throw new CommandException(io);
}
} catch (SSLException se) {
logger.log(Level.FINER, "doHttpCommand: SSL exception", se);
try {
boolean serverAppearsSecure = NetUtils.isSecurePort(host, port);
if (!serverAppearsSecure && secure) {
logger.log(Level.SEVERE, AdminLoggerInfo.mServerIsNotSecure,
new Object[] { host, port });
}
throw new CommandException(se);
} catch(IOException io) {
throw new CommandException(io);
}
} catch (SocketTimeoutException e) {
logger.log(Level.FINER, "doHttpCommand: read timeout", e);
throw new CommandException(
STRINGS.get("ReadTimeout", (float)readTimeout / 1000), e);
} catch (IOException e) {
logger.log(Level.FINER, "doHttpCommand: IO exception", e);
throw new CommandException(
STRINGS.get("IOError", e.getMessage()), e);
} catch (CommandException e) {
throw e;
} catch (Exception e) {
logger.log(Level.FINER, "doHttpCommand: exception", e);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
e.printStackTrace(new PrintStream(buf));
logger.finer(buf.toString());
throw new CommandException(e);
}
} while (shouldTryCommandAgain);
outboundPayload = null; // no longer needed
}
/**
* Creates a new HttpConnectorAddress corresponding to the location to which
* an earlier request was redirected.
*
* If the new protocol is https then the HttpConnectorAddress secure setting
* is turned on.
* @param originalAddr the address which has been redirected elsewhere
* @param redirection the location to which the attempted connection was redirected
* @return connector address for the new location
* @throws MalformedURLException
*/
private HttpConnectorAddress followRedirection(
final HttpConnectorAddress originalAddr,
final String redirection) throws MalformedURLException {
final URL url = new URL(redirection);
final boolean useSecure = (url.getProtocol().equalsIgnoreCase("https"));
HttpConnectorAddress hca = new HttpConnectorAddress(
url.getHost(),
url.getPort(),
useSecure,
originalAddr.getPath(),
originalAddr.getSSLSocketFactory());
hca.setInteractive(interactive);
return hca;
}
/**
* Provides an HttpConnectorAddress for use in connecting to the desired
* admin listener.
*
* This implementation works for true admin clients and will not work
* correctly for commands submitted to instances from inside the DAS. (That
* is done from the implementation in ServerRemoteAdminCommand which extends
* this class.)
*
* This code constructs the HttpConnectorAddress in a way that uses either
* no SSLSocketFactory (if security is off) or uses an SSLSocketFactory
* linked to the asadmin truststore.
*
* @param host the host name to which the connection should be made
* @param port the admin port on that host
* @param shouldUseSecure whether SSL should be used to connect or not
* @return
*/
protected HttpConnectorAddress getHttpConnectorAddress(
final String host, final int port, final boolean shouldUseSecure) {
HttpConnectorAddress hca = new HttpConnectorAddress(
host, port, shouldUseSecure);
hca.setInteractive(interactive);
return hca;
}
/**
* Adds any headers needed for the current environment to the admin
* request.
*
* @param urlConnection
*/
protected void addAdditionalHeaders(final URLConnection urlConnection) {
/*
* No additional headers are needed for connections originating from
* true admin clients.
*/
}
/**
* Process any headers needed from the reply to the admin
* request. Subclasses can override this method to handle processing
* headers in the command's reply.
*
* @param urlConnection
*/
protected void processHeaders(final URLConnection urlConnection) {
/*
* No headers are processed by RemoteAdminCommand.
*/
}
/*
* Returns the username/password authenticaiton information to use
* in building the outbound HTTP connection.
*
* @return the username/password auth. information to send with the request
*/
protected AuthenticationInfo authenticationInfo() {
return ((user != null || password != null) ? new AuthenticationInfo(user, password) : null);
}
/**
* Check that the connection was successful and handle any error responses,
* turning them into exceptions.
*/
private String checkConnect(HttpURLConnection urlConnection)
throws IOException, CommandException {
int code = urlConnection.getResponseCode();
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Response code: {0}", code);
}
if (code == -1) {
URL url = urlConnection.getURL();
throw new CommandException(
STRINGS.get("NotHttpResponse", url.getHost(), url.getPort()));
}
if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new AuthenticationException(reportAuthenticationException());
}
if (code == HttpURLConnection.HTTP_PRECON_FAILED) {
throw new CommandValidationException("Code: " + HttpURLConnection.HTTP_PRECON_FAILED + ": Cached CommandModel is invalid.");
}
/*
* The DAS might be redirecting to a secure port. If so, follow
* the redirection.
*/
if (isStatusRedirection(code)) {
return urlConnection.getHeaderField("Location");
}
if (code != HttpURLConnection.HTTP_OK) {
throw new CommandException(STRINGS.get("BadResponse", "" + code,
urlConnection.getResponseMessage()));
}
/*
* If the connection worked then return null, indicating no
* redirection is needed.
*/
return null;
}
private boolean isStatusRedirection(final int returnCode) {
/*
* Currently, Grizzly redirects using 302. For admin requests the
* other varieties of redirection do not apply.
*/
return (returnCode == HttpURLConnection.HTTP_MOVED_TEMP);
}
/**
* Get the usage text.
* If we got usage information from the server, use it.
*
* @return usage text
*/
public String getUsage() {
return usage;
}
/**
* Adds a single option expression to the URI. Appends a '?' in preparation
* for the next option.
*
* @param uriString the URI composed so far
* @param option the option expression to be added
* @return the URI so far, including the newly-added option
*/
private StringBuilder addStringOption(StringBuilder uriString, String name,
String option) {
try {
String encodedOption = URLEncoder.encode(option, "UTF-8");
uriString.append(name).
append('=').
append(encodedOption).
append(QUERY_STRING_SEPARATOR);
} catch (UnsupportedEncodingException e) {
// XXX - should never happen
throw new RuntimeException("Error encoding value for: " + name
+ ", value:" + option, e);
}
return uriString;
}
/**
* Add a password option, passing it as a header in the request
*/
private StringBuilder addPasswordOption(StringBuilder uriString, String name, String option) throws IOException {
if (passwordOptions == null) {
passwordOptions = new StringBuilder();
} else {
passwordOptions.append(QUERY_STRING_SEPARATOR);
}
passwordOptions
.append(name)
.append('=')
.append(URLEncoder.encode(new String(Base64.getMimeEncoder().encode(option.getBytes()), UTF_8), "UTF-8"));
return uriString;
}
/**
* Adds an option for a file argument, passing the name (for uploads) or the
* path (for no-upload) operations.
*
* @param uriString the URI string so far
* @param optionName the option which takes a path or name
* @param filename the name of the file
* @return the URI string
* @throws java.io.IOException
*/
private StringBuilder addFileOption(
StringBuilder uriString,
String optionName,
String filename) throws IOException, CommandException {
File f = SmartFile.sanitize(new File(filename));
logger.log(Level.FINER, "FILE PARAM: {0} = {1}", new Object[]{optionName, f});
final boolean uploadThisFile = doUpload && ! f.isDirectory();
// attach the file to the payload - include the option name in the
// relative URI to avoid possible conflicts with same-named files
// in different directories
if (uploadThisFile) {
logger.finer("Uploading file");
try {
outboundPayload.attachFile(FILE_PAYLOAD_MIME_TYPE,
URI.create(optionName + "/" + f.getName() + (f.isDirectory() ? "/" : "")),
optionName,
null,
f,
true /* isRecursive - in case it's a directory */);
} catch (FileNotFoundException fnfe) {
/*
* Probably due to an attempt to upload a non-existent file.
* Convert this to a CommandException so it's better handled
* by the rest of the command running infrastructure.
*/
throw new CommandException(STRINGS.get("UploadedFileNotFound", f.getAbsolutePath()));
}
}
if (f != null) {
// if we are about to upload it -- give just the name
// o/w give the full path
String pathToPass = (uploadThisFile ? f.getName() : f.getPath());
addStringOption(uriString, optionName, pathToPass);
}
return uriString;
}
/**
* Decide what request method to use in building the HTTP request.
* @return the request method appropriate to the current command and options
*/
private String chooseRequestMethod() {
// XXX - should be part of command metadata
if (doUpload) {
return "POST";
} else {
return "GET";
}
}
private void handleResponse(ParameterMap params,
InputStream in, int code, OutputStream userOut)
throws IOException, CommandException {
if (userOut == null) {
handleResponse(params, in, code);
} else {
FileUtils.copy(in, userOut, 0);
}
}
private void handleResponse(ParameterMap params,
InputStream in, int code) throws IOException, CommandException {
RemoteResponseManager rrm = null;
try {
rrm = new RemoteResponseManager(in, code, logger);
rrm.process();
} catch (RemoteSuccessException rse) {
// save results
output = rse.getMessage();
assert rrm != null;
attrs = rrm.getMainAtts();
} catch (RemoteException rfe) {
// XXX - gross
if (rfe.getRemoteCause().contains("CommandNotFoundException")) {
// CommandNotFoundException from the server, then display
// the closest matching commands
throw new InvalidCommandException(rfe.getMessage());
}
throw new CommandException(
"remote failure: " + rfe.getMessage(), rfe);
}
}
/**
* Fetch the command metadata from the remote server.
* @throws org.glassfish.api.admin.CommandException
*/
protected void fetchCommandModel() throws CommandException {
long startNanos = System.nanoTime();
commandModel = null; //For sure not be used during request header construction
// XXX - there should be a "help" command, that returns XML output
//StringBuilder uriString = new StringBuilder(ADMIN_URI_PATH).
//append("help").append(QUERY_STRING_INTRODUCER);
//addStringOption(uriString, "DEFAULT", name);
StringBuilder uriString = getCommandURI();
addStringOption(uriString, "Xhelp", "true");
// remove the last character, whether it was "?" or "&"
uriString.setLength(uriString.length() - 1);
doHttpCommand(uriString.toString(), "GET", new HttpCommand() {
@Override
public void prepareConnection(HttpURLConnection urlConnection) {
urlConnection.setRequestProperty("User-Agent", "metadata");
}
@Override
public void useConnection(HttpURLConnection urlConnection)
throws CommandException, IOException {
InputStream in = urlConnection.getInputStream();
String responseContentType = urlConnection.getContentType();
logger.log(Level.FINER, "Response Content-Type: {0}", responseContentType);
Payload.Inbound inboundPayload =
PayloadImpl.Inbound.newInstance(responseContentType, in);
if (inboundPayload == null)
throw new IOException(
STRINGS.get("NoPayloadSupport", responseContentType));
boolean isReportProcessed = false;
Iterator partIt = inboundPayload.parts();
while (partIt.hasNext()) {
/*
* There should be only one part, which should be the
* metadata, but skip any other parts just in case.
*/
if (!isReportProcessed) {
metadataErrors = new StringBuilder();
commandModel =
parseMetadata(partIt.next().getInputStream(),
metadataErrors);
logger.log(Level.FINER, "fetchCommandModel: got command opts: {0}", commandModel);
isReportProcessed = true;
} else {
partIt.next(); // just throw it away
}
}
}
});
if (commandModel == null) {
if (metadataErrors != null) {
throw new InvalidCommandException(metadataErrors.toString());
} else {
throw new InvalidCommandException(STRINGS.get("unknownError"));
}
} else {
this.commandModelFromCache = false;
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "Command model for {0} command fetched from remote server. [Duration: {1} nanos]", new Object[] {name, System.nanoTime() - startNanos});
}
//if (!omitCache) {
try {
AdminCacheUtils.getCache().put(createCommandCacheKey(), commandModel);
} catch (Exception ex) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING, AdminLoggerInfo.mCantPutToCache,
new Object[] { createCommandCacheKey() });
}
}
//}
}
}
private String createCommandCacheKey() {
StringBuilder result = new StringBuilder(getCanonicalHost().length() + name.length() + 6);
result.append(getCanonicalHost()).append('_').append(port);
result.append('/').append(name);
return result.toString();
}
protected String getCanonicalHost() {
if (canonicalHostCache == null) {
try {
InetAddress address = InetAddress.getByName(host);
canonicalHostCache = address.getCanonicalHostName();
} catch (UnknownHostException ex) {
canonicalHostCache = host;
if (canonicalHostCache != null) {
canonicalHostCache = canonicalHostCache.trim().toLowerCase(Locale.ENGLISH);
}
}
}
return canonicalHostCache;
}
/**
* Parse the XML metadata for the command on the input stream.
*
* @param in the input stream
* @return the set of ValidOptions
*/
private CommandModel parseMetadata(InputStream in, StringBuilder errors) {
if (logger.isLoggable(Level.FINER)) { // XXX - assume "debug" == "FINER"
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
FileUtils.copy(in, baos, 0);
} catch (IOException ex) { }
in = new ByteArrayInputStream(baos.toByteArray());
String response = baos.toString();
logger.finer("------- RAW METADATA RESPONSE ---------");
logger.finer(response);
logger.finer("------- RAW METADATA RESPONSE ---------");
}
CachedCommandModel cm = new CachedCommandModel(name);
boolean sawFile = false;
try {
DocumentBuilder d =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = d.parse(in);
NodeList cmd = doc.getElementsByTagName("command");
Node cmdnode = cmd.item(0);
if (cmdnode == null) {
Node report = doc.getElementsByTagName("action-report").item(0);
String cause = getAttr(report.getAttributes(), "failure-cause");
if (StringUtils.ok(cause)) {
errors.append(cause);
} else {
Node mp = report.getFirstChild(); // message-part
if (mp != null) {
cause = getAttr(mp.getAttributes(), "message");
}
if (StringUtils.ok(cause)) {
errors.append(cause);
}
}
// no command info, must be invalid command or something
// wrong with command implementation
return null;
}
NamedNodeMap cmdattrs = cmdnode.getAttributes();
usage = getAttr(cmdattrs, "usage");
cm.setUsage(usage);
String dashOk = getAttr(cmdattrs, "unknown-options-are-operands");
if (dashOk != null)
cm.dashOk = Boolean.parseBoolean(dashOk);
NodeList opts = doc.getElementsByTagName("option");
for (int i = 0; i < opts.getLength(); i++) {
Node n = opts.item(i);
NamedNodeMap attributes = n.getAttributes();
String sn = getAttr(attributes, "short");
String def = getAttr(attributes, "default");
String obs = getAttr(attributes, "obsolete");
String alias = getAttr(attributes, "alias");
ParamModelData opt = new ParamModelData(
getAttr(attributes, "name"),
typeOf(getAttr(attributes, "type")),
Boolean.parseBoolean(getAttr(attributes, "optional")),
def,
StringUtils.ok(sn) ? sn : null,
Boolean.parseBoolean(obs),
alias);
if (getAttr(attributes, "type").equals("PASSWORD")) {
opt.param._password = true;
opt.prompt = getAttr(attributes, "prompt");
opt.promptAgain = getAttr(attributes, "promptAgain");
}
cm.add(opt);
if (opt.getType() == File.class)
sawFile = true;
}
// should be only one operand item
opts = doc.getElementsByTagName("operand");
for (int i = 0; i < opts.getLength(); i++) {
Node n = opts.item(i);
NamedNodeMap attributes = n.getAttributes();
Class> type = typeOf(getAttr(attributes, "type"));
if (type == File.class) {
sawFile = true;
}
int min = Integer.parseInt(getAttr(attributes, "min"));
String max = getAttr(attributes, "max");
boolean multiple = false;
if (max.equals("unlimited")) {
multiple = true;
// XXX - should convert to array of whatever
if (type == File.class) {
type = File[].class;
} else {
type = List.class;
}
}
ParamModelData pm = new ParamModelData(
getAttr(attributes, "name"), type, min == 0, null);
pm.param._primary = true;
pm.param._multiple = multiple;
cm.add(pm);
}
/*
* If one of the options or operands is a FILE,
* make sure there's also a --upload option available.
* XXX - should only add it if it's not present
* XXX - should just define upload parameter on remote command
*/
if (sawFile) {
cm.add(new ParamModelData("upload", Boolean.class,
true, null));
addedUploadOption = true;
cm.setAddedUploadOption(true);
}
} catch (ParserConfigurationException | SAXException | IOException pex) {
// ignore for now
return null;
}
return cm;
}
private Class> typeOf(String type) {
if (type.equals("STRING"))
return String.class;
else if (type.equals("BOOLEAN"))
return Boolean.class;
else if (type.equals("FILE"))
return File.class;
else if (type.equals("PASSWORD"))
return String.class;
else if (type.equals("PROPERTIES"))
return Properties.class;
else
return String.class;
}
/**
* Return the value of a named attribute, or null if not set.
*/
private static String getAttr(NamedNodeMap attributes, String name) {
Node n = attributes.getNamedItem(name);
if (n != null) {
return n.getNodeValue();
} else {
return null;
}
}
/**
* Search all the parameters that were actually specified to see
* if any of them are FILE type parameters. If so, check for the
* "--upload" option.
*/
private void initializeDoUpload() throws CommandException {
boolean sawFile = false;
boolean sawDirectory = false;
/*
* We don't upload directories, even when asked to upload.
*/
boolean sawUploadableFile = false;
for (Map.Entry> param : options.entrySet()) {
String paramName = param.getKey();
if (paramName.equals("DEFAULT")) // operands handled below
continue;
ParamModel opt = commandModel.getModelFor(paramName);
if (opt != null &&
(opt.getType() == File.class ||
opt.getType() == File[].class)) {
sawFile = true;
for (String fname : options.get(opt.getName())) {
final File optionFile = new File(fname);
sawDirectory |= optionFile.isDirectory();
sawUploadableFile |= optionFile.isFile();
}
}
}
// now check the operands for files
ParamModel operandParam = getOperandModel();
if (operandParam != null &&
(operandParam.getType() == File.class ||
operandParam.getType() == File[].class)) {
sawFile |= !operands.isEmpty();
for (String operandValue : operands) {
final File operandFile = new File(operandValue);
sawDirectory |= operandFile.isDirectory();
sawUploadableFile |= operandFile.isFile();
}
}
if (sawFile) {
logger.finer("Saw a file parameter");
// found a FILE param, is doUpload set?
String upString = getOption("upload");
if (StringUtils.ok(upString)) {
doUpload = Boolean.parseBoolean(upString);
} else {
doUpload = !isLocal(host) && sawUploadableFile;
}
if (prohibitDirectoryUploads && sawDirectory && doUpload) {
// oops, can't upload directories
logger.log(Level.FINER, "--upload={0}, doUpload={1}", new Object[]{upString, doUpload});
throw new CommandException(STRINGS.get("CantUploadDirectory"));
}
}
if (addedUploadOption) {
logger.finer("removing --upload option");
//options.remove("upload"); // remove it
// XXX - no remove method, have to copy it
ParameterMap noptions = new ParameterMap();
for (Map.Entry> e : options.entrySet()) {
if (!e.getKey().equals("upload"))
noptions.set(e.getKey(), e.getValue());
}
options = noptions;
}
logger.log(Level.FINER, "doUpload set to {0}", doUpload);
}
/**
* Does the given hostname represent the local host?
*/
private static boolean isLocal(String hostname) {
if (hostname.equalsIgnoreCase("localhost")) // the common case
return true;
try {
// let NetUtils do the hard work
InetAddress ia = InetAddress.getByName(hostname);
return NetUtils.isLocal(ia.getHostAddress());
} catch (UnknownHostException ex) {
/*
* Sometimes people misconfigure their name service and they
* can't even look up the name of their own machine.
* Too bad. We just give up and say it's not local.
*/
return false;
}
}
/**
* Get the ParamModel that corresponds to the operand
* (primary parameter). Return null if none.
*/
private ParamModel getOperandModel() {
for (ParamModel pm : commandModel.getParameters()) {
if (pm.getParam().primary())
return pm;
}
return null;
}
/**
* Get an option value, that might come from the command line
* or from the environment. Return the default value for the
* option if not otherwise specified.
*/
private String getOption(String name) {
String val = options.getOne(name);
if (val == null)
val = getFromEnvironment(name);
if (val == null) {
// no value, find the default
ParamModel opt = commandModel.getModelFor(name);
// if no value was specified and there's a default value, return it
if (opt != null) {
String def = opt.getParam().defaultValue();
if (StringUtils.ok(def))
val = def;
}
}
return val;
}
}