blob: e023a706ffb6b99bf6b4c8a6ff931b3205a039eb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.tunnel;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.List;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleSession;
import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.event.TunnelCloseEvent;
import org.apache.guacamole.net.event.TunnelConnectEvent;
import org.apache.guacamole.rest.auth.AuthenticationService;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
import org.apache.guacamole.rest.event.ListenerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class that takes a standard request from the Guacamole JavaScript
* client and produces the corresponding GuacamoleTunnel. The implementation
* of this utility is specific to the form of request used by the upstream
* Guacamole web application, and is not necessarily useful to applications
* that use purely the Guacamole API.
*/
@Singleton
public class TunnelRequestService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(TunnelRequestService.class);
/**
* A service for authenticating users from auth tokens.
*/
@Inject
private AuthenticationService authenticationService;
/**
* A service for notifying listeners about tunnel connect/closed events.
*/
@Inject
private ListenerService listenerService;
/**
* Notifies bound listeners that a new tunnel has been connected.
* Listeners may veto a connected tunnel by throwing any GuacamoleException.
*
* @param authenticatedUser
* The AuthenticatedUser associated with the user for whom the tunnel
* is being created.
*
* @param credentials
* Credentials that authenticate the user.
*
* @param tunnel
* The tunnel that was connected.
*
* @throws GuacamoleException
* If thrown by a listener or if any listener vetoes the connected tunnel.
*/
private void fireTunnelConnectEvent(AuthenticatedUser authenticatedUser,
Credentials credentials, GuacamoleTunnel tunnel) throws GuacamoleException {
listenerService.handleEvent(new TunnelConnectEvent(authenticatedUser,
credentials, tunnel));
}
/**
* Notifies bound listeners that a tunnel is to be closed.
* Listeners are allowed to veto a request to close a tunnel by throwing any
* GuacamoleException.
*
* @param authenticatedUser
* The AuthenticatedUser associated with the user for whom the tunnel
* is being closed.
*
* @param credentials
* Credentials that authenticate the user.
*
* @param tunnel
* The tunnel that was connected.
*
* @throws GuacamoleException
* If thrown by a listener.
*/
private void fireTunnelClosedEvent(AuthenticatedUser authenticatedUser,
Credentials credentials, GuacamoleTunnel tunnel)
throws GuacamoleException {
listenerService.handleEvent(new TunnelCloseEvent(authenticatedUser,
credentials, tunnel));
}
/**
* Reads and returns the client information provided within the given
* request.
*
* @param request
* The request describing tunnel to create.
*
* @return GuacamoleClientInformation
* An object containing information about the client sending the tunnel
* request.
*
* @throws GuacamoleException
* If the parameters of the tunnel request are invalid.
*/
protected GuacamoleClientInformation getClientInformation(TunnelRequest request)
throws GuacamoleException {
// Get client information
GuacamoleClientInformation info = new GuacamoleClientInformation();
// Set width if provided
Integer width = request.getWidth();
if (width != null)
info.setOptimalScreenWidth(width);
// Set height if provided
Integer height = request.getHeight();
if (height != null)
info.setOptimalScreenHeight(height);
// Set resolution if provided
Integer dpi = request.getDPI();
if (dpi != null)
info.setOptimalResolution(dpi);
// Add audio mimetypes
List<String> audioMimetypes = request.getAudioMimetypes();
if (audioMimetypes != null)
info.getAudioMimetypes().addAll(audioMimetypes);
// Add video mimetypes
List<String> videoMimetypes = request.getVideoMimetypes();
if (videoMimetypes != null)
info.getVideoMimetypes().addAll(videoMimetypes);
// Add image mimetypes
List<String> imageMimetypes = request.getImageMimetypes();
if (imageMimetypes != null)
info.getImageMimetypes().addAll(imageMimetypes);
return info;
}
/**
* Creates a new tunnel using which is connected to the connection or
* connection group identifier by the given ID. Client information
* is specified in the {@code info} parameter.
*
* @param context
* The UserContext associated with the user for whom the tunnel is
* being created.
*
* @param type
* The type of object being connected to (connection or group).
*
* @param id
* The id of the connection or group being connected to.
*
* @param info
* Information describing the connected Guacamole client.
*
* @return
* A new tunnel, connected as required by the request.
*
* @throws GuacamoleException
* If an error occurs while creating the tunnel.
*/
protected GuacamoleTunnel createConnectedTunnel(UserContext context,
final TunnelRequest.Type type, String id,
GuacamoleClientInformation info)
throws GuacamoleException {
// Create connected tunnel from identifier
GuacamoleTunnel tunnel = null;
switch (type) {
// Connection identifiers
case CONNECTION: {
// Get connection directory
Directory<Connection> directory = context.getConnectionDirectory();
// Get authorized connection
Connection connection = directory.get(id);
if (connection == null) {
logger.info("Connection \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
throw new GuacamoleSecurityException("Requested connection is not authorized.");
}
// Connect tunnel
tunnel = connection.connect(info);
logger.info("User \"{}\" connected to connection \"{}\".", context.self().getIdentifier(), id);
break;
}
// Connection group identifiers
case CONNECTION_GROUP: {
// Get connection group directory
Directory<ConnectionGroup> directory = context.getConnectionGroupDirectory();
// Get authorized connection group
ConnectionGroup group = directory.get(id);
if (group == null) {
logger.info("Connection group \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
throw new GuacamoleSecurityException("Requested connection group is not authorized.");
}
// Connect tunnel
tunnel = group.connect(info);
logger.info("User \"{}\" connected to group \"{}\".", context.self().getIdentifier(), id);
break;
}
// Type is guaranteed to be one of the above
default:
assert(false);
}
return tunnel;
}
/**
* Associates the given tunnel with the given session, returning a wrapped
* version of the same tunnel which automatically handles closure and
* removal from the session.
*
* @param tunnel
* The connected tunnel to wrap and monitor.
*
* @param authToken
* The authentication token associated with the given session. If
* provided, this token will be automatically invalidated (and the
* corresponding session destroyed) if tunnel errors imply that the
* user is no longer authorized.
*
* @param session
* The Guacamole session to associate the tunnel with.
*
* @param context
* The UserContext associated with the user for whom the tunnel is
* being created.
*
* @param type
* The type of object being connected to (connection or group).
*
* @param id
* The id of the connection or group being connected to.
*
* @return
* A new tunnel, associated with the given session, which delegates all
* functionality to the given tunnel while monitoring and automatically
* handling closure.
*
* @throws GuacamoleException
* If an error occurs while obtaining the tunnel.
*/
protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleTunnel tunnel,
final String authToken, final GuacamoleSession session,
final UserContext context, final TunnelRequest.Type type,
final String id) throws GuacamoleException {
// Monitor tunnel closure and data
UserTunnel monitoredTunnel = new UserTunnel(context, tunnel) {
/**
* The time the connection began, measured in milliseconds since
* midnight, January 1, 1970 UTC.
*/
private final long connectionStartTime = System.currentTimeMillis();
@Override
public void close() throws GuacamoleException {
// Notify listeners to allow close request to be vetoed
AuthenticatedUser authenticatedUser = session.getAuthenticatedUser();
fireTunnelClosedEvent(authenticatedUser,
authenticatedUser.getCredentials(), tunnel);
long connectionEndTime = System.currentTimeMillis();
long duration = connectionEndTime - connectionStartTime;
// Log closure
switch (type) {
// Connection identifiers
case CONNECTION:
logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds",
session.getAuthenticatedUser().getIdentifier(), id, duration);
break;
// Connection group identifiers
case CONNECTION_GROUP:
logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds",
session.getAuthenticatedUser().getIdentifier(), id, duration);
break;
// Type is guaranteed to be one of the above
default:
assert(false);
}
try {
// Close and clean up tunnel
session.removeTunnel(getUUID().toString());
super.close();
}
// Ensure any associated session is invalidated if unauthorized
catch (GuacamoleUnauthorizedException e) {
// If there is an associated auth token, invalidate it
if (authenticationService.destroyGuacamoleSession(authToken))
logger.debug("Implicitly invalidated session for token \"{}\".", authToken);
// Continue with exception processing
throw e;
}
}
};
// Associate tunnel with session
session.addTunnel(monitoredTunnel);
return monitoredTunnel;
}
/**
* Creates a new tunnel using the parameters and credentials present in
* the given request.
*
* @param request
* The request describing the tunnel to create.
*
* @return
* The created tunnel, or null if the tunnel could not be created.
*
* @throws GuacamoleException
* If an error occurs while creating the tunnel.
*/
public GuacamoleTunnel createTunnel(TunnelRequest request)
throws GuacamoleException {
// Parse request parameters
String authToken = request.getAuthenticationToken();
String id = request.getIdentifier();
TunnelRequest.Type type = request.getType();
String authProviderIdentifier = request.getAuthenticationProviderIdentifier();
GuacamoleClientInformation info = getClientInformation(request);
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
UserContext userContext = session.getUserContext(authProviderIdentifier);
try {
// Create connected tunnel using provided connection ID and client information
GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info);
// Notify listeners to allow connection to be vetoed
fireTunnelConnectEvent(session.getAuthenticatedUser(),
session.getAuthenticatedUser().getCredentials(), tunnel);
// Associate tunnel with session
return createAssociatedTunnel(tunnel, authToken, session, userContext, type, id);
}
// Ensure any associated session is invalidated if unauthorized
catch (GuacamoleUnauthorizedException e) {
// If there is an associated auth token, invalidate it
if (authenticationService.destroyGuacamoleSession(authToken))
logger.debug("Implicitly invalidated session for token \"{}\".", authToken);
// Continue with exception processing
throw e;
}
}
}