All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.enterprise.admin.remote.RemoteRestAdminCommand Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation
 * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.enterprise.admin.remote;

import com.sun.enterprise.admin.event.AdminCommandEventBrokerImpl;
import com.sun.enterprise.admin.remote.reader.CliActionReport;
import com.sun.enterprise.admin.remote.reader.ProprietaryReader;
import com.sun.enterprise.admin.remote.reader.ProprietaryReaderFactory;
import com.sun.enterprise.admin.remote.sse.GfSseEventReceiver;
import com.sun.enterprise.admin.remote.sse.GfSseEventReceiverProprietaryReader;
import com.sun.enterprise.admin.remote.sse.GfSseInboundEvent;
import com.sun.enterprise.admin.remote.writer.ProprietaryWriter;
import com.sun.enterprise.admin.remote.writer.ProprietaryWriterFactory;
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.net.NetUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import javax.net.ssl.SSLException;

import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.glassfish.admin.payload.PayloadFilesManager;
import org.glassfish.api.ActionReport;
import org.glassfish.api.ActionReport.ExitCode;
import org.glassfish.api.admin.AdminCommandState;
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 static java.util.logging.Level.FINER;
import static java.util.logging.Level.FINEST;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;

/**
 * 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. */ //Fork of RemoteAdminCommand public class RemoteRestAdminCommand extends AdminCommandEventBrokerImpl { private static final LocalStringsImpl strings = new LocalStringsImpl(RemoteRestAdminCommand.class); private static final String ADMIN_URI_PATH = "/command/"; 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 String MEDIATYPE_TXT = "text/plain"; private static final String MEDIATYPE_JSON = "application/json"; private static final String MEDIATYPE_MULTIPART = "multipart/*"; private static final String MEDIATYPE_SSE = "text/event-stream"; private static final String EOL = StringUtils.EOL; private static final int defaultReadTimeout; // read timeout for URL conns private String responseFormatType = "hk2-agent"; // return output string rather than printing it protected String output; private Map attrs; private boolean doUpload = false; private boolean addedUploadOption = false; private RestPayloadImpl.Outbound outboundPayload; private String usage; private File fileOutputDir; private StringBuilder passwordOptions; private String manpage; private String cmduri; private ActionReport actionReport; // 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 boolean notify; 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 int readTimeout = defaultReadTimeout; private int connectTimeout = -1; private boolean interactive = true; private final List

requestHeaders = new ArrayList<>(); private boolean closeSse = false; private boolean enableCommandModelCache = true; private OutputStream userOut; /* * 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) { defaultReadTimeout = Integer.parseInt(rt); } else { defaultReadTimeout = 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 */ 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 */ void useConnection(HttpURLConnection urlConnection) throws CommandException, IOException; } public RemoteRestAdminCommand(String name, String host, int port) throws CommandException { this(name, host, port, false, "admin", null, Logger.getAnonymousLogger(), false); } public RemoteRestAdminCommand(String name, String host, int port, boolean secure, String user, char[] password, Logger logger, boolean notify) throws CommandException { this(name, host, port, secure, user, password, logger, null, null, false, notify); } /** * Construct a new remote command object. The command and arguments are supplied later using the execute method in the * superclass. */ public RemoteRestAdminCommand(String name, String host, int port, boolean secure, String user, char[] password, Logger logger, final String scope, final String authToken, final boolean prohibitDirectoryUploads, boolean notify) throws CommandException { this.name = name; this.host = host; this.port = port; this.secure = secure; this.notify = notify; 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 } } public void closeSse(String message, ActionReport.ExitCode exitCode) { ActionReport report = new CliActionReport(); report.setMessage(message); report.setActionExitCode(exitCode); setActionReport(report); this.closeSse = true; } /** * 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. */ public void setResponseFormatType(String responseFormatType) { this.responseFormatType = responseFormatType; } /** * If set, the raw response from the command is written to the specified stream. */ 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. */ public void setReadTimeout(int readTimeout) { this.readTimeout = readTimeout; } public static int getReadTimeout() { return defaultReadTimeout; } public String findPropertyInReport(String key) { if (actionReport == null) { return null; } return actionReport.findProperty(key); } /** * Set the connect timeout for the URLConnection. */ public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; } /** * Set the interactive mode for the command. By default, the command is interactive. */ public void setInteractive(boolean state) { this.interactive = state; } public void setEnableCommandModelCache(boolean enableCommandModelCache) { this.enableCommandModelCache = enableCommandModelCache; } /** * 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 && enableCommandModelCache) { long startNanos = System.nanoTime(); try { commandModel = getCommandModelFromCache(); if (commandModel != null) { this.commandModelFromCache = true; if (logger.isLoggable(FINEST)) { logger.log(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(FINEST)) { logger.log(FINEST, "Command model for command {0} is not in cache. It must be fatched from server.", name); } } } catch (Exception ex) { if (logger.isLoggable(FINEST)) { logger.log(FINEST, "Can not get data from cache under key " + createCommandCacheKey(), ex); } } } if (commandModel == null) { fetchCommandModel(); } return commandModel; } private CommandModel getCommandModelFromCache() { String cachedModel = AdminCacheUtils.getCache().get(createCommandCacheKey(), String.class); if (cachedModel == null) { return null; } cachedModel = cachedModel.trim(); int ind = cachedModel.indexOf('\n'); if (ind < 0) { return null; } String eTag = cachedModel.substring(0, ind); if (!eTag.startsWith("ETag:")) { return null; } eTag = eTag.substring(5).trim(); if (logger.isLoggable(FINEST)) { logger.log(FINEST, "Cached command model ETag is {0}", eTag); } String content = cachedModel.substring(ind + 1).trim(); CachedCommandModel result = parseMetadata(content, eTag); return result; } /** * Parse the JSon metadata for the command. * * @param str the string * @return the etag to compare the command cache model */ private CachedCommandModel parseMetadata(String str, String etag) { if (logger.isLoggable(FINER)) { // XXX - assume "debug" == "FINER" logger.finer("------- RAW METADATA RESPONSE ---------"); logger.log(FINER, "ETag: {0}", etag); logger.finer(str); logger.finer("------- RAW METADATA RESPONSE ---------"); } if (str == null) { return null; } try { boolean sawFile = false; JSONObject obj = new JSONObject(str); obj = obj.getJSONObject("command"); CachedCommandModel cm = new CachedCommandModel(obj.getString("@name"), etag); cm.dashOk = obj.optBoolean("@unknown-options-are-operands", false); cm.managedJob = obj.optBoolean("@managed-job", false); cm.setUsage(obj.optString("usage", null)); Object optns = obj.opt("option"); if (!JSONObject.NULL.equals(optns)) { JSONArray jsonOptions; if (optns instanceof JSONArray) { jsonOptions = (JSONArray) optns; } else { jsonOptions = new JSONArray(); jsonOptions.put(optns); } for (int i = 0; i < jsonOptions.length(); i++) { JSONObject jsOpt = jsonOptions.getJSONObject(i); String type = jsOpt.getString("@type"); ParamModelData opt = new ParamModelData(jsOpt.getString("@name"), typeOf(type), jsOpt.optBoolean("@optional", false), jsOpt.optString("@default"), jsOpt.optString("@short"), jsOpt.optBoolean("@obsolete", false), jsOpt.optString("@alias")); opt.param._acceptableValues = jsOpt.optString("@acceptable-values"); if ("PASSWORD".equals(type)) { opt.param._password = true; opt.prompt = jsOpt.optString("@prompt"); opt.promptAgain = jsOpt.optString("@prompt-again"); } else if ("FILE".equals(type)) { sawFile = true; } if (jsOpt.optBoolean("@primary", false)) { opt.param._primary = true; } if (jsOpt.optBoolean("@multiple", false)) { if (opt.type == File.class) { opt.type = File[].class; } else { opt.type = List.class; } opt.param._multiple = true; } cm.add(opt); } } if (sawFile) { cm.add(new ParamModelData("upload", Boolean.class, true, null)); addedUploadOption = true; cm.setAddedUploadOption(true); } if (notify) { cm.add(new ParamModelData("notify", Boolean.class, false, "false")); } this.usage = cm.getUsage(); return cm; } catch (JSONException ex) { logger.log(FINER, "Can not parse command metadata", ex); return null; } } /** * If command model was load from local cache. */ public boolean isCommandModelFromCache() { return commandModelFromCache; } /** * Set the directory in which any returned files will be stored. The default is the user's home directory. */ public void setFileOutputDirectory(File dir) { fileOutputDir = dir; } /** * Return a modifiable list of headers to be added to the request. */ public List

headers() { return requestHeaders; } protected boolean useSse() throws CommandException { return getCommandModel().isManagedJob(); } /** * Run the command using the specified arguments. Return the output of the command. */ public String executeCommand(ParameterMap opts) throws CommandException { if (logger.isLoggable(FINER)) { logger.log(FINER, "RemoteRestAdminCommand.executeCommand() - name: {0}", this.name); } //Just to be sure. Cover get help if (opts != null && opts.size() == 1 && opts.containsKey("help")) { return getManPage(); } ParameterMap params = processParams(opts); boolean retry; do { //Cache update cycle retry = false; try { executeRemoteCommand(params); } catch (CommandValidationException mve) { if (refetchInvalidModel() && isCommandModelFromCache()) { fetchCommandModel(); retry = true; } else { throw mve; } } return output; } while (retry); } private ParameterMap processParams(ParameterMap opts) throws CommandException { if (opts == null) { opts = new ParameterMap(); } // 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 = new RestPayloadImpl.Outbound(true); } else { outboundPayload = null; } ParameterMap result = new ParameterMap(); 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(result, paramName, paramValue); } else { result.add(paramName, paramValue); } } } // add operands for (String operand : operands) { if (operandParam.getType() == File.class || operandParam.getType() == File[].class) { addFileOption(result, "DEFAULT", operand); } else { result.add("DEFAULT", operand); } } return result; } catch (IOException ioex) { // possibly an error caused while reading or writing a file? throw new CommandException("I/O Error", ioex); } } /** * If admin model is invalid, will be automatically refetched? */ protected boolean refetchInvalidModel() { return true; } /** * 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. */ 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. */ 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. */ protected String getCommandURI() { if (cmduri == null) { StringBuilder rv = new StringBuilder(ADMIN_URI_PATH); if (scope != null) { rv.append(scope); } rv.append(name); cmduri = rv.toString(); } return cmduri; } /** * Actually execute the remote command. */ private void executeRemoteCommand(final ParameterMap params) throws CommandException { doHttpCommand(getCommandURI(), "POST", new HttpCommand() { @Override public void prepareConnection(final HttpURLConnection urlConnection) throws IOException { try { if (useSse()) { urlConnection.addRequestProperty("Accept", MEDIATYPE_SSE); } else { urlConnection.addRequestProperty("Accept", MEDIATYPE_JSON + "; q=0.8, " + MEDIATYPE_MULTIPART + "; q=0.9"); } } catch (CommandException cex) { throw new IOException(cex.getLocalizedMessage(), cex); } // add any user-specified headers for (Header h : requestHeaders) { urlConnection.addRequestProperty(h.getName(), h.getValue()); } //Write data ParamsWithPayload pwp; if (doUpload) { urlConnection.setChunkedStreamingMode(0); pwp = new ParamsWithPayload(outboundPayload, params); } else { pwp = new ParamsWithPayload(null, params); } ProprietaryWriter writer = ProprietaryWriterFactory.getWriter(pwp); if (logger.isLoggable(FINER)) { logger.log(FINER, "Writer to use {0}", writer.getClass().getName()); } writer.writeTo(pwp, urlConnection); } @Override public void useConnection(final HttpURLConnection urlConnection) throws CommandException, IOException { String resultMediaType = urlConnection.getContentType(); if (logger.isLoggable(FINER)) { logger.log(FINER, "Result type is {0}", resultMediaType); logger.log(FINER, "URL connection is {0}", urlConnection.getClass().getName()); } if (resultMediaType != null && resultMediaType.startsWith(MEDIATYPE_SSE)) { String instanceId = null; boolean retryableCommand = false; try { logger.log(FINEST, "Response is SSE - about to read events"); closeSse = false; final ProprietaryReader reader = new GfSseEventReceiverProprietaryReader(); final GfSseEventReceiver eventReceiver = reader.readFrom(urlConnection.getInputStream(), resultMediaType); GfSseInboundEvent event; do { event = eventReceiver.readEvent(); if (event != null) { logger.log(FINEST, "Event: {0}", event.getName()); fireEvent(event.getName(), event); if (AdminCommandState.EVENT_STATE_CHANGED.equals(event.getName())) { AdminCommandState acs = event.getData(AdminCommandState.class, MEDIATYPE_JSON); if (acs.getId() != null) { instanceId = acs.getId(); logger.log(FINEST, "Command instance ID: {0}", instanceId); } if (acs.getState() == AdminCommandState.State.COMPLETED || acs.getState() == AdminCommandState.State.RECORDED || acs.getState() == AdminCommandState.State.REVERTED) { if (acs.getActionReport() != null) { setActionReport(acs.getActionReport()); } closeSse = true; if (!acs.isOutboundPayloadEmpty()) { logger.log(FINEST, "Romote command holds data. Must load it"); downloadPayloadFromManaged(instanceId); } } else if (acs.getState() == AdminCommandState.State.FAILED_RETRYABLE) { logger.log(INFO, strings.get("remotecommand.failedretryable", acs.getId())); if (acs.getActionReport() != null) { setActionReport(acs.getActionReport()); } closeSse = true; } else if (acs.getState() == AdminCommandState.State.RUNNING_RETRYABLE) { logger.log(FINEST, "Command stores checkpoint and is retryable"); retryableCommand = true; } } } } while (event != null && !eventReceiver.isClosed() && !closeSse); if (closeSse) { try { eventReceiver.close(); } catch (Exception exc) { } } } catch (IOException ioex) { if (instanceId != null && "Premature EOF".equals(ioex.getMessage())) { if (retryableCommand) { throw new CommandException( strings.get("remotecommand.lostConnection.retryableCommand", new Object[] { instanceId }), ioex); } else { throw new CommandException(strings.get("remotecommand.lostConnection", new Object[] { instanceId }), ioex); } } else { throw new CommandException(ioex.getMessage(), ioex); } } catch (Exception ex) { throw new CommandException(ex.getMessage(), ex); } } else { ProprietaryReader reader = ProprietaryReaderFactory.getReader(ParamsWithPayload.class, resultMediaType); if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) { ActionReport report; if (reader == null) { report = new CliActionReport(); report.setActionExitCode(ExitCode.FAILURE); report.setMessage(urlConnection.getResponseMessage()); } else { report = reader.readFrom(urlConnection.getErrorStream(), resultMediaType).getActionReport(); } setActionReport(report); } else { ParamsWithPayload pwp = reader.readFrom(urlConnection.getInputStream(), resultMediaType); if (pwp.getPayloadInbound() == null) { setActionReport(pwp.getActionReport()); } else if (resultMediaType.startsWith("multipart/")) { RestPayloadImpl.Inbound inbound = pwp.getPayloadInbound(); setActionReport(pwp.getActionReport()); if (logger.isLoggable(FINER)) { logger.log(FINER, "------ PAYLOAD ------"); Iterator parts = inbound.parts(); while (parts.hasNext()) { Payload.Part part = parts.next(); logger.log(FINER, " - {0} [{1}]", new Object[] { part.getName(), part.getContentType() }); } logger.log(FINER, "---- END PAYLOAD ----"); } PayloadFilesManager downloadedFilesMgr = new PayloadFilesManager.Perm(fileOutputDir, null, logger, null); try { downloadedFilesMgr.processParts(inbound); } catch (CommandException cex) { throw cex; } catch (Exception ex) { throw new CommandException(ex.getMessage(), ex); } } } } } }); if (actionReport == null) { this.output = null; throw new CommandException(strings.get("emptyResponse")); } if (actionReport.getActionExitCode() == ExitCode.FAILURE) { throw new CommandException(strings.getString("remote.failure.prefix", "remote failure:") + " " + this.output); } } private void downloadPayloadFromManaged(String jobId) { if (jobId == null) { return; } try { RemoteRestAdminCommand command = new RemoteRestAdminCommand("_get-payload", this.host, this.port, this.secure, this.user, this.password, this.logger, this.scope, this.authToken, this.prohibitDirectoryUploads, notify); ParameterMap params = new ParameterMap(); params.add("DEFAULT", jobId); command.executeCommand(params); } catch (CommandException ex) { logger.log(WARNING, strings.getString("remote.sse.canNotGetPayload", "Cannot retrieve payload. {0}"), ex.getMessage()); } } protected void setActionReport(ActionReport ar) { this.actionReport = ar; if (ar == null) { this.output = null; } else { StringBuilder sb = new StringBuilder(); if (ar instanceof CliActionReport) { addCombinedMessages((CliActionReport) ar, sb); } else if (ar.getMessage() != null) { sb.append(ar.getMessage()); } addSubMessages("", ar.getTopMessagePart(), sb); this.output = sb.toString(); if (logger.isLoggable(FINER)) { logger.log(FINER, "------ ACTION REPORT ------"); logger.log(FINER, String.valueOf(actionReport)); logger.log(FINER, "---- END ACTION REPORT ----"); } } } public ActionReport getActionReport() { return actionReport; } private static void addSubMessages(String indentPrefix, ActionReport.MessagePart mp, StringBuilder sb) { if (mp == null || sb == null) { return; } if (indentPrefix == null) { indentPrefix = ""; } List children = mp.getChildren(); if (children != null) { for (ActionReport.MessagePart subPart : children) { if (sb.length() > 0) { sb.append(EOL); } if (ok(subPart.getMessage())) { sb.append(subPart.getMessage()); } addSubMessages(indentPrefix + " ", subPart, sb); } } } private static void addCombinedMessages(CliActionReport aReport, StringBuilder sb) { if (aReport == null || sb == null) { return; } String mainMsg = ""; //this is the message related to the topMessage String failMsg; //this is the message related to failure cause // Other code in the server may write something like report.setMessage(exception.getMessage()) // and also set report.setFailureCause(exception). We need to avoid the duplicate message. if (aReport.getMessage() != null && aReport.getMessage().length() != 0) { if (sb.length() > 0) { sb.append(EOL); } sb.append(aReport.getMessage()); } if (aReport.getFailureCause() != null && aReport.getFailureCause().getMessage() != null && aReport.getFailureCause().getMessage().length() != 0) { failMsg = aReport.getFailureCause().getMessage(); if (!failMsg.equals(mainMsg)) { if (sb.length() > 0) { sb.append(EOL); } } sb.append(failMsg); } for (CliActionReport sub : aReport.getSubActionsReport()) { addCombinedMessages(sub, sb); } } 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; /* * 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 { final AuthenticationInfo authInfo = authenticationInfo(); if (logger.isLoggable(FINER)) { logger.log(FINER, "URI: {0}", uriString); logger.log(FINER, "URL: {0}", url.toURL(uriString).toString()); logger.log(FINER, "Method: {0}", httpMethod); logger.log(FINER, "Password options: {0}", passwordOptions); logger.log(FINER, "Using auth info: {0}", authInfo); } 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()); } urlConnection.addRequestProperty("Cache-Control", "no-cache"); urlConnection.addRequestProperty("Pragma", "no-cache"); if (authToken != null) { /* * If this request is for metadata then we expect to reuse * the auth token. */ urlConnection.setRequestProperty(SecureAdmin.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(FINER)) { logger.log(FINER, "CommandModel ETag: {0}", ((CachedCommandModel) commandModel).getETag()); } } urlConnection.setRequestMethod(httpMethod); urlConnection.setReadTimeout(readTimeout); if (connectTimeout >= 0) { urlConnection.setConnectTimeout(connectTimeout); } addAdditionalHeaders(urlConnection); urlConnection.addRequestProperty("X-Requested-By", "cli"); 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(FINER, "Following redirection to " + 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(FINER, "DAS has challenged for credentials"); /* * Try to update the credentials if we haven't already done so. */ if (askedUserForCredentials) { /* * We already updated the credentials once, and the updated * ones did not work. No recourse. */ logger.log(FINER, "Already tried with updated credentials; cannot authenticate"); throw authEx; } /* * Try to update the creds. */ logger.log(FINER, "Try to update credentials"); if (!updateAuthentication()) { /* * No updated credentials are avaiable, so we * have no more options. */ logger.log(FINER, "Could not update credentials; cannot authenticate"); throw authEx; } /* * We have another set of credentials we can try. */ logger.log(FINER, "Was able to update the credentials so will retry with the updated ones"); askedUserForCredentials = true; shouldTryCommandAgain = true; continue; } catch (ConnectException ce) { logger.log(FINER, "doHttpCommand: connect exception {0}", 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(FINER, "doHttpCommand: host exception {0}", he); // bad host name String msg = strings.get("UnknownHostException", host); throw new CommandException(msg, he); } catch (SocketException se) { logger.log(FINER, "doHttpCommand: socket exception {0}", se); throw new CommandException(se); } catch (SSLException se) { logger.log(FINER, "doHttpCommand: SSL exception {0}", se); if (secure) { logger.log(SEVERE, AdminLoggerInfo.mServerIsNotSecure, new Object[] { host, port }); } throw new CommandException(se); } catch (SocketTimeoutException e) { logger.log(FINER, "doHttpCommand: read timeout {0}", e); throw new CommandException(strings.get("ReadTimeout", (float) readTimeout / 1000), e); } catch (IOException e) { logger.log(FINER, "doHttpCommand: IO exception {0}", e); throw new CommandException(strings.get("IOError", e.getMessage()), e); } catch (CommandException e) { throw e; } catch (Exception e) { logger.log(FINER, "Something went wrong: " + e.getMessage(), e); 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(FINER)) { logger.log(FINER, "Response code: " + 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."); } if (code == HttpURLConnection.HTTP_NOT_FOUND) { try (InputStream errorStream = urlConnection.getErrorStream()) { throw new InvalidCommandException( ProprietaryReaderFactory. getReader(String.class, urlConnection.getContentType()) .readFrom(errorStream, urlConnection.getContentType())); } catch (IOException ioex) { throw new InvalidCommandException(urlConnection.getResponseMessage()); } } /* * 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 && code != HttpURLConnection.HTTP_INTERNAL_ERROR) { throw new CommandException(strings.get("BadResponse", String.valueOf(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 an option for a file argument, passing the name (for uploads) or the path (for no-upload) operations. * * @param params 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 void addFileOption(ParameterMap params, String optionName, String filename) throws IOException, CommandException { File f = SmartFile.sanitize(new File(filename)); logger.finer("FILE PARAM: " + 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()); params.add(optionName, pathToPass); } } /** * Fetch the command metadata from the remote server. */ protected void fetchCommandModel() throws CommandException { final long startNanos = System.nanoTime(); commandModel = null; //For sure not be used during request header construction doHttpCommand(getCommandURI(), "GET", new HttpCommand() { @Override public void prepareConnection(HttpURLConnection urlConnection) { urlConnection.setRequestProperty("Accept", MEDIATYPE_JSON); } @Override public void useConnection(HttpURLConnection urlConnection) throws CommandException, IOException { String eTag = urlConnection.getHeaderField("ETag"); if (eTag != null) { eTag = eTag.trim(); if (eTag.startsWith("W/")) { eTag = eTag.substring(2).trim(); } if (eTag.startsWith("\"")) { eTag = eTag.substring(1); } if (eTag.endsWith("\"")) { eTag = eTag.substring(0, eTag.length() - 1); } } String json = ProprietaryReaderFactory.getReader(String.class, urlConnection.getContentType()) .readFrom(urlConnection.getInputStream(), urlConnection.getContentType()); commandModel = parseMetadata(json, eTag); if (commandModel != null) { commandModelFromCache = false; if (logger.isLoggable(FINEST)) { logger.log(FINEST, "Command model for {0} command fetched from remote server. [Duration: {1} nanos]", new Object[] { name, System.nanoTime() - startNanos }); } try { StringBuilder forCache = new StringBuilder(json.length() + 40); forCache.append("ETag: ").append(eTag); forCache.append("\n"); forCache.append(json); AdminCacheUtils.getCache().put(createCommandCacheKey(), forCache.toString()); } catch (Exception ex) { if (logger.isLoggable(WARNING)) { logger.log(WARNING, AdminLoggerInfo.mCantPutToCache, new Object[] { createCommandCacheKey() }); } } } else { throw new InvalidCommandException(strings.get("unknownError")); } } }); } public String getManPage() throws CommandException { if (manpage == null) { doHttpCommand(getCommandURI() + "/manpage", "GET", new HttpCommand() { @Override public void prepareConnection(HttpURLConnection urlConnection) { urlConnection.setRequestProperty("Accept", MEDIATYPE_TXT); } @Override public void useConnection(HttpURLConnection urlConnection) throws CommandException, IOException { manpage = ProprietaryReaderFactory.getReader(String.class, urlConnection.getContentType()) .readFrom(urlConnection.getInputStream(), urlConnection.getContentType()); } }); } return manpage; } private String createCommandCacheKey() { StringBuilder result = new StringBuilder(getCanonicalHost().length() + name.length() + 12); result.append("cache/"); 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; } 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; } } /** * 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 (ok(upString)) { doUpload = Boolean.parseBoolean(upString); } else { doUpload = !isLocal(host) && sawUploadableFile; } if (prohibitDirectoryUploads && sawDirectory && doUpload) { // oops, can't upload directories logger.finer("--upload=" + upString + ", doUpload=" + 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.finer("doUpload set to " + 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 (ok(def)) { val = def; } } } return val; } private static boolean ok(String s) { return s != null && s.length() > 0; } /** * Can be called to start async preinitialisation. It can help a little bit in usage performance. */ public static void preinit() { Thread thread = new Thread(new Runnable() { @Override public void run() { ProprietaryReaderFactory.getReader(Class.class, "not/defined"); ProprietaryWriterFactory.getWriter(Class.class); } }); thread.setDaemon(true); thread.start(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy