/* * ProActive Parallel Suite(TM): * The Open Source library for parallel and distributed * Workflows & Scheduling, Orchestration, Cloud Automation * and Big Data Analysis on Enterprise Grids & Clouds. * * Copyright (c) 2007 - 2017 ActiveEon * Contact: contact@activeeon.com * * This library is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation: version 3 of * the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * If needed, contact us to obtain a release under GPL Version 2 or 3 * or a different license than the AGPL. */ package org.ow2.proactive_grid_cloud_portal.webapp; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.HttpRequest; import org.objectweb.proactive.core.util.log.ProActiveLogger; import org.ow2.proactive.scheduler.common.exception.NotConnectedException; import org.ow2.proactive.scheduler.common.exception.PermissionException; import org.ow2.proactive.scheduler.common.exception.UnknownJobException; import org.ow2.proactive.scheduler.common.exception.UnknownTaskException; import org.ow2.proactive.scheduler.common.job.JobState; import org.ow2.proactive.scheduler.common.task.TaskResult; import org.ow2.proactive.scheduler.common.task.TaskState; import org.ow2.proactive.scheduler.common.util.SchedulerProxyUserInterface; import org.ow2.proactive_grid_cloud_portal.common.Session; import org.ow2.proactive_grid_cloud_portal.common.SharedSessionStore; import com.netiq.websockify.IProxyTargetResolver; /** * A target resolved for websocket proxy capable of reading parameters from the websocket path. * <p> * The parameters are used to retrieve a task's logs and check for the "PA_REMOTE_CONNECTION" string to appear. * Then parameters of "PA_REMOTE_CONNECTION" are used to proxy the websocket connection. */ public class NoVncSecuredTargetResolver implements IProxyTargetResolver { private static final Logger LOGGER = ProActiveLogger.getLogger(NoVncSecuredTargetResolver.class); private static final String PLATFORM_INDEPENDENT_LINE_BREAK = "\r\n?|\n"; private static final String PA_REMOTE_CONNECTION = "PA_REMOTE_CONNECTION"; private static final String PA_REMOTE_CONNECTION_SEPARATOR = ";"; private static final String PA_REMOTE_CONNECTION_HOST_PORT_SEPARATOR = ":"; private static final String VNC_PROTOCOL = "vnc"; @Override public InetSocketAddress resolveTarget(MessageEvent messageEvent) { if (messageEvent == null) { return null; // websockify switched to direct proxy handler, not supported here } Map<String, String> parameters = getQueryMap(((HttpRequest) messageEvent.getMessage()).getUri()); String sessionId = parameters.get("sessionId"); String jobId = parameters.get("jobId"); String taskName = parameters.get("taskName"); return doResolve(sessionId, jobId, taskName); } // package-protected for testing InetSocketAddress doResolve(String sessionId, String jobId, String taskName) { if (sessionId == null || jobId == null || taskName == null) { LOGGER.warn("One of the web socket path parameter is missing (sessionId, jobId, taskName)."); return null; } Session session = SharedSessionStore.getInstance().get(sessionId); if (session == null) { LOGGER.warn("Unknown sessionId."); return null; } SchedulerProxyUserInterface scheduler = session.getScheduler(); try { TaskResult taskResult = scheduler.getTaskResult(jobId, taskName); List<String> paRemoteConnectionLines = retrievePaRemoteConnectionLines(session, jobId, taskResult); String taskId = retrieveTaskId(taskName, scheduler.getJobState(jobId)); return resolveVncTargetFromLogs(paRemoteConnectionLines, jobId, taskId); } catch (NotConnectedException e) { LOGGER.warn("Failed to connect to scheduler", e); } catch (UnknownJobException e) { LOGGER.warn("Job does not exist", e); } catch (UnknownTaskException e) { LOGGER.warn("Task does not exist", e); } catch (PermissionException e) { LOGGER.warn("Not allowed to access task", e); } return null; } private InetSocketAddress resolveVncTargetFromLogs(List<String> paRemoteConnectionLines, String jobId, String taskId) { for (String paRemoteConnectionLine : paRemoteConnectionLines) { String[] paRemoteConnectionArgs = paRemoteConnectionLine.split(PA_REMOTE_CONNECTION); if (paRemoteConnectionArgs.length == 2) { paRemoteConnectionArgs = paRemoteConnectionArgs[1].split(PA_REMOTE_CONNECTION_SEPARATOR); if (paRemoteConnectionArgs.length == 5) { String jobIdFromRemoteConnectionArgs = paRemoteConnectionArgs[1]; String taskIdFromRemoteConnectionArgs = paRemoteConnectionArgs[2]; String type = paRemoteConnectionArgs[3]; String args = paRemoteConnectionArgs[4]; if (jobIdFromRemoteConnectionArgs.equals(jobId) && taskIdFromRemoteConnectionArgs.equals(taskId) && VNC_PROTOCOL.equals(type)) { String[] targetHostAndPort = args.split(PA_REMOTE_CONNECTION_HOST_PORT_SEPARATOR); String vncHost = targetHostAndPort[0].trim(); String vncPort = targetHostAndPort[1].trim(); LOGGER.debug("Proxying to " + vncHost + ":" + vncPort); return new InetSocketAddress(vncHost, Integer.parseInt(vncPort)); } else { LOGGER.debug("Protocol or task unknown in PA_REMOTE_CONNECTION string (" + paRemoteConnectionLine + ")"); } } } LOGGER.debug("Missing arguments in PA_REMOTE_CONNECTION string, " + "(" + paRemoteConnectionLine + ")" + "format should be PA_REMOTE_CONNECTION;$taskId;vnc;host:port"); } LOGGER.warn("Could not find the PA_REMOTE_CONNECTION string"); return null; } private static String retrieveTaskId(String taskName, JobState jobState) { for (TaskState taskState : jobState.getHMTasks().values()) { if (taskState.getName().equals(taskName)) { return taskState.getId().value(); } } return null; } private List<String> retrievePaRemoteConnectionLines(Session session, String jobId, TaskResult taskResult) { List<String> paRemoteConnectionLines = Collections.emptyList(); String liveLogs = getJobLiveLogs(session, jobId); if (liveLogs != null) { paRemoteConnectionLines = retrievePaRemoteConnectionLines(liveLogs); } if (paRemoteConnectionLines.isEmpty()) { if (taskResult != null && taskResult.getOutput() != null) { paRemoteConnectionLines = retrievePaRemoteConnectionLines(taskResult.getOutput().getAllLogs(false)); } } return paRemoteConnectionLines; } private String getJobLiveLogs(Session session, String jobId) { try { return session.getJobsOutputController().getAllLogs(jobId); } catch (Exception e) { LOGGER.warn("Could not retrieve live logs", e); return null; } } private List<String> retrievePaRemoteConnectionLines(String logs) { String[] strings = lineByLine(logs); List<String> lines = new ArrayList<>(strings.length); for (String line : strings) { if (line.contains(PA_REMOTE_CONNECTION)) { lines.add(line); } } return lines; } private static Map<String, String> getQueryMap(String query) { String[] pathAndParams = query.split("\\?"); String paramString = pathAndParams[0]; if (pathAndParams.length > 1) { paramString = pathAndParams[1]; } String[] params = paramString.split("&"); Map<String, String> map = new HashMap<>(); for (String param : params) { String name = param.split("=")[0]; String value = param.split("=")[1]; map.put(name, value); } return map; } private static String[] lineByLine(String lines) { return lines.split(PLATFORM_INDEPENDENT_LINE_BREAK); } }