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

com.sun.enterprise.v3.admin.AdminAdapter Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * 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 privacyClass;

    private boolean isRegistered = false;
            
    CountDownLatch latch = new CountDownLatch(1);

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected AdminAdapter(Class 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy