package org.ovirt.engine.core.services; import java.io.BufferedReader; import java.io.IOException; import java.net.HttpURLConnection; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.type.TypeFactory; import org.ovirt.engine.core.bll.context.EngineContext; import org.ovirt.engine.core.bll.interfaces.BackendInternal; import org.ovirt.engine.core.common.action.LoginOnBehalfParameters; import org.ovirt.engine.core.common.action.VdcActionParametersBase; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.common.action.VdcReturnValueBase; import org.ovirt.engine.core.common.businessentities.ActionGroup; import org.ovirt.engine.core.common.businessentities.UserProfile; import org.ovirt.engine.core.common.businessentities.VDS; import org.ovirt.engine.core.common.businessentities.VM; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.queries.GetEntitiesWithPermittedActionParameters; import org.ovirt.engine.core.common.queries.IdQueryParameters; import org.ovirt.engine.core.common.queries.VdcQueryParametersBase; import org.ovirt.engine.core.common.queries.VdcQueryReturnValue; import org.ovirt.engine.core.common.queries.VdcQueryType; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.utils.crypt.EngineEncryptionUtils; import org.ovirt.engine.core.uutils.crypto.ticket.TicketDecoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class VMConsoleProxyServlet extends HttpServlet { @Inject private BackendInternal backend; private static final String VM_CONSOLE_PROXY_EKU = "1.3.6.1.4.1.2312.13.1.2.1.1"; private static final Logger log = LoggerFactory.getLogger(VMConsoleProxyServlet.class); // TODO: implmement key filtering based on input parameters private List<Map<String, String>> availablePublicKeys(String keyFingerPrint, String keyType, String keyContent) { List<Map<String, String>> jsonUsers = new ArrayList<>(); VdcQueryParametersBase userProfileParams = new VdcQueryParametersBase(); VdcQueryReturnValue v = backend.runInternalQuery(VdcQueryType.GetAllUserProfiles, userProfileParams); if (v != null) { List<UserProfile> profiles = v.getReturnValue(); for (UserProfile profile : profiles) { if (StringUtils.isNotEmpty(profile.getSshPublicKey())) { for (String publicKey : StringUtils.split(profile.getSshPublicKey(), "\n")) { if (StringUtils.isNotEmpty(publicKey)) { Map<String, String> jsonUser = new HashMap<>(); jsonUser.put("entityid", profile.getUserId().toString()); jsonUser.put("entity", profile.getLoginName()); jsonUser.put("key", publicKey.trim()); jsonUsers.add(jsonUser); } } } } } return jsonUsers; } private List<Map<String, String>> availableConsoles(String userIdAsString) { List<Map<String, String>> jsonVms = new ArrayList<>(); Guid userGuid = null; try { if (StringUtils.isNotEmpty(userIdAsString)) { userGuid = Guid.createGuidFromString(userIdAsString); } } catch (IllegalArgumentException e) { log.debug("Could not read User GUID"); } if (userGuid != null) { VdcReturnValueBase loginResult = backend.runInternalAction(VdcActionType.LoginOnBehalf, new LoginOnBehalfParameters(userGuid)); if (!loginResult.getSucceeded()) { throw new RuntimeException("Unable to create session using LoginOnBehalf"); } String engineSessionId = loginResult.getActionReturnValue(); try { VdcQueryReturnValue retVms = backend.runInternalQuery(VdcQueryType.GetAllVmsForUserAndActionGroup, new GetEntitiesWithPermittedActionParameters(ActionGroup.CONNECT_TO_SERIAL_CONSOLE), new EngineContext().withSessionId(engineSessionId)); if (retVms != null) { List<VM> vmsList = retVms.getReturnValue(); for (VM vm : vmsList) { Map<String, String> jsonVm = new HashMap<>(); if (vm.getRunOnVds() != null) { // TODO: avoid one query per loop. Bulk query? VdcQueryReturnValue retValue = backend.runInternalQuery(VdcQueryType.GetVdsByVdsId, new IdQueryParameters(vm.getRunOnVds())); if (retValue != null && retValue.getReturnValue() != null) { VDS vds = retValue.getReturnValue(); jsonVm.put("vmid", vm.getId().toString()); jsonVm.put("vmname", vm.getName()); jsonVm.put("host", vds.getHostName()); /* there is only one serial console, no need and no way to distinguish them */ jsonVm.put("console", "default"); jsonVms.add(jsonVm); } } } } } finally { backend.runInternalAction(VdcActionType.LogoutSession, new VdcActionParametersBase(engineSessionId)); } } return jsonVms; } // Caller must ensure to close the #body to avoid resource leaking. // Recommended way is to use this helper inside a try-with-resources block. private String readBody(BufferedReader body) throws IOException { StringBuilder buffer = new StringBuilder(); int r; while ((r = body.read()) != -1) { buffer.append((char) r); } return buffer.toString(); } private String validateTicket(String ticket) throws GeneralSecurityException, IOException { TicketDecoder ticketDecoder = new TicketDecoder(EngineEncryptionUtils.getTrustStore(), VM_CONSOLE_PROXY_EKU, Config.<Integer> getValue(ConfigValues.VMConsoleTicketTolerance)); return ticketDecoder.decode(ticket); } private Map<String, Object> buildResult(String content_type, String content_id, Object content) { Map<String, Object> result = new HashMap<>(); result.put("version", "1"); result.put("content", content_type); result.put(content_id, content); return result; } private Map<String, Object> produceContentFromParameters(Map<String, String> parameters) { String command = parameters.get("command"); String version = parameters.get("version"); Map<String, Object> result = null; if ("1".equals(version)) { if ("available_consoles".equals(command)) { String userId = parameters.get("user_id"); result = buildResult("console_list", "consoles", availableConsoles(userId)); } else if ("public_keys".equals(command)) { String keyFingerPrint = parameters.get("key_fp"); String keyType = parameters.get("key_type"); String keyContent = parameters.get("key_content"); result = buildResult("key_list", "keys", availablePublicKeys( (keyFingerPrint != null) ? keyFingerPrint : "", (keyType != null) ? keyType : "", (keyContent != null) ? keyContent : "")); } else { log.error("Unknown command: ", command); } } else { log.error("Unsupported version: ", version); } return result; } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { String stringParameters = validateTicket(readBody(request.getReader())); ObjectMapper mapper = new ObjectMapper(); Map<String, String> parameters = mapper.readValue( stringParameters, TypeFactory.defaultInstance().constructMapType(HashMap.class, String.class, String.class) ); Map<String, Object> result = produceContentFromParameters(parameters); if (result != null) { response.setContentType("application/json"); mapper.writeValue(response.getOutputStream(), result); } else { response.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR); } } catch (GeneralSecurityException e) { log.error("Error validating ticket: ", e); response.setStatus(HttpURLConnection.HTTP_FORBIDDEN); } catch (IOException e) { log.error("Error decoding ticket: ", e); response.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR); } catch (Exception e) { log.error("Error processing request: ", e); response.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR); } } }