com.sun.enterprise.v3.admin.AdminAdapter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of payara-micro Show documentation
Show all versions of payara-micro Show documentation
Micro Distribution of the Payara Project
/*
* 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.
*/
package com.sun.enterprise.v3.admin;
import com.sun.enterprise.admin.remote.RemoteRestAdminCommand;
import com.sun.enterprise.config.serverbeans.*;
import com.sun.enterprise.module.ModulesRegistry;
import com.sun.enterprise.module.common_impl.LogHelper;
import com.sun.enterprise.universal.GFBase64Decoder;
import com.sun.enterprise.util.LocalStringManagerImpl;
import com.sun.enterprise.util.SystemPropertyConstants;
import com.sun.enterprise.util.uuid.UuidGenerator;
import com.sun.enterprise.util.uuid.UuidGeneratorImpl;
import com.sun.enterprise.v3.admin.adapter.AdminEndpointDecider;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Named;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import org.glassfish.admin.payload.PayloadImpl;
import org.glassfish.api.ActionReport;
import org.glassfish.api.admin.*;
import org.glassfish.api.container.Adapter;
import org.glassfish.api.event.EventListener;
import org.glassfish.api.event.EventTypes;
import org.glassfish.api.event.Events;
import org.glassfish.api.event.RestrictTo;
import org.glassfish.grizzly.http.Cookie;
import org.glassfish.grizzly.http.server.HttpHandler;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.grizzly.http.server.Response;
import org.glassfish.grizzly.http.server.StaticHttpHandler;
import org.glassfish.grizzly.http.util.CookieSerializerUtils;
import org.glassfish.grizzly.http.util.HttpStatus;
import org.glassfish.hk2.api.PostConstruct;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.internal.api.AdminAccessController;
import org.glassfish.internal.api.Privacy;
import org.glassfish.internal.api.RemoteAdminAccessException;
import org.glassfish.internal.api.ServerContext;
import org.glassfish.kernel.KernelLoggerInfo;
import org.glassfish.server.ServerEnvironmentImpl;
/**
* Listen to admin commands...
* @author dochez
*/
public abstract class AdminAdapter extends StaticHttpHandler implements Adapter, PostConstruct, EventListener {
public final static String VS_NAME="__asadmin";
public final static String PREFIX_URI = "/" + VS_NAME;
private final static LocalStringManagerImpl adminStrings = new LocalStringManagerImpl(AdminAdapter.class);
private final static Logger aalogger = KernelLoggerInfo.getLogger();
private static final GFBase64Decoder decoder = new GFBase64Decoder();
private static final String BASIC = "Basic ";
private static final String SET_COOKIE_HEADER = "Set-Cookie";
public static final String SESSION_COOKIE_NAME = "JSESSIONID";
public static final int MAX_AGE = 86400 ;
public static final String ASADMIN_PATH="/__asadmin";
private static final String QUERY_STRING_SEPARATOR = "&";
@Inject
ModulesRegistry modulesRegistry;
@Inject
CommandRunnerImpl commandRunner;
@Inject
ServerEnvironmentImpl env;
@Inject
Events events;
@Inject @Named(ServerEnvironment.DEFAULT_INSTANCE_NAME)
Config config;
private AdminEndpointDecider epd = null;
@Inject
ServerContext sc;
@Inject
ServiceLocator habitat;
@Inject @Named(ServerEnvironment.DEFAULT_INSTANCE_NAME)
volatile AdminService as;
@Inject
volatile Domain domain;
@Inject @Named(ServerEnvironment.DEFAULT_INSTANCE_NAME)
private volatile Server server;
@Inject
AdminAccessController authenticator;
final Class extends Privacy> privacyClass;
private boolean isRegistered = false;
CountDownLatch latch = new CountDownLatch(1);
@SuppressWarnings({ "unchecked", "rawtypes" })
protected AdminAdapter(Class extends Privacy> privacyClass) {
super((Set) null);
this.privacyClass = privacyClass;
}
@Override
public final HttpHandler getHttpService() {
return this;
}
@Override
public void postConstruct() {
events.register(this);
epd = new AdminEndpointDecider(config);
addDocRoot(env.getProps().get(SystemPropertyConstants.INSTANCE_ROOT_PROPERTY) + "/asadmindocroot/");
}
/**
* Call the service method, and notify all listeners
*
* @exception Exception if an error happens during handling of
* the request. Common errors are:
* - IOException if an input/output error occurs and we are
* processing an included servlet (otherwise it is swallowed and
* handled by the top level error handler mechanism)
*
- ServletException if a servlet throws an exception and
* we are processing an included servlet (otherwise it is swallowed
* and handled by the top level error handler mechanism)
*
* Tomcat should be able to handle and log any other exception ( including
* runtime exceptions )
*/
@Override
public void onMissingResource(Request req, Response res) {
LogHelper.getDefaultLogger().log(Level.FINER, "Received something on {0}", req.getRequestURI());
LogHelper.getDefaultLogger().log(Level.FINER, "QueryString = {0}", req.getQueryString());
HttpStatus statusCode = HttpStatus.OK_200;
String requestURI = req.getRequestURI();
/* if (requestURI.startsWith("/__asadmin/ADMINGUI")) {
super.service(req, res);
}*/
ActionReport report = getClientActionReport(requestURI, req);
// remove the qualifier if necessary
if (requestURI.indexOf('.')!=-1) {
requestURI = requestURI.substring(0, requestURI.indexOf('.'));
}
Payload.Outbound outboundPayload = PayloadImpl.Outbound.newInstance();
try {
if (!latch.await(20L, TimeUnit.SECONDS)) {
report = getClientActionReport(req.getRequestURI(), req);
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
report.setMessage("V3 cannot process this command at this time, please wait");
} else {
final Subject s = (authenticator == null) ? null : authenticator.loginAsAdmin(req);
if (s == null) {
reportAuthFailure(res, report, "adapter.auth.userpassword",
"Invalid user name or password",
HttpURLConnection.HTTP_UNAUTHORIZED,
"WWW-Authenticate", "BASIC");
return;
}
report = doCommand(requestURI, req, report, outboundPayload, s);
}
} catch (ProcessHttpCommandRequestException reqEx) {
report = reqEx.getReport();
statusCode = reqEx.getResponseStatus();
} catch(InterruptedException e) {
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
report.setMessage("V3 cannot process this command at this time, please wait");
} catch (Exception e) {
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
report.setMessage("Exception while processing command: " + e);
}
try {
res.setStatus(statusCode);
/*
* Format the command result report into the first part (part #0) of
* the outbound payload and set the response's content type based
* on the payload's. If the report is the only part then the
* stream will be written as content type text/something and
* will contain only the report. If the payload already has
* content - such as files to be downloaded, for example - then the
* content type of the payload reflects its multi-part nature and
* an implementation-specific content type will be set in the response.
*/
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
report.writeReport(baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
final Properties reportProps = new Properties();
reportProps.setProperty("data-request-type", "report");
outboundPayload.addPart(0, report.getContentType(), "report",
reportProps, bais);
res.setContentType(outboundPayload.getContentType());
String commandName = req.getRequestURI().substring(getContextRoot().length() + 1);
//Check session routing for commands that have @ExecuteOn(RuntimeType.SINGLE_INSTANCE)
if ( isSingleInstanceCommand(commandName)) {
res.addHeader(SET_COOKIE_HEADER, getCookieHeader(req));
}
outboundPayload.writeTo(res.getOutputStream());
res.getOutputStream().flush();
res.finish();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* This method checks if the request has a Cookie header and
* if the instance name serving the request is the same as the
* jvmRoute information
* @param req Request to examine the Cookie header
* @return true if the Cookie header is set and the jvmRoute information is the same as
* the instance serving the request , false otherwise
*
*/
public boolean hasCookieHeader(Request req) {
String[] nameValuePair = getJSESSIONIDHeaders(req);
if (nameValuePair != null ) {
String headerValue = nameValuePair[1];
int index = headerValue.lastIndexOf('.');
return headerValue.substring(index+1)
.equals(server.getName())? true : false;
}
return false;
}
/**
* This method will return the Cookie header with name JSESSIONID="..."
* @param req The request which may contain cookie headers
* @return cookie header
*/
public String[] getJSESSIONIDHeaders(Request req) {
for (String header : req.getHeaders("Cookie")){
String cookieHeaders[] = header.trim().split(";");
for (String cookieHeader:cookieHeaders) {
String[] nameValuePair = cookieHeader.trim().split("=");
if (nameValuePair[0].equals(SESSION_COOKIE_NAME)) {
return nameValuePair;
}
}
}
return null;
}
/**
* This method checks if this command has @ExecuteOn annotation with
* RuntimeType.SINGle_INSTANCE
* @param commandName the command which is executed
* @return true only if @ExecuteOn has RuntimeType.SINGLE_INSTANCE false for
* other cases
*/
public boolean isSingleInstanceCommand(String commandName) {
CommandModel model = commandRunner.getModel(getScope(commandName),getCommandAfterScope(commandName),aalogger) ;
if (model != null ) {
ExecuteOn executeOn = model.getClusteringAttributes();
if ((executeOn != null) && (executeOn.value().length ==1) &&
executeOn.value()[0].equals(RuntimeType.SINGLE_INSTANCE)) {
return true;
}
}
return false;
}
/**
* This will create a unique SessionId, Max-Age,Version,Path to be added to the Set-Cookie header
* @return Set-Cookie2 header
*/
public String getCookieHeader(Request req) {
String sessionId = null;
// If the request has a Cookie header and
// there is no failover then send back the same
// JSESSIONID in Set-Cookie2 header
if ( hasCookieHeader(req)) {
sessionId = getJSESSIONIDHeaders(req)[1];
} else {
//There is no Cookie header in request so generate a new JSESSIONID or
//failover has occured in which case you can generate a new JSESSIONID
sessionId = createSessionId();
}
StringBuilder sb = new StringBuilder();
final Cookie cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
cookie.setMaxAge(MAX_AGE);
cookie.setPath(ASADMIN_PATH);
cookie.setVersion(1);
CookieSerializerUtils.serializeServerCookie(sb, true, false, false, cookie);
return sb.toString();
}
/**
* This will create a new sessionId and add the server name as a jvmroute information to it
* @return String to be used for the JSESSIONID Set-Cookie2 header
*/
public String createSessionId(){
UuidGenerator uuidGenerator = new UuidGeneratorImpl();
StringBuffer sessionBuf = new StringBuffer();
String sessionId = uuidGenerator.generateUuid();
sessionBuf.append(sessionId).append('.').append(server.getName());
return sessionBuf.toString();
}
public AdminAccessController.Access authenticate(Request req) throws Exception {
/*
* At this point, this method should be obsolete. But in case it
* comes back to life it now conforms to the new API for loginAsAdmin.
* That is, loginAsAdmin throws a RemoteAdminAccessException if the
* request is remote but secure admin is disabled and it throws a
* LoginException if the user is not a legitimate administrator.
* Further, loginAsAdmin now does nothing regarding full vs. read-only
* access; those decisions are made during authorization of particular
* commands.
*/
try {
authenticator.loginAsAdmin(req);
return (env.isDas() ? AdminAccessController.Access.FULL : AdminAccessController.Access.READONLY);
} catch (RemoteAdminAccessException ex) {
return AdminAccessController.Access.FORBIDDEN;
} catch (LoginException ex) {
return AdminAccessController.Access.NONE;
}
}
/** A convenience method to extract user name from a request. It assumes the HTTP Basic Auth.
*
* @param req instance of Request
* @return a two-element string array. If Auth header exists and can be correctly decoded, returns the user name
* and password as the two elements. If any error occurs or if the header does not exist, returns an array with
* two blank strings. Never returns a null.
* @throws IOException in case of error with decoding the buffer (HTTP basic auth)
*/
public static String[] getUserPassword(Request req) throws IOException {
//implementation note: other adapters make use of this method
String authHeader = req.getHeader("Authorization");
if (authHeader == null) {
return new String[]{"", ""};
}
String enc = authHeader.substring(BASIC.length());
String dec = new String(decoder.decodeBuffer(enc));
int i = dec.indexOf(':');
if (i < 0)
return new String[] { "", "" };
return new String[] { dec.substring(0, i), dec.substring(i + 1) };
}
private void reportAuthFailure(final Response res,
final ActionReport report,
final String msgKey,
final String msg,
final int httpStatus,
final String headerName,
final String headerValue) throws IOException {
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
final String messageForResponse = adminStrings.getLocalString(msgKey, msg);
report.setMessage(messageForResponse);
report.setActionDescription("Authentication error");
res.setStatus(httpStatus, messageForResponse);
if (headerName != null) {
res.setHeader(headerName, headerValue);
}
res.setContentType(report.getContentType());
report.writeReport(res.getOutputStream());
res.getOutputStream().flush();
res.finish();
}
private ActionReport getClientActionReport(String requestURI, Request req) {
ActionReport report=null;
// first we look at the command extension (ie list-applications.[json | html | mf]
if (requestURI.indexOf('.')!=-1) {
String qualifier = requestURI.substring(requestURI.indexOf('.')+1);
report = habitat.getService(ActionReport.class, qualifier);
} else {
String userAgent = req.getHeader("User-Agent");
if (userAgent!=null)
report = habitat.getService(ActionReport.class, userAgent.substring(userAgent.indexOf('/')+1));
if (report==null) {
String accept = req.getHeader("Accept");
if (accept!=null) {
StringTokenizer st = new StringTokenizer(accept, ",");
while (report==null && st.hasMoreElements()) {
final String scheme=st.nextToken();
report = habitat.getService(ActionReport.class, scheme.substring(scheme.indexOf('/')+1));
}
}
}
}
if (report==null) {
// get the default one.
report = habitat.getService(ActionReport.class, "html");
}
return report;
}
protected abstract boolean validatePrivacy(AdminCommand command);
private ActionReport doCommand(String requestURI, Request req, ActionReport report,
Payload.Outbound outboundPayload, Subject subject) throws ProcessHttpCommandRequestException {
if (!requestURI.startsWith(getContextRoot())) {
String msg = adminStrings.getLocalString("adapter.panic",
"Wrong request landed in AdminAdapter {0}", requestURI);
report.setMessage(msg);
LogHelper.getDefaultLogger().info(msg);
return report;
}
// wbn handle no command and no slash-suffix
String command ="";
if (requestURI.length() > getContextRoot().length() + 1) {
command = requestURI.substring(getContextRoot().length() + 1);
}
String scope = getScope(command);
command = getCommandAfterScope(command);
String qs = req.getQueryString();
final ParameterMap parameters = extractParameters(qs);
String passwordOptions = req.getHeader("X-passwords");
if (passwordOptions != null) {
decodePasswords(parameters, passwordOptions);
}
try {
Payload.Inbound inboundPayload = PayloadImpl.Inbound
.newInstance(req.getContentType(), req.getInputStream());
if (aalogger.isLoggable(Level.FINE)) {
aalogger.log(Level.FINE, "***** AdminAdapter {0} *****", req.getMethod());
}
AdminCommand adminCommand = commandRunner.getCommand(scope, command, report, aalogger);
if (adminCommand==null) {
// maybe commandRunner already reported the failure?
if (report.getActionExitCode() == ActionReport.ExitCode.FAILURE)
return report;
String message =
adminStrings.getLocalString("adapter.command.notfound",
"Command {0} not found", command);
// cound't find command, not a big deal
aalogger.log(Level.FINE, message);
report.setMessage(message);
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
return report;
}
//Validate admin command eTag
String modelETag = req.getHeader(RemoteRestAdminCommand.COMMAND_MODEL_MATCH_HEADER);
if (modelETag != null && !commandRunner.validateCommandModelETag(adminCommand, modelETag)) {
String message =
adminStrings.getLocalString("commandmodel.etag.invalid",
"Cached command model for command {0} is invalid.", command);
aalogger.log(Level.FINE, message);
report.setMessage(message);
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
throw new ProcessHttpCommandRequestException(report, HttpStatus.PRECONDITION_FAILED_412);
}
//Execute
if (validatePrivacy(adminCommand)) {
//if (adminCommand.getClass().getAnnotation(Visibility.class).privacy().equals(visibility.privacy())) {
// todo : needs to be changed, we should reuse adminCommand
CommandRunner.CommandInvocation inv = commandRunner.getCommandInvocation(scope, command, report, subject,parameters.containsKey("notify"));
inv.parameters(parameters).inbound(inboundPayload).outbound(outboundPayload).execute();
try {
// note it has become extraordinarily difficult to change the reporter!
CommandRunnerImpl.ExecutionContext inv2 = (CommandRunnerImpl.ExecutionContext) inv;
report = inv2.report();
}
catch(Exception e) {
}
} else {
report.failure( aalogger,
adminStrings.getLocalString("adapter.wrongprivacy",
"Command {0} does not have {1} visibility",
command, privacyClass.getSimpleName().toLowerCase(Locale.ENGLISH)),
null);
return report;
}
} catch (ProcessHttpCommandRequestException reqEx) {
throw reqEx;
} catch (Throwable t) {
/*
* Must put the error information into the report
* for the client to see it.
*/
report.setActionExitCode(ActionReport.ExitCode.FAILURE);
report.setFailureCause(t);
report.setMessage(t.getLocalizedMessage());
report.setActionDescription("Last-chance AdminAdapter exception handler");
}
return report;
}
/**
* Finish the response and recycle the request/response tokens. Base on
* the connection header, the underlying socket transport will be closed
*/
public void afterService(Request req, Response res) throws Exception {
}
/**
* Notify all container event listeners that a particular event has
* occurred for this Adapter. The default implementation performs
* this notification synchronously using the calling thread.
*
* @param type Event type
* @param data Event data
*/
public void fireAdapterEvent(String type, Object data) {
}
/**
* decode the parameters that were passed in the X-Passwords header
*
* @params requestString value of the X-Passwords header
* @returns a decoded requestString
*/
void decodePasswords(ParameterMap pmap, final String requestString) {
StringTokenizer stoken = new StringTokenizer(requestString == null ? "" : requestString, QUERY_STRING_SEPARATOR);
while (stoken.hasMoreTokens()) {
String token = stoken.nextToken();
if (token.indexOf("=") == -1)
continue;
String paramName = token.substring(0, token.indexOf("="));
String value = token.substring(token.indexOf("=") + 1);
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
aalogger.log(Level.WARNING, KernelLoggerInfo.cantDecodeParameter,
new Object[] { paramName, value });
continue;
}
try {
value = new String(decoder.decodeBuffer(value));
} catch (IOException e) {
aalogger.log(Level.WARNING, KernelLoggerInfo.cantDecodeParameter,
new Object[] { paramName, value });
continue;
}
pmap.add(paramName, value);
}
}
/**
* extract parameters from URI and save it in ParameterMap obj
*
* @params requestString string URI to extract
*
* @returns ParameterMap
*/
ParameterMap extractParameters(final String requestString) {
// extract parameters...
final ParameterMap parameters = new ParameterMap();
StringTokenizer stoken = new StringTokenizer(requestString == null ? "" : requestString, QUERY_STRING_SEPARATOR);
while (stoken.hasMoreTokens()) {
String token = stoken.nextToken();
if (token.indexOf("=") == -1)
continue;
String paramName = token.substring(0, token.indexOf("="));
String value = token.substring(token.indexOf("=") + 1);
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
aalogger.log(Level.WARNING, KernelLoggerInfo.cantDecodeParameter,
new Object[] {paramName, value});
}
parameters.add(paramName, value);
}
// Dump parameters...
if (aalogger.isLoggable(Level.FINER)) {
for (Map.Entry> entry : parameters.entrySet()) {
for (String v : entry.getValue())
aalogger.log(Level.FINER, "Key {0} = {1}", new Object[]{entry.getKey(), v});
}
}
return parameters;
}
@Override
public void event(@RestrictTo(EventTypes.SERVER_READY_NAME) Event event) {
if (event.is(EventTypes.SERVER_READY)) {
latch.countDown();
aalogger.fine("Ready to receive administrative commands");
}
//the count-down does not start if any other event is received
}
@Override
public int getListenPort() {
return epd.getListenPort();
}
@Override
public InetAddress getListenAddress() {
return epd.getListenAddress();
}
@Override
public List getVirtualServers() {
return epd.getAsadminHosts();
}
/**
* Checks whether this adapter has been registered as a network endpoint.
*/
@Override
public boolean isRegistered() {
return isRegistered;
}
/**
* Marks this adapter as having been registered or unregistered as a
* network endpoint
*/
@Override
public void setRegistered(boolean isRegistered) {
this.isRegistered = isRegistered;
}
/**
* A command is defined in a particular scope by
* using a prefix on the command service names, as in @Service(name="ascope/mycommand")
* This method gets the scope for a command which is "ascope/"
* for the above example
* @param command The command to be executed
* @return the scope for a command
*/
private String getScope(String command) {
int ci = command.indexOf("/");
return (ci != -1) ? command.substring(0, ci + 1) : null;
}
/**
* This method gets the command after the scope string
* as defined for a command like this @Service(name="ascope/mycommand")
* @param command The command to be executed
* @return the shortened command after the scope ie "mycommand"
* for the above example
*/
private String getCommandAfterScope(String command) {
int ci = command.indexOf("/");
return (ci != -1) ? command = command.substring(ci + 1) : command;
}
}