Package org.glyptodon.guacamole.net.basic

Source Code of org.glyptodon.guacamole.net.basic.AuthenticatingFilter

/*
* Copyright (C) 2013 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package org.glyptodon.guacamole.net.basic;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.bind.DatatypeConverter;
import org.glyptodon.guacamole.GuacamoleClientException;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.net.auth.Credentials;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.event.SessionListenerCollection;
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
import org.glyptodon.guacamole.net.event.AuthenticationFailureEvent;
import org.glyptodon.guacamole.net.event.AuthenticationSuccessEvent;
import org.glyptodon.guacamole.net.event.listener.AuthenticationFailureListener;
import org.glyptodon.guacamole.net.event.listener.AuthenticationSuccessListener;
import org.glyptodon.guacamole.properties.GuacamoleProperties;
import org.glyptodon.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Filter which provides watches requests for credentials, authenticating the
* user against the configured AuthenticationProvider if credentials are
* present. Note that if authentication fails, the request is still allowed. To
* restrict access based on the result of authentication, use
* RestrictedHttpServlet or RestrictedFilter.
*
* The user context is retrieved using the authentication provider defined in
* guacamole.properties. The authentication provider has access to the request
* and session, in addition to any submitted username and password, in order
* to authenticate the user.
*
* The user context will be stored in the current HttpSession.
*
* Success and failure are logged.
*
* @author Michael Jumper
*/
public class AuthenticatingFilter implements Filter {

    /**
     * Logger for this class.
     */
    private final Logger logger = LoggerFactory.getLogger(AuthenticatingFilter.class);

    /**
     * The session attribute holding the current UserContext.
     */
    public static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";

    /**
     * The session attribute holding the credentials authorizing this session.
     */
    public static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";

    /**
     * The session attribute holding the session-scoped clipboard storage.
     */
    public static final String CLIPBOARD_ATTRIBUTE = "GUAC_CLIP";
   
    /**
     * The AuthenticationProvider to use to authenticate all requests.
     */
    private AuthenticationProvider authProvider;

    /**
     * Whether HTTP authentication should be used (the "Authorization" header).
     */
    private boolean useHttpAuthentication;

    @Override
    public void init(FilterConfig config) throws ServletException {

        // Parse Guacamole configuration
        try {

            // Get auth provider instance
            authProvider = GuacamoleProperties.getRequiredProperty(BasicGuacamoleProperties.AUTH_PROVIDER);

            // Enable HTTP auth, if requested
            useHttpAuthentication = GuacamoleProperties.getProperty(BasicGuacamoleProperties.ENABLE_HTTP_AUTH, false);

        }
        catch (GuacamoleException e) {
            logger.error("Unable to read guacamole.properties: {}", e.getMessage());
            logger.debug("Error reading guacamole.properties.", e);
            throw new ServletException(e);
        }

    }

    /**
     * Notifies all listeners in the given collection that authentication has
     * failed.
     *
     * @param listeners A collection of all listeners that should be notified.
     * @param credentials The credentials associated with the authentication
     *                    request that failed.
     */
    private void notifyFailed(Collection listeners, Credentials credentials) {

        // Build event for auth failure
        AuthenticationFailureEvent event = new AuthenticationFailureEvent(credentials);

        // Notify all listeners
        for (Object listener : listeners) {
            try {
                if (listener instanceof AuthenticationFailureListener)
                    ((AuthenticationFailureListener) listener).authenticationFailed(event);
            }
            catch (GuacamoleException e) {
                logger.debug("Error notifying AuthenticationFailureListener: {}", e);
            }
        }

    }

    /**
     * Notifies all listeners in the given collection that authentication was
     * successful.
     *
     * @param listeners A collection of all listeners that should be notified.
     * @param context The UserContext created as a result of authentication
     *                success.
     * @param credentials The credentials associated with the authentication
     *                    request that succeeded.
     * @return true if all listeners are allowing the authentication success,
     *         or if there are no listeners, and false if any listener is
     *         canceling the authentication success. Note that once one
     *         listener cancels, no other listeners will run.
     * @throws GuacamoleException If any listener throws an error while being
     *                            notified. Note that if any listener throws an
     *                            error, the success is canceled, and no other
     *                            listeners will run.
     */
    private boolean notifySuccess(Collection listeners, UserContext context,
            Credentials credentials) throws GuacamoleException {

        // Build event for auth success
        AuthenticationSuccessEvent event =
                new AuthenticationSuccessEvent(context, credentials);

        // Notify all listeners
        for (Object listener : listeners) {
            if (listener instanceof AuthenticationSuccessListener) {

                // Cancel immediately if hook returns false
                if (!((AuthenticationSuccessListener) listener).authenticationSucceeded(event))
                    return false;

            }
        }

        return true;

    }

    /**
     * Sends an error on the given HTTP response using the information within
     * the given GuacamoleStatus.
     *
     * @param response The HTTP response to use to send the error.
     * @param guac_status The status to send
     * @param message A human-readable message that can be presented to the
     *                user.
     * @throws ServletException If an error prevents sending of the error
     *                          code.
     */
    public static void sendError(HttpServletResponse response,
            GuacamoleStatus guac_status, String message)
            throws ServletException {

        try {

            // If response not committed, send error code and message
            if (!response.isCommitted()) {
                response.addHeader("Guacamole-Status-Code", Integer.toString(guac_status.getGuacamoleStatusCode()));
                response.addHeader("Guacamole-Error-Message", message);
                response.sendError(guac_status.getHttpStatusCode());
            }

        }
        catch (IOException ioe) {

            // If unable to send error at all due to I/O problems,
            // rethrow as servlet exception
            throw new ServletException(ioe);

        }

    }

    /**
     * Returns the credentials associated with the given session.
     *
     * @param session The session to retrieve credentials from.
     * @return The credentials associated with the given session.
     */
    public static Credentials getCredentials(HttpSession session) {
        return (Credentials) session.getAttribute(CREDENTIALS_ATTRIBUTE);
    }

    /**
     * Returns the UserContext associated with the given session.
     *
     * @param session The session to retrieve UserContext from.
     * @return The UserContext associated with the given session.
     */
    public static UserContext getUserContext(HttpSession session) {
        return (UserContext) session.getAttribute(CONTEXT_ATTRIBUTE);
    }

    /**
     * Returns the ClipboardState associated with the given session. If none
     * exists yet, one is created.
     *
     * @param session The session to retrieve the ClipboardState from.
     * @return The ClipboardState associated with the given session.
     */
    public static ClipboardState getClipboardState(HttpSession session) {

        ClipboardState clipboard = (ClipboardState) session.getAttribute(CLIPBOARD_ATTRIBUTE);
        if (clipboard == null) {
            clipboard = new ClipboardState();
            session.setAttribute(CLIPBOARD_ATTRIBUTE, clipboard);
        }

        return clipboard;

    }

    /**
     * Returns whether the request given has updated credentials. If this
     * function returns false, the UserContext will not be updated.
     *
     * @param request The request to check for credentials.
     * @return true if the request contains credentials, false otherwise.
     */
    protected boolean hasNewCredentials(HttpServletRequest request) {
        return true;
    }

    /**
     * Regular expression which matches any IPv4 address.
     */
    private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})";

    /**
     * Regular expression which matches any IPv6 address.
     */
    private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})";

    /**
     * Regular expression which matches any IP address, regardless of version.
     */
    private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")";
   
    /**
     * Pattern which matches valid values of the de-facto standard
     * "X-Forwarded-For" header.
     */
    private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + "(, " + IP_ADDRESS_REGEX + ")*$");

    /**
     * Returns a formatted string containing an IP address, or list of IP
     * addresses, which represent the HTTP client and any involved proxies. As
     * the headers used to determine proxies can easily be forged, this data is
     * superficially validated to ensure that it at least looks like a list of
     * IPs.
     *
     * @param request The HTTP request to format.
     * @return A formatted string containing one or more IP addresses.
     */
    private String getLoggableAddress(HttpServletRequest request) {

        // Log X-Forwarded-For, if present and valid
        String header = request.getHeader("X-Forwarded-For");
        if (header != null && X_FORWARDED_FOR.matcher(header).matches())
            return "[" + header + ", " + request.getRemoteAddr() + "]";

        // If header absent or invalid, just use source IP
        return request.getRemoteAddr();
       
    }
   
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
    throws IOException, ServletException {
      
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
       
        // Set character encoding to UTF-8 if it's not already set
        if(request.getCharacterEncoding() == null) {
            try {
                request.setCharacterEncoding("UTF-8");
            } catch (UnsupportedEncodingException exception) {
               throw new ServletException(exception);
            }
        }

        try {

            // Obtain context from session
            HttpSession httpSession = request.getSession(true);
            UserContext context = getUserContext(httpSession);

            // If new credentials present, update/create context
            if (hasNewCredentials(request)) {

                // Retrieve username and password from parms
                String username = request.getParameter("username");
                String password = request.getParameter("password");

                // If no username/password given, try Authorization header
                if (useHttpAuthentication && username == null && password == null) {

                    String authorization = request.getHeader("Authorization");
                    if (authorization != null && authorization.startsWith("Basic ")) {

                        // Decode base64 authorization
                        String basicBase64 = authorization.substring(6);
                        String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8");

                        // Pull username/password from auth data
                        int colon = basicCredentials.indexOf(':');
                        if (colon != -1) {
                            username = basicCredentials.substring(0, colon);
                            password = basicCredentials.substring(colon+1);
                        }

                        else
                            logger.info("Invalid HTTP Basic \"Authorization\" header received.");

                    }

                } // end Authorization header fallback
               
                // Build credentials object
                Credentials credentials = new Credentials();
                credentials.setSession(httpSession);
                credentials.setRequest(request);
                credentials.setUsername(username);
                credentials.setPassword(password);

                SessionListenerCollection listeners = new SessionListenerCollection(httpSession);

                // If no cached context, attempt to get new context
                if (context == null) {

                    context = authProvider.getUserContext(credentials);

                    // Log successful authentication
                    if (context != null && logger.isInfoEnabled())
                        logger.info("User \"{}\" successfully authenticated from {}.",
                                context.self().getUsername(), getLoggableAddress(request));
                   
                }

                // Otherwise, update existing context
                else
                    context = authProvider.updateUserContext(context, credentials);

                // If auth failed, notify listeners
                if (context == null) {

                    if (logger.isWarnEnabled()) {

                        // Only bother logging failures involving usernames
                        if (credentials.getUsername() != null)
                            logger.info("Authentication attempt from {} for user \"{}\" failed.",
                                    getLoggableAddress(request), credentials.getUsername());
                        else
                            logger.debug("Authentication attempt from {} without username failed.",
                                    getLoggableAddress(request));
                    }

                    notifyFailed(listeners, credentials);
                }

                // If auth succeeded, notify and check with listeners
                else if (!notifySuccess(listeners, context, credentials))
                    logger.info("Successful authentication canceled by hook.");

                // If auth still OK, associate context with session
                else {
                    httpSession.setAttribute(CONTEXT_ATTRIBUTE,     context);
                    httpSession.setAttribute(CREDENTIALS_ATTRIBUTE, credentials);
                }

            } // end if credentials present

            // Allow servlet to run now that authentication has been validated
            chain.doFilter(request, response);

        }

        // Catch any thrown guacamole exception and attempt to pass within the
        // HTTP response, logging each error appropriately.
        catch (GuacamoleClientException e) {
            logger.info("HTTP request rejected: {}", e.getMessage());
            logger.debug("HTTP request rejected by AuthenticatingFilter.", e);
            sendError(response, e.getStatus(), e.getMessage());
        }
        catch (GuacamoleException e) {
            logger.error("Authentication failed internally: {}", e.getMessage());
            logger.debug("Internal server error during authentication.", e);
            sendError(response, e.getStatus(), "Internal server error.");
        }

    }

    @Override
    public void destroy() {
        // No destruction needed
    }

}
TOP

Related Classes of org.glyptodon.guacamole.net.basic.AuthenticatingFilter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.