mirror of https://github.com/apache/cloudstack.git
console: Console access enhancements (#6577)
This PR creates a new API createConsoleAccess to create VM console URL allowing it to connect using other UI implementations. To avoid reply attacks, the console access is enhanced to use a one time token per session New configuration added: consoleproxy.extra.security.validation.enabled: Enable/disable extra security validation for console proxy using a token Documentation PR: apache/cloudstack-documentation#284
This commit is contained in:
parent
7be7ef66fb
commit
b2fbe7bb12
|
|
@ -32,6 +32,7 @@ import java.util.Properties;
|
|||
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
import com.cloud.agent.api.proxy.AllowConsoleAccessCommand;
|
||||
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
|
@ -105,12 +106,28 @@ public class ConsoleProxyResource extends ServerResourceBase implements ServerRe
|
|||
} else if (cmd instanceof CheckHealthCommand) {
|
||||
return new CheckHealthAnswer((CheckHealthCommand)cmd, true);
|
||||
} else if (cmd instanceof StartConsoleProxyAgentHttpHandlerCommand) {
|
||||
return execute((StartConsoleProxyAgentHttpHandlerCommand)cmd);
|
||||
return execute((StartConsoleProxyAgentHttpHandlerCommand) cmd);
|
||||
} else if (cmd instanceof AllowConsoleAccessCommand) {
|
||||
return execute((AllowConsoleAccessCommand) cmd);
|
||||
} else {
|
||||
return Answer.createUnsupportedCommandAnswer(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
private Answer execute(AllowConsoleAccessCommand cmd) {
|
||||
String sessionUuid = cmd.getSessionUuid();
|
||||
try {
|
||||
Class<?> consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy");
|
||||
Method methodSetup = consoleProxyClazz.getMethod("addAllowedSession", String.class);
|
||||
methodSetup.invoke(null, sessionUuid);
|
||||
return new Answer(cmd);
|
||||
} catch (SecurityException | NoSuchMethodException | ClassNotFoundException | InvocationTargetException | IllegalAccessException e) {
|
||||
String errorMsg = "Unable to add allowed session due to: " + e.getMessage();
|
||||
s_logger.error(errorMsg, e);
|
||||
return new Answer(cmd, false, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private Answer execute(StartConsoleProxyAgentHttpHandlerCommand cmd) {
|
||||
s_logger.info("Invoke launchConsoleProxy() in responding to StartConsoleProxyAgentHttpHandlerCommand");
|
||||
launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword(), cmd.getEncryptorPassword(), cmd.isSourceIpCheckEnabled());
|
||||
|
|
@ -382,9 +399,10 @@ public class ConsoleProxyResource extends ServerResourceBase implements ServerRe
|
|||
}
|
||||
}
|
||||
|
||||
public String authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket, Boolean isReauthentication) {
|
||||
public String authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket,
|
||||
Boolean isReauthentication, String sessionToken) {
|
||||
|
||||
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(host, port, vmId, sid, ticket);
|
||||
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(host, port, vmId, sid, ticket, sessionToken);
|
||||
cmd.setReauthenticating(isReauthentication);
|
||||
|
||||
ConsoleProxyAuthenticationResult result = new ConsoleProxyAuthenticationResult();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
// 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.cloudstack.api.command.user.consoleproxy;
|
||||
|
||||
public class ConsoleEndpoint {
|
||||
|
||||
private boolean result;
|
||||
private String details;
|
||||
private String url;
|
||||
private String websocketToken;
|
||||
private String websocketPath;
|
||||
private String websocketHost;
|
||||
private String websocketPort;
|
||||
private String websocketExtra;
|
||||
|
||||
public ConsoleEndpoint(boolean result, String url) {
|
||||
this.result = result;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public ConsoleEndpoint(boolean result, String url, String details) {
|
||||
this(result, url);
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public boolean isResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(boolean result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(String details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public String getWebsocketToken() {
|
||||
return websocketToken;
|
||||
}
|
||||
|
||||
public void setWebsocketToken(String websocketToken) {
|
||||
this.websocketToken = websocketToken;
|
||||
}
|
||||
|
||||
public String getWebsocketPath() {
|
||||
return websocketPath;
|
||||
}
|
||||
|
||||
public void setWebsocketPath(String websocketPath) {
|
||||
this.websocketPath = websocketPath;
|
||||
}
|
||||
|
||||
public String getWebsocketHost() {
|
||||
return websocketHost;
|
||||
}
|
||||
|
||||
public void setWebsocketHost(String websocketHost) {
|
||||
this.websocketHost = websocketHost;
|
||||
}
|
||||
|
||||
public String getWebsocketPort() {
|
||||
return websocketPort;
|
||||
}
|
||||
|
||||
public void setWebsocketPort(String websocketPort) {
|
||||
this.websocketPort = websocketPort;
|
||||
}
|
||||
|
||||
public String getWebsocketExtra() {
|
||||
return websocketExtra;
|
||||
}
|
||||
|
||||
public void setWebsocketExtra(String websocketExtra) {
|
||||
this.websocketExtra = websocketExtra;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// 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.cloudstack.api.command.user.consoleproxy;
|
||||
|
||||
import com.cloud.exception.ConcurrentOperationException;
|
||||
import com.cloud.exception.InsufficientCapacityException;
|
||||
import com.cloud.exception.NetworkRuleConflictException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.exception.ResourceUnavailableException;
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.response.ConsoleEndpointWebsocketResponse;
|
||||
import org.apache.cloudstack.api.response.CreateConsoleEndpointResponse;
|
||||
import org.apache.cloudstack.api.response.UserVmResponse;
|
||||
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = CreateConsoleEndpointCmd.APINAME, description = "Create a console endpoint to connect to a VM console",
|
||||
responseObject = CreateConsoleEndpointResponse.class, since = "4.18.0",
|
||||
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
|
||||
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
|
||||
public class CreateConsoleEndpointCmd extends BaseCmd {
|
||||
|
||||
public static final String APINAME = "createConsoleEndpoint";
|
||||
public static final Logger s_logger = Logger.getLogger(CreateConsoleEndpointCmd.class.getName());
|
||||
|
||||
@Inject
|
||||
private ConsoleAccessManager consoleManager;
|
||||
|
||||
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
|
||||
type = CommandType.UUID,
|
||||
entityType = UserVmResponse.class,
|
||||
required = true,
|
||||
description = "ID of the VM")
|
||||
private Long vmId;
|
||||
|
||||
@Parameter(name = ApiConstants.TOKEN,
|
||||
type = CommandType.STRING,
|
||||
required = false,
|
||||
description = "(optional) extra security token, valid when the extra validation is enabled")
|
||||
private String extraSecurityToken;
|
||||
|
||||
@Override
|
||||
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
|
||||
String clientAddress = getClientAddress();
|
||||
ConsoleEndpoint endpoint = consoleManager.generateConsoleEndpoint(vmId, extraSecurityToken, clientAddress);
|
||||
if (endpoint != null) {
|
||||
CreateConsoleEndpointResponse response = createResponse(endpoint);
|
||||
setResponseObject(response);
|
||||
} else {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Unable to generate console endpoint for vm " + vmId);
|
||||
}
|
||||
}
|
||||
|
||||
private CreateConsoleEndpointResponse createResponse(ConsoleEndpoint endpoint) {
|
||||
CreateConsoleEndpointResponse response = new CreateConsoleEndpointResponse();
|
||||
response.setResult(endpoint.isResult());
|
||||
response.setDetails(endpoint.getDetails());
|
||||
response.setUrl(endpoint.getUrl());
|
||||
response.setWebsocketResponse(createWebsocketResponse(endpoint));
|
||||
response.setResponseName(getCommandName());
|
||||
response.setObjectName("consoleendpoint");
|
||||
return response;
|
||||
}
|
||||
|
||||
private ConsoleEndpointWebsocketResponse createWebsocketResponse(ConsoleEndpoint endpoint) {
|
||||
ConsoleEndpointWebsocketResponse wsResponse = new ConsoleEndpointWebsocketResponse();
|
||||
wsResponse.setHost(endpoint.getWebsocketHost());
|
||||
wsResponse.setPort(endpoint.getWebsocketPort());
|
||||
wsResponse.setPath(endpoint.getWebsocketPath());
|
||||
wsResponse.setToken(endpoint.getWebsocketToken());
|
||||
wsResponse.setExtra(endpoint.getWebsocketExtra());
|
||||
wsResponse.setObjectName("websocket");
|
||||
return wsResponse;
|
||||
}
|
||||
|
||||
private String getParameterBase(String paramKey) {
|
||||
Map<String, String> params = getFullUrlParams();
|
||||
return MapUtils.isNotEmpty(params) ? params.get(paramKey) : null;
|
||||
}
|
||||
|
||||
private String getClientAddress() {
|
||||
return getParameterBase(ConsoleAccessUtils.CLIENT_INET_ADDRESS_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return CallContext.current().getCallingAccount().getId();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// 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.cloudstack.api.response;
|
||||
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
|
||||
public class ConsoleEndpointWebsocketResponse extends BaseResponse {
|
||||
|
||||
public ConsoleEndpointWebsocketResponse() {
|
||||
}
|
||||
|
||||
@SerializedName(ApiConstants.TOKEN)
|
||||
@Param(description = "the console websocket token")
|
||||
private String token;
|
||||
|
||||
@SerializedName("host")
|
||||
@Param(description = "the console websocket host")
|
||||
private String host;
|
||||
|
||||
@SerializedName(ApiConstants.PORT)
|
||||
@Param(description = "the console websocket port")
|
||||
private String port;
|
||||
|
||||
@SerializedName(ApiConstants.PATH)
|
||||
@Param(description = "the console websocket path")
|
||||
private String path;
|
||||
|
||||
@SerializedName("extra")
|
||||
@Param(description = "the console websocket extra field for validation (if enabled)")
|
||||
private String extra;
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public String getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(String port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getExtra() {
|
||||
return extra;
|
||||
}
|
||||
|
||||
public void setExtra(String extra) {
|
||||
this.extra = extra;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// 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.cloudstack.api.response;
|
||||
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
|
||||
public class CreateConsoleEndpointResponse extends BaseResponse {
|
||||
|
||||
public CreateConsoleEndpointResponse() {
|
||||
}
|
||||
|
||||
@SerializedName(ApiConstants.RESULT)
|
||||
@Param(description = "true if the console endpoint is generated properly")
|
||||
private Boolean result;
|
||||
|
||||
@SerializedName(ApiConstants.DETAILS)
|
||||
@Param(description = "details in case of an error")
|
||||
private String details;
|
||||
|
||||
@SerializedName(ApiConstants.URL)
|
||||
@Param(description = "the console url")
|
||||
private String url;
|
||||
|
||||
@SerializedName("websocket")
|
||||
@Param(description = "the console websocket options")
|
||||
private ConsoleEndpointWebsocketResponse websocketResponse;
|
||||
|
||||
public Boolean getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(Boolean result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(String details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public ConsoleEndpointWebsocketResponse getWebsocketResponse() {
|
||||
return websocketResponse;
|
||||
}
|
||||
|
||||
public void setWebsocketResponse(ConsoleEndpointWebsocketResponse websocketResponse) {
|
||||
this.websocketResponse = websocketResponse;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// 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.cloudstack.consoleproxy;
|
||||
|
||||
import com.cloud.utils.component.Manager;
|
||||
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.config.Configurable;
|
||||
|
||||
public interface ConsoleAccessManager extends Manager, Configurable {
|
||||
|
||||
ConfigKey<Boolean> ConsoleProxyExtraSecurityValidationEnabled = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Boolean.class,
|
||||
"consoleproxy.extra.security.validation.enabled", "false",
|
||||
"Enable/disable extra security validation for console proxy using an extra token.", true);
|
||||
|
||||
ConsoleEndpoint generateConsoleEndpoint(Long vmId, String extraSecurityToken, String clientAddress);
|
||||
|
||||
boolean isSessionAllowed(String sessionUuid);
|
||||
|
||||
void removeSessions(String[] sessionUuids);
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
|
|||
private String _vmId;
|
||||
private String _sid;
|
||||
private String _ticket;
|
||||
private String sessionUuid;
|
||||
|
||||
private boolean _isReauthenticating;
|
||||
|
||||
|
|
@ -33,12 +34,14 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
|
|||
_isReauthenticating = false;
|
||||
}
|
||||
|
||||
public ConsoleAccessAuthenticationCommand(String host, String port, String vmId, String sid, String ticket) {
|
||||
public ConsoleAccessAuthenticationCommand(String host, String port, String vmId, String sid, String ticket,
|
||||
String sessiontkn) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
_vmId = vmId;
|
||||
_sid = sid;
|
||||
_ticket = ticket;
|
||||
sessionUuid = sessiontkn;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
|
|
@ -68,4 +71,12 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
|
|||
public void setReauthenticating(boolean value) {
|
||||
_isReauthenticating = value;
|
||||
}
|
||||
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
public void setSessionUuid(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// 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 com.cloud.agent.api.proxy;
|
||||
|
||||
public class AllowConsoleAccessCommand extends ProxyCommand {
|
||||
|
||||
private String sessionUuid;
|
||||
|
||||
public AllowConsoleAccessCommand() {
|
||||
}
|
||||
|
||||
public AllowConsoleAccessCommand(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean executeInSequence() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
public void setSessionUuid(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,16 @@ public class ConsoleProxyConnectionInfo {
|
|||
public String tag;
|
||||
public long createTime;
|
||||
public long lastUsedTime;
|
||||
protected String sessionUuid;
|
||||
|
||||
public ConsoleProxyConnectionInfo() {
|
||||
}
|
||||
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
public void setSessionUuid(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public class ConsoleProxyInfo {
|
|||
private int proxyPort;
|
||||
private String proxyImageUrl;
|
||||
private int proxyUrlPort = 8000;
|
||||
private String proxyName;
|
||||
|
||||
public ConsoleProxyInfo(int proxyUrlPort) {
|
||||
this.proxyUrlPort = proxyUrlPort;
|
||||
|
|
@ -100,4 +101,12 @@ public class ConsoleProxyInfo {
|
|||
public void setSslEnabled(boolean sslEnabled) {
|
||||
this.sslEnabled = sslEnabled;
|
||||
}
|
||||
|
||||
public String getProxyName() {
|
||||
return proxyName;
|
||||
}
|
||||
|
||||
public void setProxyName(String proxyName) {
|
||||
this.proxyName = proxyName;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ package com.cloud.info;
|
|||
|
||||
public class ConsoleProxyStatus {
|
||||
private ConsoleProxyConnectionInfo[] connections;
|
||||
private String[] removedSessions;
|
||||
|
||||
public ConsoleProxyStatus() {
|
||||
}
|
||||
|
|
@ -28,4 +29,8 @@ public class ConsoleProxyStatus {
|
|||
public ConsoleProxyConnectionInfo[] getConnections() {
|
||||
return connections;
|
||||
}
|
||||
|
||||
public String[] getRemovedSessions() {
|
||||
return removedSessions;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,14 +41,11 @@ public interface CapacityManager {
|
|||
static final String StorageOverprovisioningFactorCK = "storage.overprovisioning.factor";
|
||||
static final String StorageAllocatedCapacityDisableThresholdCK = "pool.storage.allocated.capacity.disablethreshold";
|
||||
|
||||
static final String CATEGORY_ADVANCED = "Advanced";
|
||||
static final String CATEGORY_ALERT = "Alert";
|
||||
|
||||
static final ConfigKey<Float> CpuOverprovisioningFactor =
|
||||
new ConfigKey<>(
|
||||
Float.class,
|
||||
CpuOverprovisioningFactorCK,
|
||||
CATEGORY_ADVANCED,
|
||||
ConfigKey.CATEGORY_ADVANCED,
|
||||
"1.0",
|
||||
"Used for CPU overprovisioning calculation; available CPU will be (actualCpuCapacity * cpu.overprovisioning.factor)",
|
||||
true,
|
||||
|
|
@ -58,7 +55,7 @@ public interface CapacityManager {
|
|||
new ConfigKey<>(
|
||||
Float.class,
|
||||
MemOverprovisioningFactorCK,
|
||||
CATEGORY_ADVANCED,
|
||||
ConfigKey.CATEGORY_ADVANCED,
|
||||
"1.0",
|
||||
"Used for memory overprovisioning calculation",
|
||||
true,
|
||||
|
|
@ -66,7 +63,7 @@ public interface CapacityManager {
|
|||
null);
|
||||
static final ConfigKey<Double> StorageCapacityDisableThreshold =
|
||||
new ConfigKey<>(
|
||||
CATEGORY_ALERT,
|
||||
ConfigKey.CATEGORY_ALERT,
|
||||
Double.class,
|
||||
StorageCapacityDisableThresholdCK,
|
||||
"0.85",
|
||||
|
|
@ -84,7 +81,7 @@ public interface CapacityManager {
|
|||
ConfigKey.Scope.StoragePool);
|
||||
static final ConfigKey<Double> StorageAllocatedCapacityDisableThreshold =
|
||||
new ConfigKey<>(
|
||||
CATEGORY_ALERT,
|
||||
ConfigKey.CATEGORY_ALERT,
|
||||
Double.class,
|
||||
StorageAllocatedCapacityDisableThresholdCK,
|
||||
"0.85",
|
||||
|
|
@ -95,7 +92,7 @@ public interface CapacityManager {
|
|||
new ConfigKey<>(
|
||||
Boolean.class,
|
||||
"cluster.storage.operations.exclude",
|
||||
CATEGORY_ADVANCED,
|
||||
ConfigKey.CATEGORY_ADVANCED,
|
||||
"false",
|
||||
"Exclude cluster from storage operations",
|
||||
true,
|
||||
|
|
@ -105,7 +102,7 @@ public interface CapacityManager {
|
|||
new ConfigKey<>(
|
||||
String.class,
|
||||
"secstorage.nfs.version",
|
||||
CATEGORY_ADVANCED,
|
||||
ConfigKey.CATEGORY_ADVANCED,
|
||||
null,
|
||||
"Enforces specific NFS version when mounting Secondary Storage. If NULL default selection is performed",
|
||||
true,
|
||||
|
|
@ -114,7 +111,7 @@ public interface CapacityManager {
|
|||
|
||||
static final ConfigKey<Float> SecondaryStorageCapacityThreshold =
|
||||
new ConfigKey<>(
|
||||
CATEGORY_ADVANCED,
|
||||
ConfigKey.CATEGORY_ADVANCED,
|
||||
Float.class,
|
||||
"secondary.storage.capacity.threshold",
|
||||
"0.90",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ import com.cloud.utils.exception.CloudRuntimeException;
|
|||
*/
|
||||
public class ConfigKey<T> {
|
||||
|
||||
public static final String CATEGORY_ADVANCED = "Advanced";
|
||||
public static final String CATEGORY_ALERT = "Alert";
|
||||
|
||||
public static enum Scope {
|
||||
Global, Zone, Cluster, StoragePool, Account, ManagementServer, ImageStore, Domain
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ import org.apache.cloudstack.api.ServerApiException;
|
|||
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpointCmd;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.managed.context.ManagedContext;
|
||||
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
|
@ -324,6 +326,7 @@ public class ApiServlet extends HttpServlet {
|
|||
// Add the HTTP method (GET/POST/PUT/DELETE) as well into the params map.
|
||||
params.put("httpmethod", new String[]{req.getMethod()});
|
||||
setProjectContext(params);
|
||||
setClientAddressForConsoleEndpointAccess(command, params, req);
|
||||
final String response = apiServer.handleRequest(params, responseType, auditTrailSb);
|
||||
HttpUtils.writeHttpResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType, ApiServer.JSONcontentType.value());
|
||||
} else {
|
||||
|
|
@ -356,6 +359,15 @@ public class ApiServlet extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
protected void setClientAddressForConsoleEndpointAccess(String command, Map<String, Object[]> params, HttpServletRequest req) throws UnknownHostException {
|
||||
if (org.apache.commons.lang3.StringUtils.isNotBlank(command) &&
|
||||
command.equalsIgnoreCase(CreateConsoleEndpointCmd.APINAME)) {
|
||||
InetAddress addr = getClientAddress(req);
|
||||
String clientAddress = addr != null ? addr.getHostAddress() : null;
|
||||
params.put(ConsoleAccessUtils.CLIENT_INET_ADDRESS_KEY, new String[] {clientAddress});
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String saveLogString(String stringToLog) {
|
||||
return stringToLog == null ? null : stringToLog.replace(LOG_REPLACEMENTS, REPLACEMENT);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.util.Map;
|
|||
import javax.inject.Inject;
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
|
|
@ -67,6 +68,8 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
|
|||
protected ConsoleProxyDao _cpDao;
|
||||
@Inject
|
||||
protected KeystoreManager _ksMgr;
|
||||
@Inject
|
||||
protected ConsoleAccessManager consoleAccessManager;
|
||||
|
||||
@Inject
|
||||
ConfigurationDao _configDao;
|
||||
|
|
@ -77,8 +80,9 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
|
|||
|
||||
public class AgentBasedAgentHook extends AgentHookBase {
|
||||
|
||||
public AgentBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) {
|
||||
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr);
|
||||
public AgentBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr,
|
||||
AgentManager agentMgr, KeysManager keysMgr, ConsoleAccessManager consoleAccessManager) {
|
||||
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr, consoleAccessManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -121,7 +125,8 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
|
|||
|
||||
_consoleProxyUrlDomain = configs.get("consoleproxy.url.domain");
|
||||
|
||||
_listener = new ConsoleProxyListener(new AgentBasedAgentHook(_instanceDao, _hostDao, _configDao, _ksMgr, _agentMgr, _keysMgr));
|
||||
_listener = new ConsoleProxyListener(new AgentBasedAgentHook(_instanceDao, _hostDao, _configDao, _ksMgr,
|
||||
_agentMgr, _keysMgr, consoleAccessManager));
|
||||
_agentMgr.registerForHostEvents(_listener, true, true, false);
|
||||
|
||||
if (s_logger.isInfoEnabled()) {
|
||||
|
|
@ -182,6 +187,11 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVncPort() {
|
||||
return _consoleProxyPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rebootProxy(long proxyVmId) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.SecureRandom;
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
|
||||
import org.apache.cloudstack.consoleproxy.ConsoleAccessManagerImpl;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.framework.security.keys.KeysManager;
|
||||
import org.apache.cloudstack.framework.security.keystore.KeystoreManager;
|
||||
|
|
@ -68,14 +70,17 @@ public abstract class AgentHookBase implements AgentHook {
|
|||
AgentManager _agentMgr;
|
||||
KeystoreManager _ksMgr;
|
||||
KeysManager _keysMgr;
|
||||
ConsoleAccessManager consoleAccessManager;
|
||||
|
||||
public AgentHookBase(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) {
|
||||
protected AgentHookBase(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr,
|
||||
AgentManager agentMgr, KeysManager keysMgr, ConsoleAccessManager consoleAccessMgr) {
|
||||
_instanceDao = instanceDao;
|
||||
_hostDao = hostDao;
|
||||
_agentMgr = agentMgr;
|
||||
_configDao = cfgDao;
|
||||
_ksMgr = ksMgr;
|
||||
_keysMgr = keysMgr;
|
||||
consoleAccessManager = consoleAccessMgr;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -83,6 +88,8 @@ public abstract class AgentHookBase implements AgentHook {
|
|||
Long vmId = null;
|
||||
|
||||
String ticketInUrl = cmd.getTicket();
|
||||
String sessionUuid = cmd.getSessionUuid();
|
||||
|
||||
if (ticketInUrl == null) {
|
||||
s_logger.error("Access ticket could not be found, you could be running an old version of console proxy. vmId: " + cmd.getVmId());
|
||||
return new ConsoleAccessAuthenticationAnswer(cmd, false);
|
||||
|
|
@ -93,16 +100,20 @@ public abstract class AgentHookBase implements AgentHook {
|
|||
}
|
||||
|
||||
if (!cmd.isReauthenticating()) {
|
||||
String ticket = ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId());
|
||||
String ticket = ConsoleAccessManagerImpl.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), sessionUuid);
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("Console authentication. Ticket in 1 minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticket);
|
||||
}
|
||||
|
||||
if (!consoleAccessManager.isSessionAllowed(sessionUuid)) {
|
||||
s_logger.error("Invalid session, only one session allowed per token");
|
||||
return new ConsoleAccessAuthenticationAnswer(cmd, false);
|
||||
}
|
||||
|
||||
if (!ticket.equals(ticketInUrl)) {
|
||||
Date now = new Date();
|
||||
// considering of minute round-up
|
||||
String minuteEarlyTicket =
|
||||
ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000));
|
||||
String minuteEarlyTicket = ConsoleAccessManagerImpl.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000), sessionUuid);
|
||||
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("Console authentication. Ticket in 2-minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " +
|
||||
|
|
|
|||
|
|
@ -23,39 +23,39 @@ import org.apache.cloudstack.framework.config.ConfigKey;
|
|||
|
||||
public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
|
||||
|
||||
public static final int DEFAULT_PROXY_CAPACITY = 50;
|
||||
public static final int DEFAULT_STANDBY_CAPACITY = 10;
|
||||
public static final int DEFAULT_PROXY_VM_RAMSIZE = 1024; // 1G
|
||||
public static final int DEFAULT_PROXY_VM_CPUMHZ = 500; // 500 MHz
|
||||
int DEFAULT_PROXY_CAPACITY = 50;
|
||||
int DEFAULT_STANDBY_CAPACITY = 10;
|
||||
int DEFAULT_PROXY_VM_RAMSIZE = 1024; // 1G
|
||||
int DEFAULT_PROXY_VM_CPUMHZ = 500; // 500 MHz
|
||||
|
||||
public static final int DEFAULT_PROXY_CMD_PORT = 8001;
|
||||
public static final int DEFAULT_PROXY_VNC_PORT = 0;
|
||||
public static final int DEFAULT_PROXY_URL_PORT = 80;
|
||||
public static final int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes
|
||||
int DEFAULT_PROXY_CMD_PORT = 8001;
|
||||
int DEFAULT_PROXY_VNC_PORT = 0;
|
||||
int DEFAULT_PROXY_URL_PORT = 80;
|
||||
int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes
|
||||
|
||||
public static final int DEFAULT_NOVNC_PORT = 8080;
|
||||
String ALERT_SUBJECT = "proxy-alert";
|
||||
String CERTIFICATE_NAME = "CPVMCertificate";
|
||||
|
||||
public static final String ALERT_SUBJECT = "proxy-alert";
|
||||
public static final String CERTIFICATE_NAME = "CPVMCertificate";
|
||||
|
||||
public static final ConfigKey<Boolean> NoVncConsoleDefault = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.default", "true",
|
||||
ConfigKey<Boolean> NoVncConsoleDefault = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Boolean.class, "novnc.console.default", "true",
|
||||
"If true, noVNC console will be default console for virtual machines", true);
|
||||
|
||||
public static final ConfigKey<Boolean> NoVncConsoleSourceIpCheckEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.sourceip.check.enabled", "false",
|
||||
ConfigKey<Boolean> NoVncConsoleSourceIpCheckEnabled = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Boolean.class, "novnc.console.sourceip.check.enabled", "false",
|
||||
"If true, The source IP to access novnc console must be same as the IP in request to management server for console URL. Needs to reconnect CPVM to management server when this changes (via restart CPVM, or management server, or cloud service in CPVM)", false);
|
||||
|
||||
public void setManagementState(ConsoleProxyManagementState state);
|
||||
void setManagementState(ConsoleProxyManagementState state);
|
||||
|
||||
public ConsoleProxyManagementState getManagementState();
|
||||
ConsoleProxyManagementState getManagementState();
|
||||
|
||||
public void resumeLastManagementState();
|
||||
void resumeLastManagementState();
|
||||
|
||||
public ConsoleProxyVO startProxy(long proxyVmId, boolean ignoreRestartSetting);
|
||||
ConsoleProxyVO startProxy(long proxyVmId, boolean ignoreRestartSetting);
|
||||
|
||||
public boolean stopProxy(long proxyVmId);
|
||||
boolean stopProxy(long proxyVmId);
|
||||
|
||||
public boolean rebootProxy(long proxyVmId);
|
||||
boolean rebootProxy(long proxyVmId);
|
||||
|
||||
public boolean destroyProxy(long proxyVmId);
|
||||
boolean destroyProxy(long proxyVmId);
|
||||
|
||||
int getVncPort();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import javax.naming.ConfigurationException;
|
|||
import com.cloud.utils.PasswordGenerator;
|
||||
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
|
||||
import org.apache.cloudstack.ca.CAManager;
|
||||
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
|
||||
import org.apache.cloudstack.framework.ca.Certificate;
|
||||
|
|
@ -263,11 +264,14 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
private KeystoreDao _ksDao;
|
||||
@Inject
|
||||
private KeystoreManager _ksMgr;
|
||||
@Inject
|
||||
private ConsoleAccessManager consoleAccessManager;
|
||||
|
||||
public class VmBasedAgentHook extends AgentHookBase {
|
||||
|
||||
public VmBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) {
|
||||
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr);
|
||||
public VmBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr,
|
||||
AgentManager agentMgr, KeysManager keysMgr, ConsoleAccessManager consoleAccessManager) {
|
||||
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr, consoleAccessManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -340,11 +344,14 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
s_logger.warn(String.format("SSL is enabled for console proxy [%s] but no server certificate found in database.", proxy.toString()));
|
||||
}
|
||||
|
||||
ConsoleProxyInfo info;
|
||||
if (staticPublicIp == null) {
|
||||
return new ConsoleProxyInfo(proxy.isSslEnabled(), proxy.getPublicIpAddress(), consoleProxyPort, proxy.getPort(), consoleProxyUrlDomain);
|
||||
info = new ConsoleProxyInfo(proxy.isSslEnabled(), proxy.getPublicIpAddress(), consoleProxyPort, proxy.getPort(), consoleProxyUrlDomain);
|
||||
} else {
|
||||
return new ConsoleProxyInfo(proxy.isSslEnabled(), staticPublicIp, consoleProxyPort, staticPort, consoleProxyUrlDomain);
|
||||
info = new ConsoleProxyInfo(proxy.isSslEnabled(), staticPublicIp, consoleProxyPort, staticPort, consoleProxyUrlDomain);
|
||||
}
|
||||
info.setProxyName(proxy.getHostName());
|
||||
return info;
|
||||
}
|
||||
|
||||
public ConsoleProxyVO doAssignProxy(long dataCenterId, long vmId) {
|
||||
|
|
@ -982,7 +989,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
} catch (Exception e) {
|
||||
s_logger.error(String.format("Unable to set console proxy management state to [%s] due to [%s].", state, e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1017,7 +1024,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
if (lastState != state) {
|
||||
configurationDao.update(Config.ConsoleProxyManagementState.key(), Config.ConsoleProxyManagementState.getCategory(), lastState.toString());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
} catch (Exception e) {
|
||||
s_logger.error(String.format("Unable to resume last management state due to [%s].", e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1096,6 +1103,11 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVncPort() {
|
||||
return sslEnabled && _ksDao.findByName(ConsoleProxyManager.CERTIFICATE_NAME) != null ? 8443 : 8080;
|
||||
}
|
||||
|
||||
private String getAllocProxyLockName() {
|
||||
return "consoleproxy.alloc";
|
||||
}
|
||||
|
|
@ -1156,7 +1168,8 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
value = agentMgrConfigs.get("port");
|
||||
managementPort = NumbersUtil.parseInt(value, 8250);
|
||||
|
||||
consoleProxyListener = new ConsoleProxyListener(new VmBasedAgentHook(vmInstanceDao, hostDao, configurationDao, _ksMgr, agentManager, keysManager));
|
||||
consoleProxyListener = new ConsoleProxyListener(new VmBasedAgentHook(vmInstanceDao, hostDao, configurationDao,
|
||||
_ksMgr, agentManager, keysManager, consoleAccessManager));
|
||||
agentManager.registerForHostEvents(consoleProxyListener, true, true, false);
|
||||
|
||||
virtualMachineManager.registerGuru(VirtualMachine.Type.ConsoleProxy, this);
|
||||
|
|
@ -1288,6 +1301,9 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
if (dc.getDns2() != null) {
|
||||
buf.append(" dns2=").append(dc.getDns2());
|
||||
}
|
||||
if (VirtualMachine.Type.ConsoleProxy == profile.getVirtualMachine().getType()) {
|
||||
buf.append(" vncport=").append(getVncPort());
|
||||
}
|
||||
buf.append(" keystore_password=").append(VirtualMachineGuru.getEncodedString(PasswordGenerator.generateRandomPassword(16)));
|
||||
String bootArgs = buf.toString();
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
|
|
@ -1621,7 +1637,9 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
|
|||
if (status.getConnections() != null) {
|
||||
count = status.getConnections().length;
|
||||
}
|
||||
|
||||
if (status.getRemovedSessions() != null) {
|
||||
consoleAccessManager.removeSessions(status.getRemovedSessions());
|
||||
}
|
||||
details = statusInfo.getBytes(Charset.forName("US-ASCII"));
|
||||
} else {
|
||||
s_logger.debug(String.format("Unable to retrieve load info from proxy {\"vmId\": %s}. Invalid load info [%s].", proxyVmId, statusInfo));
|
||||
|
|
|
|||
|
|
@ -52,8 +52,12 @@ public interface ManagementServer extends ManagementService, PluggableService {
|
|||
|
||||
DetailVO findDetail(long hostId, String name);
|
||||
|
||||
Pair<Boolean, String> setConsoleAccessForVm(long vmId, String sessionUuid);
|
||||
|
||||
String getConsoleAccessUrlRoot(long vmId);
|
||||
|
||||
String getConsoleAccessAddress(long vmId);
|
||||
|
||||
GuestOSVO getGuestOs(Long guestOsId);
|
||||
|
||||
GuestOSHypervisorVO getGuestOsHypervisor(Long guestOsHypervisorId);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import com.cloud.agent.api.Answer;
|
|||
import com.cloud.agent.api.Command;
|
||||
import com.cloud.agent.api.PatchSystemVmAnswer;
|
||||
import com.cloud.agent.api.PatchSystemVmCommand;
|
||||
import com.cloud.agent.api.proxy.AllowConsoleAccessCommand;
|
||||
import com.cloud.agent.api.routing.NetworkElementCommand;
|
||||
import com.cloud.agent.manager.Commands;
|
||||
import com.cloud.dc.DomainVlanMapVO;
|
||||
|
|
@ -365,6 +366,7 @@ import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScalePolicyCmd
|
|||
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmGroupCmd;
|
||||
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmProfileCmd;
|
||||
import org.apache.cloudstack.api.command.user.config.ListCapabilitiesCmd;
|
||||
import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpointCmd;
|
||||
import org.apache.cloudstack.api.command.user.event.ArchiveEventsCmd;
|
||||
import org.apache.cloudstack.api.command.user.event.DeleteEventsCmd;
|
||||
import org.apache.cloudstack.api.command.user.event.ListEventTypesCmd;
|
||||
|
|
@ -2847,6 +2849,49 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Boolean, String> setConsoleAccessForVm(long vmId, String sessionUuid) {
|
||||
final VMInstanceVO vm = _vmInstanceDao.findById(vmId);
|
||||
if (vm == null) {
|
||||
return new Pair<>(false, "Cannot find a VM with id = " + vmId);
|
||||
}
|
||||
final ConsoleProxyInfo proxy = getConsoleProxyForVm(vm.getDataCenterId(), vmId);
|
||||
if (proxy == null) {
|
||||
return new Pair<>(false, "Cannot find a console proxy for the VM " + vmId);
|
||||
}
|
||||
AllowConsoleAccessCommand cmd = new AllowConsoleAccessCommand(sessionUuid);
|
||||
HostVO hostVO = _hostDao.findByTypeNameAndZoneId(vm.getDataCenterId(), proxy.getProxyName(), Type.ConsoleProxy);
|
||||
if (hostVO == null) {
|
||||
return new Pair<>(false, "Cannot find a console proxy agent for CPVM with name " + proxy.getProxyName());
|
||||
}
|
||||
Answer answer;
|
||||
try {
|
||||
answer = _agentMgr.send(hostVO.getId(), cmd);
|
||||
} catch (AgentUnavailableException | OperationTimedoutException e) {
|
||||
String errorMsg = "Could not send allow session command to CPVM: " + e.getMessage();
|
||||
s_logger.error(errorMsg, e);
|
||||
return new Pair<>(false, errorMsg);
|
||||
}
|
||||
boolean result = false;
|
||||
String details = "null answer";
|
||||
|
||||
if (answer != null) {
|
||||
result = answer.getResult();
|
||||
details = answer.getDetails();
|
||||
}
|
||||
return new Pair<>(result, details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConsoleAccessAddress(long vmId) {
|
||||
final VMInstanceVO vm = _vmInstanceDao.findById(vmId);
|
||||
if (vm != null) {
|
||||
final ConsoleProxyInfo proxy = getConsoleProxyForVm(vm.getDataCenterId(), vmId);
|
||||
return proxy != null ? proxy.getProxyAddress() : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<String, Integer> getVncPort(final VirtualMachine vm) {
|
||||
if (vm.getHostId() == null) {
|
||||
|
|
@ -3625,6 +3670,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
|
|||
cmdList.add(IssueOutOfBandManagementPowerActionCmd.class);
|
||||
cmdList.add(ChangeOutOfBandManagementPasswordCmd.class);
|
||||
cmdList.add(GetUserKeysCmd.class);
|
||||
cmdList.add(CreateConsoleEndpointCmd.class);
|
||||
return cmdList;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,19 @@ public class ConsoleProxyClientParam {
|
|||
private String sourceIP;
|
||||
private String websocketUrl;
|
||||
|
||||
private String sessionUuid;
|
||||
|
||||
/**
|
||||
* The server-side generated value for extra console endpoint validation
|
||||
*/
|
||||
private String extraSecurityToken;
|
||||
|
||||
/**
|
||||
* The extra parameter received in the console URL, must be compared against the server-side generated value
|
||||
* for extra validation (if has been enabled)
|
||||
*/
|
||||
private String clientProvidedExtraSecurityToken;
|
||||
|
||||
public ConsoleProxyClientParam() {
|
||||
clientHostPort = 0;
|
||||
}
|
||||
|
|
@ -159,4 +172,28 @@ public class ConsoleProxyClientParam {
|
|||
public void setWebsocketUrl(String websocketUrl) {
|
||||
this.websocketUrl = websocketUrl;
|
||||
}
|
||||
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
public String getExtraSecurityToken() {
|
||||
return extraSecurityToken;
|
||||
}
|
||||
|
||||
public void setExtraSecurityToken(String extraSecurityToken) {
|
||||
this.extraSecurityToken = extraSecurityToken;
|
||||
}
|
||||
|
||||
public String getClientProvidedExtraSecurityToken() {
|
||||
return clientProvidedExtraSecurityToken;
|
||||
}
|
||||
|
||||
public void setClientProvidedExtraSecurityToken(String clientProvidedExtraSecurityToken) {
|
||||
this.clientProvidedExtraSecurityToken = clientProvidedExtraSecurityToken;
|
||||
}
|
||||
|
||||
public void setSessionUuid(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@
|
|||
package com.cloud.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
|
@ -37,45 +35,29 @@ import javax.servlet.http.HttpServletRequest;
|
|||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.cloud.agent.AgentManager;
|
||||
import com.cloud.agent.api.Answer;
|
||||
import com.cloud.agent.api.GetVmVncTicketAnswer;
|
||||
import com.cloud.agent.api.GetVmVncTicketCommand;
|
||||
import com.cloud.exception.AgentUnavailableException;
|
||||
import com.cloud.exception.OperationTimedoutException;
|
||||
import org.apache.cloudstack.framework.security.keys.KeysManager;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
|
||||
|
||||
|
||||
import com.cloud.vm.VmDetailConstants;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import com.cloud.api.ApiServlet;
|
||||
import com.cloud.consoleproxy.ConsoleProxyManager;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.hypervisor.Hypervisor;
|
||||
import com.cloud.resource.ResourceState;
|
||||
import com.cloud.server.ManagementServer;
|
||||
import com.cloud.storage.GuestOSVO;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.uservm.UserVm;
|
||||
import com.cloud.utils.ConstantTimeComparator;
|
||||
import com.cloud.utils.Pair;
|
||||
import com.cloud.utils.Ternary;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import com.cloud.utils.db.TransactionLegacy;
|
||||
import com.cloud.vm.UserVmDetailVO;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import com.cloud.vm.VirtualMachineManager;
|
||||
import com.cloud.vm.dao.UserVmDetailsDao;
|
||||
|
||||
/**
|
||||
* Thumbnail access : /console?cmd=thumbnail&vm=xxx&w=xxx&h=xxx
|
||||
|
|
@ -98,11 +80,7 @@ public class ConsoleProxyServlet extends HttpServlet {
|
|||
@Inject
|
||||
EntityManager _entityMgr;
|
||||
@Inject
|
||||
UserVmDetailsDao _userVmDetailsDao;
|
||||
@Inject
|
||||
KeysManager _keysMgr;
|
||||
@Inject
|
||||
AgentManager agentManager;
|
||||
|
||||
static KeysManager s_keysMgr;
|
||||
|
||||
|
|
@ -198,12 +176,10 @@ public class ConsoleProxyServlet extends HttpServlet {
|
|||
|
||||
if (cmd.equalsIgnoreCase("thumbnail")) {
|
||||
handleThumbnailRequest(req, resp, vmId);
|
||||
} else if (cmd.equalsIgnoreCase("access")) {
|
||||
handleAccessRequest(req, resp, vmId);
|
||||
} else {
|
||||
handleAuthRequest(req, resp, vmId);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
} catch (Exception e) {
|
||||
s_logger.error("Unexepected exception in ConsoleProxyServlet", e);
|
||||
sendResponse(resp, "Server Internal Error");
|
||||
}
|
||||
|
|
@ -260,61 +236,6 @@ public class ConsoleProxyServlet extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleAccessRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) {
|
||||
VirtualMachine vm = _vmMgr.findById(vmId);
|
||||
if (vm == null) {
|
||||
s_logger.warn("VM " + vmId + " does not exist, sending blank response for console access request");
|
||||
sendResponse(resp, "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (vm.getHostId() == null) {
|
||||
s_logger.warn("VM " + vmId + " lost host info, sending blank response for console access request");
|
||||
sendResponse(resp, "");
|
||||
return;
|
||||
}
|
||||
|
||||
HostVO host = _ms.getHostBy(vm.getHostId());
|
||||
if (host == null) {
|
||||
s_logger.warn("VM " + vmId + "'s host does not exist, sending blank response for console access request");
|
||||
sendResponse(resp, "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Hypervisor.HypervisorType.LXC.equals(vm.getHypervisorType())){
|
||||
sendResponse(resp, "<html><body><p>Console access is not supported for LXC</p></body></html>");
|
||||
return;
|
||||
}
|
||||
|
||||
String rootUrl = _ms.getConsoleAccessUrlRoot(vmId);
|
||||
if (rootUrl == null) {
|
||||
sendResponse(resp, "<html><body><p>Console access will be ready in a few minutes. Please try it again later.</p></body></html>");
|
||||
return;
|
||||
}
|
||||
|
||||
String vmName = vm.getHostName();
|
||||
if (vm.getType() == VirtualMachine.Type.User) {
|
||||
UserVm userVm = _entityMgr.findById(UserVm.class, vmId);
|
||||
String displayName = userVm.getDisplayName();
|
||||
if (displayName != null && !displayName.isEmpty() && !displayName.equals(vmName)) {
|
||||
vmName += "(" + displayName + ")";
|
||||
}
|
||||
}
|
||||
|
||||
InetAddress remoteAddress = null;
|
||||
try {
|
||||
remoteAddress = ApiServlet.getClientAddress(req);
|
||||
} catch (UnknownHostException e) {
|
||||
s_logger.warn("UnknownHostException when trying to lookup remote IP-Address. This should never happen. Blocking request.", e);
|
||||
}
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append("<html><title>").append(escapeHTML(vmName)).append("</title><frameset><frame src=\"").append(composeConsoleAccessUrl(rootUrl, vm, host, remoteAddress));
|
||||
sb.append("\"></frame></frameset></html>");
|
||||
s_logger.debug("the console url is :: " + sb.toString());
|
||||
sendResponse(resp, sb.toString());
|
||||
}
|
||||
|
||||
private void handleAuthRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) {
|
||||
|
||||
// TODO authentication channel between console proxy VM and management server needs to be secured,
|
||||
|
|
@ -436,145 +357,6 @@ public class ConsoleProxyServlet extends HttpServlet {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL to establish a VNC over websocket connection
|
||||
*/
|
||||
private void setWebsocketUrl(VirtualMachine vm, ConsoleProxyClientParam param) {
|
||||
String ticket = acquireVncTicketForVmwareVm(vm);
|
||||
if (StringUtils.isBlank(ticket)) {
|
||||
s_logger.error("Could not obtain VNC ticket for VM " + vm.getInstanceName());
|
||||
return;
|
||||
}
|
||||
String wsUrl = composeWebsocketUrlForVmwareVm(ticket, param);
|
||||
param.setWebsocketUrl(wsUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format expected: wss://<ESXi_HOST_IP>:443/ticket/<TICKET_ID>
|
||||
*/
|
||||
private String composeWebsocketUrlForVmwareVm(String ticket, ConsoleProxyClientParam param) {
|
||||
param.setClientHostPort(443);
|
||||
return String.format("wss://%s:%s/ticket/%s", param.getClientHostAddress(), param.getClientHostPort(), ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a ticket to be used for console proxy as described in 'Removal of VNC Server from ESXi' on:
|
||||
* https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
|
||||
*/
|
||||
private String acquireVncTicketForVmwareVm(VirtualMachine vm) {
|
||||
try {
|
||||
s_logger.info("Acquiring VNC ticket for VM = " + vm.getHostName());
|
||||
GetVmVncTicketCommand cmd = new GetVmVncTicketCommand(vm.getInstanceName());
|
||||
Answer answer = agentManager.send(vm.getHostId(), cmd);
|
||||
GetVmVncTicketAnswer ans = (GetVmVncTicketAnswer) answer;
|
||||
if (!ans.getResult()) {
|
||||
s_logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails());
|
||||
}
|
||||
return ans.getTicket();
|
||||
} catch (AgentUnavailableException | OperationTimedoutException e) {
|
||||
s_logger.error("Error acquiring ticket", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO hostVo, InetAddress addr) {
|
||||
StringBuffer sb = new StringBuffer(rootUrl);
|
||||
String host = hostVo.getPrivateIpAddress();
|
||||
|
||||
Pair<String, Integer> portInfo = null;
|
||||
if (hostVo.getHypervisorType() == Hypervisor.HypervisorType.KVM &&
|
||||
(hostVo.getResourceState().equals(ResourceState.ErrorInMaintenance) ||
|
||||
hostVo.getResourceState().equals(ResourceState.ErrorInPrepareForMaintenance))) {
|
||||
UserVmDetailVO detailAddress = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_ADDRESS);
|
||||
UserVmDetailVO detailPort = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_PORT);
|
||||
if (detailAddress != null && detailPort != null) {
|
||||
portInfo = new Pair<>(detailAddress.getValue(), Integer.valueOf(detailPort.getValue()));
|
||||
} else {
|
||||
s_logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " +
|
||||
"no VNC Address/Port was available. Falling back to default one from MS.");
|
||||
}
|
||||
}
|
||||
|
||||
if (portInfo == null) {
|
||||
portInfo = _ms.getVncPort(vm);
|
||||
}
|
||||
|
||||
if (s_logger.isDebugEnabled())
|
||||
s_logger.debug("Port info " + portInfo.first());
|
||||
|
||||
Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first());
|
||||
|
||||
int port = -1;
|
||||
if (portInfo.second() == -9) {
|
||||
//for hyperv
|
||||
port = Integer.parseInt(_ms.findDetail(hostVo.getId(), "rdp.server.port").getValue());
|
||||
} else {
|
||||
port = portInfo.second();
|
||||
}
|
||||
|
||||
String sid = vm.getVncPassword();
|
||||
UserVmDetailVO details = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD);
|
||||
|
||||
String tag = vm.getUuid();
|
||||
|
||||
String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag);
|
||||
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
|
||||
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
|
||||
param.setClientHostAddress(parsedHostInfo.first());
|
||||
param.setClientHostPort(port);
|
||||
param.setClientHostPassword(sid);
|
||||
param.setClientTag(tag);
|
||||
param.setTicket(ticket);
|
||||
param.setSourceIP(addr != null ? addr.getHostAddress(): null);
|
||||
|
||||
if (requiresVncOverWebSocketConnection(vm, hostVo)) {
|
||||
setWebsocketUrl(vm, param);
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
param.setLocale(details.getValue());
|
||||
}
|
||||
|
||||
if (portInfo.second() == -9) {
|
||||
//For Hyperv Clinet Host Address will send Instance id
|
||||
param.setHypervHost(host);
|
||||
param.setUsername(_ms.findDetail(hostVo.getId(), "username").getValue());
|
||||
param.setPassword(_ms.findDetail(hostVo.getId(), "password").getValue());
|
||||
}
|
||||
if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) {
|
||||
param.setClientTunnelUrl(parsedHostInfo.second());
|
||||
param.setClientTunnelSession(parsedHostInfo.third());
|
||||
}
|
||||
|
||||
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
|
||||
sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
|
||||
} else {
|
||||
sb.append("/resource/noVNC/vnc.html")
|
||||
.append("?autoconnect=true")
|
||||
.append("&port=" + ConsoleProxyManager.DEFAULT_NOVNC_PORT)
|
||||
.append("&token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
|
||||
}
|
||||
|
||||
// for console access, we need guest OS type to help implement keyboard
|
||||
long guestOs = vm.getGuestOSId();
|
||||
GuestOSVO guestOsVo = _ms.getGuestOs(guestOs);
|
||||
if (guestOsVo.getCategoryId() == 6)
|
||||
sb.append("&guest=windows");
|
||||
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("Compose console url: " + sb.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Since VMware 7.0 VNC servers are deprecated, it uses a ticket to create a VNC over websocket connection
|
||||
* Check: https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
|
||||
*/
|
||||
private boolean requiresVncOverWebSocketConnection(VirtualMachine vm, HostVO hostVo) {
|
||||
return vm.getHypervisorType() == Hypervisor.HypervisorType.VMware && hostVo.getHypervisorVersion().compareTo("7.0") >= 0;
|
||||
}
|
||||
|
||||
public static String genAccessTicket(String host, String port, String sid, String tag) {
|
||||
return genAccessTicket(host, port, sid, tag, new Date());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,494 @@
|
|||
// 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.cloudstack.consoleproxy;
|
||||
|
||||
import com.cloud.agent.AgentManager;
|
||||
import com.cloud.agent.api.Answer;
|
||||
import com.cloud.agent.api.GetVmVncTicketAnswer;
|
||||
import com.cloud.agent.api.GetVmVncTicketCommand;
|
||||
import com.cloud.consoleproxy.ConsoleProxyManager;
|
||||
import com.cloud.exception.AgentUnavailableException;
|
||||
import com.cloud.exception.OperationTimedoutException;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.host.HostVO;
|
||||
import com.cloud.hypervisor.Hypervisor;
|
||||
import com.cloud.resource.ResourceState;
|
||||
import com.cloud.server.ManagementServer;
|
||||
import com.cloud.servlet.ConsoleProxyClientParam;
|
||||
import com.cloud.servlet.ConsoleProxyPasswordBasedEncryptor;
|
||||
import com.cloud.storage.GuestOSVO;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.utils.Pair;
|
||||
import com.cloud.utils.Ternary;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.cloud.vm.UserVmDetailVO;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import com.cloud.vm.VirtualMachineManager;
|
||||
import com.cloud.vm.VmDetailConstants;
|
||||
import com.cloud.vm.dao.UserVmDetailsDao;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.security.keys.KeysManager;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.inject.Inject;
|
||||
import javax.naming.ConfigurationException;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAccessManager {
|
||||
|
||||
@Inject
|
||||
private AccountManager accountManager;
|
||||
@Inject
|
||||
private VirtualMachineManager virtualMachineManager;
|
||||
@Inject
|
||||
private ManagementServer managementServer;
|
||||
@Inject
|
||||
private EntityManager entityManager;
|
||||
@Inject
|
||||
private UserVmDetailsDao userVmDetailsDao;
|
||||
@Inject
|
||||
private KeysManager keysManager;
|
||||
@Inject
|
||||
private AgentManager agentManager;
|
||||
@Inject
|
||||
private ConsoleProxyManager consoleProxyManager;
|
||||
|
||||
private static KeysManager secretKeysManager;
|
||||
private final Gson gson = new GsonBuilder().create();
|
||||
|
||||
public static final Logger s_logger = Logger.getLogger(ConsoleAccessManagerImpl.class.getName());
|
||||
|
||||
private static Set<String> allowedSessions;
|
||||
|
||||
@Override
|
||||
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
|
||||
ConsoleAccessManagerImpl.secretKeysManager = keysManager;
|
||||
ConsoleAccessManagerImpl.allowedSessions = new HashSet<>();
|
||||
return super.configure(name, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsoleEndpoint generateConsoleEndpoint(Long vmId, String extraSecurityToken, String clientAddress) {
|
||||
try {
|
||||
if (ObjectUtils.anyNull(accountManager, virtualMachineManager, managementServer)) {
|
||||
return new ConsoleEndpoint(false, null, "Console service is not ready");
|
||||
}
|
||||
|
||||
if (keysManager.getHashKey() == null) {
|
||||
String msg = "Console access denied. Ticket service is not ready yet";
|
||||
s_logger.debug(msg);
|
||||
return new ConsoleEndpoint(false, null, msg);
|
||||
}
|
||||
|
||||
Account account = CallContext.current().getCallingAccount();
|
||||
|
||||
// Do a sanity check here to make sure the user hasn't already been deleted
|
||||
if (account == null) {
|
||||
s_logger.debug("Invalid user/account, reject console access");
|
||||
return new ConsoleEndpoint(false, null,"Access denied. Invalid or inconsistent account is found");
|
||||
}
|
||||
|
||||
VirtualMachine vm = entityManager.findById(VirtualMachine.class, vmId);
|
||||
if (vm == null) {
|
||||
s_logger.info("Invalid console servlet command parameter: " + vmId);
|
||||
return new ConsoleEndpoint(false, null, "Cannot find VM with ID " + vmId);
|
||||
}
|
||||
|
||||
if (!checkSessionPermission(vm, account)) {
|
||||
return new ConsoleEndpoint(false, null, "Permission denied");
|
||||
}
|
||||
|
||||
if (BooleanUtils.isTrue(ConsoleAccessManager.ConsoleProxyExtraSecurityValidationEnabled.value()) &&
|
||||
StringUtils.isBlank(extraSecurityToken)) {
|
||||
String errorMsg = "Extra security validation is enabled but the extra token is missing";
|
||||
s_logger.error(errorMsg);
|
||||
return new ConsoleEndpoint(false, errorMsg);
|
||||
}
|
||||
|
||||
String sessionUuid = UUID.randomUUID().toString();
|
||||
return generateAccessEndpoint(vmId, sessionUuid, extraSecurityToken, clientAddress);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = String.format("Unexepected exception in ConsoleAccessManager - vmId: %s, clientAddress: %s",
|
||||
vmId, clientAddress);
|
||||
s_logger.error(errorMsg, e);
|
||||
return new ConsoleEndpoint(false, null, "Server Internal Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSessionAllowed(String sessionUuid) {
|
||||
return allowedSessions.contains(sessionUuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSessions(String[] sessionUuids) {
|
||||
for (String r : sessionUuids) {
|
||||
allowedSessions.remove(r);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean checkSessionPermission(VirtualMachine vm, Account account) {
|
||||
if (accountManager.isRootAdmin(account.getId())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (vm.getType()) {
|
||||
case User:
|
||||
try {
|
||||
accountManager.checkAccess(account, null, true, vm);
|
||||
} catch (PermissionDeniedException ex) {
|
||||
if (accountManager.isNormalUser(account.getId())) {
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("VM access is denied for VM ID " + vm.getUuid() + ". VM owner account " +
|
||||
vm.getAccountId() + " does not match the account id in session " +
|
||||
account.getId() + " and caller is a normal user");
|
||||
}
|
||||
} else if ((accountManager.isDomainAdmin(account.getId())
|
||||
|| account.getType() == Account.Type.READ_ONLY_ADMIN) && s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("VM access is denied for VM ID " + vm.getUuid() + ". VM owner account " +
|
||||
vm.getAccountId() + " does not match the account id in session " +
|
||||
account.getId() + " and the domain-admin caller does not manage the target domain");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case DomainRouter:
|
||||
case ConsoleProxy:
|
||||
case SecondaryStorageVm:
|
||||
return false;
|
||||
|
||||
default:
|
||||
s_logger.warn("Unrecoginized virtual machine type, deny access by default. type: " + vm.getType());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ConsoleEndpoint generateAccessEndpoint(Long vmId, String sessionUuid, String extraSecurityToken, String clientAddress) {
|
||||
VirtualMachine vm = virtualMachineManager.findById(vmId);
|
||||
String msg;
|
||||
if (vm == null) {
|
||||
msg = "VM " + vmId + " does not exist, sending blank response for console access request";
|
||||
s_logger.warn(msg);
|
||||
throw new CloudRuntimeException(msg);
|
||||
}
|
||||
|
||||
if (vm.getHostId() == null) {
|
||||
msg = "VM " + vmId + " lost host info, sending blank response for console access request";
|
||||
s_logger.warn(msg);
|
||||
throw new CloudRuntimeException(msg);
|
||||
}
|
||||
|
||||
HostVO host = managementServer.getHostBy(vm.getHostId());
|
||||
if (host == null) {
|
||||
msg = "VM " + vmId + "'s host does not exist, sending blank response for console access request";
|
||||
s_logger.warn(msg);
|
||||
throw new CloudRuntimeException(msg);
|
||||
}
|
||||
|
||||
if (Hypervisor.HypervisorType.LXC.equals(vm.getHypervisorType())) {
|
||||
throw new CloudRuntimeException("Console access is not supported for LXC");
|
||||
}
|
||||
|
||||
String rootUrl = managementServer.getConsoleAccessUrlRoot(vmId);
|
||||
if (rootUrl == null) {
|
||||
throw new CloudRuntimeException("Console access will be ready in a few minutes. Please try it again later.");
|
||||
}
|
||||
|
||||
ConsoleEndpoint consoleEndpoint = composeConsoleAccessEndpoint(rootUrl, vm, host, clientAddress, sessionUuid, extraSecurityToken);
|
||||
s_logger.debug("The console URL is: " + consoleEndpoint.getUrl());
|
||||
return consoleEndpoint;
|
||||
}
|
||||
|
||||
private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMachine vm, HostVO hostVo, String addr,
|
||||
String sessionUuid, String extraSecurityToken) {
|
||||
String host = hostVo.getPrivateIpAddress();
|
||||
|
||||
Pair<String, Integer> portInfo = null;
|
||||
if (hostVo.getHypervisorType() == Hypervisor.HypervisorType.KVM &&
|
||||
(hostVo.getResourceState().equals(ResourceState.ErrorInMaintenance) ||
|
||||
hostVo.getResourceState().equals(ResourceState.ErrorInPrepareForMaintenance))) {
|
||||
UserVmDetailVO detailAddress = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_ADDRESS);
|
||||
UserVmDetailVO detailPort = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_PORT);
|
||||
if (detailAddress != null && detailPort != null) {
|
||||
portInfo = new Pair<>(detailAddress.getValue(), Integer.valueOf(detailPort.getValue()));
|
||||
} else {
|
||||
s_logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " +
|
||||
"no VNC Address/Port was available. Falling back to default one from MS.");
|
||||
}
|
||||
}
|
||||
|
||||
if (portInfo == null) {
|
||||
portInfo = managementServer.getVncPort(vm);
|
||||
}
|
||||
|
||||
if (s_logger.isDebugEnabled())
|
||||
s_logger.debug("Port info " + portInfo.first());
|
||||
|
||||
Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first());
|
||||
|
||||
int port = -1;
|
||||
if (portInfo.second() == -9) {
|
||||
//for hyperv
|
||||
port = Integer.parseInt(managementServer.findDetail(hostVo.getId(), "rdp.server.port").getValue());
|
||||
} else {
|
||||
port = portInfo.second();
|
||||
}
|
||||
|
||||
String sid = vm.getVncPassword();
|
||||
UserVmDetailVO details = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD);
|
||||
|
||||
String tag = vm.getUuid();
|
||||
|
||||
String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid);
|
||||
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
|
||||
ConsoleProxyClientParam param = generateConsoleProxyClientParam(parsedHostInfo, port, sid, tag, ticket,
|
||||
sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host);
|
||||
String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param);
|
||||
int vncPort = consoleProxyManager.getVncPort();
|
||||
|
||||
String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm);
|
||||
|
||||
s_logger.debug("Adding allowed session: " + sessionUuid);
|
||||
allowedSessions.add(sessionUuid);
|
||||
managementServer.setConsoleAccessForVm(vm.getId(), sessionUuid);
|
||||
|
||||
ConsoleEndpoint consoleEndpoint = new ConsoleEndpoint(true, url);
|
||||
consoleEndpoint.setWebsocketHost(managementServer.getConsoleAccessAddress(vm.getId()));
|
||||
consoleEndpoint.setWebsocketPort(String.valueOf(vncPort));
|
||||
consoleEndpoint.setWebsocketPath("websockify");
|
||||
consoleEndpoint.setWebsocketToken(token);
|
||||
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
|
||||
consoleEndpoint.setWebsocketExtra(param.getExtraSecurityToken());
|
||||
}
|
||||
return consoleEndpoint;
|
||||
}
|
||||
|
||||
private String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam param, String token, int vncPort,
|
||||
VirtualMachine vm) {
|
||||
StringBuilder sb = new StringBuilder(rootUrl);
|
||||
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
|
||||
sb.append("/ajax?token=" + token);
|
||||
} else {
|
||||
sb.append("/resource/noVNC/vnc.html")
|
||||
.append("?autoconnect=true")
|
||||
.append("&port=" + vncPort)
|
||||
.append("&token=" + token);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
|
||||
sb.append("&extra=" + param.getExtraSecurityToken());
|
||||
}
|
||||
|
||||
// for console access, we need guest OS type to help implement keyboard
|
||||
long guestOs = vm.getGuestOSId();
|
||||
GuestOSVO guestOsVo = managementServer.getGuestOs(guestOs);
|
||||
if (guestOsVo.getCategoryId() == 6)
|
||||
sb.append("&guest=windows");
|
||||
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("Compose console url: " + sb);
|
||||
}
|
||||
return sb.toString().startsWith("https") ? sb.toString() : "http:" + sb;
|
||||
}
|
||||
|
||||
private ConsoleProxyClientParam generateConsoleProxyClientParam(Ternary<String, String, String> parsedHostInfo,
|
||||
int port, String sid, String tag, String ticket,
|
||||
String sessionUuid, String addr,
|
||||
String extraSecurityToken, VirtualMachine vm,
|
||||
HostVO hostVo, UserVmDetailVO details,
|
||||
Pair<String, Integer> portInfo, String host) {
|
||||
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
|
||||
param.setClientHostAddress(parsedHostInfo.first());
|
||||
param.setClientHostPort(port);
|
||||
param.setClientHostPassword(sid);
|
||||
param.setClientTag(tag);
|
||||
param.setTicket(ticket);
|
||||
param.setSessionUuid(sessionUuid);
|
||||
param.setSourceIP(addr);
|
||||
|
||||
if (StringUtils.isNotBlank(extraSecurityToken)) {
|
||||
param.setExtraSecurityToken(extraSecurityToken);
|
||||
s_logger.debug("Added security token for client validation");
|
||||
}
|
||||
|
||||
if (requiresVncOverWebSocketConnection(vm, hostVo)) {
|
||||
setWebsocketUrl(vm, param);
|
||||
}
|
||||
|
||||
if (details != null) {
|
||||
param.setLocale(details.getValue());
|
||||
}
|
||||
|
||||
if (portInfo.second() == -9) {
|
||||
//For Hyperv Clinet Host Address will send Instance id
|
||||
param.setHypervHost(host);
|
||||
param.setUsername(managementServer.findDetail(hostVo.getId(), "username").getValue());
|
||||
param.setPassword(managementServer.findDetail(hostVo.getId(), "password").getValue());
|
||||
}
|
||||
if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) {
|
||||
param.setClientTunnelUrl(parsedHostInfo.second());
|
||||
param.setClientTunnelSession(parsedHostInfo.third());
|
||||
}
|
||||
return param;
|
||||
}
|
||||
|
||||
public static Ternary<String, String, String> parseHostInfo(String hostInfo) {
|
||||
String host = null;
|
||||
String tunnelUrl = null;
|
||||
String tunnelSession = null;
|
||||
|
||||
s_logger.info("Parse host info returned from executing GetVNCPortCommand. host info: " + hostInfo);
|
||||
|
||||
if (hostInfo != null) {
|
||||
if (hostInfo.startsWith("consoleurl")) {
|
||||
String[] tokens = hostInfo.split("&");
|
||||
|
||||
if (hostInfo.length() > 19 && hostInfo.indexOf('/', 19) > 19) {
|
||||
host = hostInfo.substring(19, hostInfo.indexOf('/', 19)).trim();
|
||||
tunnelUrl = tokens[0].substring("consoleurl=".length());
|
||||
tunnelSession = tokens[1].split("=")[1];
|
||||
} else {
|
||||
host = "";
|
||||
}
|
||||
} else if (hostInfo.startsWith("instanceId")) {
|
||||
host = hostInfo.substring(hostInfo.indexOf('=') + 1);
|
||||
} else {
|
||||
host = hostInfo;
|
||||
}
|
||||
} else {
|
||||
host = hostInfo;
|
||||
}
|
||||
|
||||
return new Ternary<>(host, tunnelUrl, tunnelSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since VMware 7.0 VNC servers are deprecated, it uses a ticket to create a VNC over websocket connection
|
||||
* Check: https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
|
||||
*/
|
||||
private boolean requiresVncOverWebSocketConnection(VirtualMachine vm, HostVO hostVo) {
|
||||
return vm.getHypervisorType() == Hypervisor.HypervisorType.VMware && hostVo.getHypervisorVersion().compareTo("7.0") >= 0;
|
||||
}
|
||||
|
||||
public static String genAccessTicket(String host, String port, String sid, String tag, String sessionUuid) {
|
||||
return genAccessTicket(host, port, sid, tag, new Date(), sessionUuid);
|
||||
}
|
||||
|
||||
public static String genAccessTicket(String host, String port, String sid, String tag, Date normalizedHashTime, String sessionUuid) {
|
||||
String params = "host=" + host + "&port=" + port + "&sid=" + sid + "&tag=" + tag + "&session=" + sessionUuid;
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA512");
|
||||
|
||||
long ts = normalizedHashTime.getTime();
|
||||
ts = ts / 60000; // round up to 1 minute
|
||||
String secretKey = secretKeysManager.getHashKey();
|
||||
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), mac.getAlgorithm());
|
||||
mac.init(keySpec);
|
||||
mac.update(params.getBytes());
|
||||
mac.update(String.valueOf(ts).getBytes());
|
||||
|
||||
byte[] encryptedBytes = mac.doFinal();
|
||||
|
||||
return Base64.encodeBase64String(encryptedBytes);
|
||||
} catch (Exception e) {
|
||||
s_logger.error("Unexpected exception ", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getEncryptorPassword() {
|
||||
String key = keysManager.getEncryptionKey();
|
||||
String iv = keysManager.getEncryptionIV();
|
||||
|
||||
ConsoleProxyPasswordBasedEncryptor.KeyIVPair keyIvPair = new ConsoleProxyPasswordBasedEncryptor.KeyIVPair(key, iv);
|
||||
return gson.toJson(keyIvPair);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL to establish a VNC over websocket connection
|
||||
*/
|
||||
private void setWebsocketUrl(VirtualMachine vm, ConsoleProxyClientParam param) {
|
||||
String ticket = acquireVncTicketForVmwareVm(vm);
|
||||
if (StringUtils.isBlank(ticket)) {
|
||||
s_logger.error("Could not obtain VNC ticket for VM " + vm.getInstanceName());
|
||||
return;
|
||||
}
|
||||
String wsUrl = composeWebsocketUrlForVmwareVm(ticket, param);
|
||||
param.setWebsocketUrl(wsUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format expected: wss://<ESXi_HOST_IP>:443/ticket/<TICKET_ID>
|
||||
*/
|
||||
private String composeWebsocketUrlForVmwareVm(String ticket, ConsoleProxyClientParam param) {
|
||||
param.setClientHostPort(443);
|
||||
return String.format("wss://%s:%s/ticket/%s", param.getClientHostAddress(), param.getClientHostPort(), ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a ticket to be used for console proxy as described in 'Removal of VNC Server from ESXi' on:
|
||||
* https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
|
||||
*/
|
||||
private String acquireVncTicketForVmwareVm(VirtualMachine vm) {
|
||||
try {
|
||||
s_logger.info("Acquiring VNC ticket for VM = " + vm.getHostName());
|
||||
GetVmVncTicketCommand cmd = new GetVmVncTicketCommand(vm.getInstanceName());
|
||||
Answer answer = agentManager.send(vm.getHostId(), cmd);
|
||||
GetVmVncTicketAnswer ans = (GetVmVncTicketAnswer) answer;
|
||||
if (!ans.getResult()) {
|
||||
s_logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails());
|
||||
}
|
||||
return ans.getTicket();
|
||||
} catch (AgentUnavailableException | OperationTimedoutException e) {
|
||||
s_logger.error("Error acquiring ticket", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigComponentName() {
|
||||
return ConsoleAccessManagerImpl.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigKey<?>[] getConfigKeys() {
|
||||
return new ConfigKey[] { ConsoleProxyExtraSecurityValidationEnabled };
|
||||
}
|
||||
}
|
||||
|
|
@ -109,6 +109,8 @@
|
|||
value="#{consoleProxyAllocatorsRegistry.registered}" />
|
||||
</bean>
|
||||
|
||||
<bean id="consoleAccessManagerImpl" class="org.apache.cloudstack.consoleproxy.ConsoleAccessManagerImpl" />
|
||||
|
||||
<bean id="securityGroupManagerImpl2" class="com.cloud.network.security.SecurityGroupManagerImpl2" />
|
||||
|
||||
<bean id="ipv6AddressManagerImpl" class="com.cloud.network.Ipv6AddressManagerImpl" />
|
||||
|
|
|
|||
|
|
@ -44,12 +44,12 @@ import org.junit.Test;
|
|||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import com.cloud.server.ManagementServer;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.User;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApiServletTest {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
// 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.cloudstack.consoleproxy;
|
||||
|
||||
import com.cloud.agent.AgentManager;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.server.ManagementServer;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import com.cloud.vm.VirtualMachine;
|
||||
import com.cloud.vm.VirtualMachineManager;
|
||||
import com.cloud.vm.dao.UserVmDetailsDao;
|
||||
import org.apache.cloudstack.acl.SecurityChecker;
|
||||
import org.apache.cloudstack.framework.security.keys.KeysManager;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ConsoleAccessManagerImplTest {
|
||||
|
||||
@Mock
|
||||
private AccountManager accountManager;
|
||||
@Mock
|
||||
private VirtualMachineManager virtualMachineManager;
|
||||
@Mock
|
||||
private ManagementServer managementServer;
|
||||
@Mock
|
||||
private EntityManager entityManager;
|
||||
@Mock
|
||||
private UserVmDetailsDao userVmDetailsDao;
|
||||
@Mock
|
||||
private KeysManager keysManager;
|
||||
@Mock
|
||||
private AgentManager agentManager;
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
ConsoleAccessManagerImpl consoleAccessManager = new ConsoleAccessManagerImpl();
|
||||
|
||||
@Mock
|
||||
VirtualMachine virtualMachine;
|
||||
@Mock
|
||||
Account account;
|
||||
|
||||
@Test
|
||||
public void testCheckSessionPermissionAdminAccount() {
|
||||
Mockito.when(account.getId()).thenReturn(1L);
|
||||
Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(true);
|
||||
Assert.assertTrue(consoleAccessManager.checkSessionPermission(virtualMachine, account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSessionPermissionUserOwnedVm() {
|
||||
Mockito.when(account.getId()).thenReturn(1L);
|
||||
Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(false);
|
||||
Mockito.when(virtualMachine.getType()).thenReturn(VirtualMachine.Type.User);
|
||||
Mockito.doNothing().when(accountManager).checkAccess(
|
||||
Mockito.eq(account), Mockito.nullable(SecurityChecker.AccessType.class),
|
||||
Mockito.eq(true), Mockito.eq(virtualMachine));
|
||||
Assert.assertTrue(consoleAccessManager.checkSessionPermission(virtualMachine, account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSessionPermissionDifferentUserOwnedVm() {
|
||||
Mockito.when(account.getId()).thenReturn(1L);
|
||||
Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(false);
|
||||
Mockito.when(virtualMachine.getType()).thenReturn(VirtualMachine.Type.User);
|
||||
Mockito.doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(
|
||||
Mockito.eq(account), Mockito.nullable(SecurityChecker.AccessType.class),
|
||||
Mockito.eq(true), Mockito.eq(virtualMachine));
|
||||
Assert.assertFalse(consoleAccessManager.checkSessionPermission(virtualMachine, account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSessionPermissionForUsersOnSystemVms() {
|
||||
Mockito.when(account.getId()).thenReturn(1L);
|
||||
Mockito.when(accountManager.isRootAdmin(1L)).thenReturn(false);
|
||||
List<VirtualMachine.Type> systemVmTypes = Arrays.asList(VirtualMachine.Type.DomainRouter,
|
||||
VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm);
|
||||
for (VirtualMachine.Type type : systemVmTypes) {
|
||||
Mockito.when(virtualMachine.getType()).thenReturn(type);
|
||||
Assert.assertFalse(consoleAccessManager.checkSessionPermission(virtualMachine, account));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,9 +26,11 @@ import java.lang.reflect.Method;
|
|||
import java.net.InetSocketAddress;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
|
@ -77,6 +79,12 @@ public class ConsoleProxy {
|
|||
static String encryptorPassword = "Dummy";
|
||||
static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"};
|
||||
|
||||
static Set<String> allowedSessions = new HashSet<>();
|
||||
|
||||
public static void addAllowedSession(String sessionUuid) {
|
||||
allowedSessions.add(sessionUuid);
|
||||
}
|
||||
|
||||
private static void configLog4j() {
|
||||
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
||||
URL configUrl = loader.getResource("/conf/log4j-cloud.xml");
|
||||
|
|
@ -178,6 +186,29 @@ public class ConsoleProxy {
|
|||
authResult.setHost(param.getClientHostAddress());
|
||||
authResult.setPort(param.getClientHostPort());
|
||||
|
||||
if (org.apache.commons.lang3.StringUtils.isNotBlank(param.getExtraSecurityToken())) {
|
||||
String extraToken = param.getExtraSecurityToken();
|
||||
String clientProvidedToken = param.getClientProvidedExtraSecurityToken();
|
||||
s_logger.debug(String.format("Extra security validation for the console access, provided %s " +
|
||||
"to validate against %s", clientProvidedToken, extraToken));
|
||||
|
||||
if (!extraToken.equals(clientProvidedToken)) {
|
||||
s_logger.error("The provided extra token does not match the expected value for this console endpoint");
|
||||
authResult.setSuccess(false);
|
||||
return authResult;
|
||||
}
|
||||
}
|
||||
|
||||
String sessionUuid = param.getSessionUuid();
|
||||
if (allowedSessions.contains(sessionUuid)) {
|
||||
s_logger.debug("Acquiring the session " + sessionUuid + " not available for future use");
|
||||
allowedSessions.remove(sessionUuid);
|
||||
} else {
|
||||
s_logger.info("Session " + sessionUuid + " has already been used, cannot connect");
|
||||
authResult.setSuccess(false);
|
||||
return authResult;
|
||||
}
|
||||
|
||||
String websocketUrl = param.getWebsocketUrl();
|
||||
if (StringUtils.isNotBlank(websocketUrl)) {
|
||||
return authResult;
|
||||
|
|
@ -192,7 +223,7 @@ public class ConsoleProxy {
|
|||
try {
|
||||
result =
|
||||
authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), param.getClientTag(),
|
||||
param.getClientHostPassword(), param.getTicket(), new Boolean(reauthentication));
|
||||
param.getClientHostPassword(), param.getTicket(), reauthentication, param.getSessionUuid());
|
||||
} catch (IllegalAccessException e) {
|
||||
s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e);
|
||||
authResult.setSuccess(false);
|
||||
|
|
@ -266,7 +297,8 @@ public class ConsoleProxy {
|
|||
try {
|
||||
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
||||
Class<?> contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource");
|
||||
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class, Boolean.class);
|
||||
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class,
|
||||
String.class, String.class, String.class, Boolean.class, String.class);
|
||||
reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class);
|
||||
ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class);
|
||||
} catch (SecurityException e) {
|
||||
|
|
@ -371,9 +403,10 @@ public class ConsoleProxy {
|
|||
}
|
||||
|
||||
private static ConsoleProxyNoVNCServer getNoVNCServer() {
|
||||
if (httpListenPort == 443)
|
||||
return new ConsoleProxyNoVNCServer(ksBits, ksPassword);
|
||||
return new ConsoleProxyNoVNCServer();
|
||||
int vncPort = ConsoleProxyNoVNCServer.getVNCPort();
|
||||
return vncPort == ConsoleProxyNoVNCServer.WSS_PORT ?
|
||||
new ConsoleProxyNoVNCServer(ksBits, ksPassword) :
|
||||
new ConsoleProxyNoVNCServer();
|
||||
}
|
||||
|
||||
private static void startupHttpCmdPort() {
|
||||
|
|
|
|||
|
|
@ -78,4 +78,6 @@ public interface ConsoleProxyClient {
|
|||
void initClient(ConsoleProxyClientParam param);
|
||||
|
||||
void closeClient();
|
||||
|
||||
String getSessionUuid();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons
|
|||
protected boolean framebufferResized = false;
|
||||
protected int resizedFramebufferWidth;
|
||||
protected int resizedFramebufferHeight;
|
||||
protected String sessionUuid;
|
||||
|
||||
public ConsoleProxyClientBase() {
|
||||
tracker = new TileTracker();
|
||||
|
|
@ -422,4 +423,9 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons
|
|||
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(ConsoleProxy.getEncryptorPassword());
|
||||
this.clientToken = encryptor.encryptObject(ConsoleProxyClientParam.class, clientParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,19 @@ public class ConsoleProxyClientParam {
|
|||
|
||||
private String sourceIP;
|
||||
|
||||
private String sessionUuid;
|
||||
|
||||
/**
|
||||
* The server-side generated value for extra console endpoint validation
|
||||
*/
|
||||
private String extraSecurityToken;
|
||||
|
||||
/**
|
||||
* The extra parameter received in the console URL, must be compared against the server-side generated value
|
||||
* for extra validation (if has been enabled)
|
||||
*/
|
||||
private String clientProvidedExtraSecurityToken;
|
||||
|
||||
public ConsoleProxyClientParam() {
|
||||
clientHostPort = 0;
|
||||
}
|
||||
|
|
@ -162,4 +175,28 @@ public class ConsoleProxyClientParam {
|
|||
public void setWebsocketUrl(String websocketUrl) {
|
||||
this.websocketUrl = websocketUrl;
|
||||
}
|
||||
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
public void setSessionUuid(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
|
||||
public String getExtraSecurityToken() {
|
||||
return extraSecurityToken;
|
||||
}
|
||||
|
||||
public void setExtraSecurityToken(String extraSecurityToken) {
|
||||
this.extraSecurityToken = extraSecurityToken;
|
||||
}
|
||||
|
||||
public String getClientProvidedExtraSecurityToken() {
|
||||
return clientProvidedExtraSecurityToken;
|
||||
}
|
||||
|
||||
public void setClientProvidedExtraSecurityToken(String clientProvidedExtraSecurityToken) {
|
||||
this.clientProvidedExtraSecurityToken = clientProvidedExtraSecurityToken;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import java.io.OutputStreamWriter;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
|
@ -31,10 +32,16 @@ import com.google.gson.GsonBuilder;
|
|||
public class ConsoleProxyClientStatsCollector {
|
||||
|
||||
ArrayList<ConsoleProxyConnection> connections;
|
||||
ArrayList<String> removedSessions;
|
||||
|
||||
public ConsoleProxyClientStatsCollector() {
|
||||
}
|
||||
|
||||
public void setRemovedSessions(List<String> removed) {
|
||||
removedSessions = new ArrayList<>();
|
||||
removedSessions.addAll(removed);
|
||||
}
|
||||
|
||||
public ConsoleProxyClientStatsCollector(Hashtable<String, ConsoleProxyClient> connMap) {
|
||||
setConnections(connMap);
|
||||
}
|
||||
|
|
@ -67,6 +74,7 @@ public class ConsoleProxyClientStatsCollector {
|
|||
conn.tag = client.getClientTag();
|
||||
conn.createTime = client.getClientCreateTime();
|
||||
conn.lastUsedTime = client.getClientLastFrontEndActivityTime();
|
||||
conn.setSessionUuid(client.getSessionUuid());
|
||||
conns.add(conn);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +89,15 @@ public class ConsoleProxyClientStatsCollector {
|
|||
public String tag;
|
||||
public long createTime;
|
||||
public long lastUsedTime;
|
||||
protected String sessionUuid;
|
||||
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
public void setSessionUuid(String sessionUuid) {
|
||||
this.sessionUuid = sessionUuid;
|
||||
}
|
||||
|
||||
public ConsoleProxyConnection() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
package com.cloud.consoleproxy;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
|
@ -67,9 +69,12 @@ public class ConsoleProxyGCThread extends Thread {
|
|||
|
||||
boolean bReportLoad = false;
|
||||
long lastReportTick = System.currentTimeMillis();
|
||||
List<String> removedSessions = new ArrayList<>();
|
||||
|
||||
while (true) {
|
||||
cleanupLogging();
|
||||
bReportLoad = false;
|
||||
removedSessions.clear();
|
||||
|
||||
if (s_logger.isDebugEnabled())
|
||||
s_logger.debug("connMap=" + connMap);
|
||||
|
|
@ -89,6 +94,7 @@ public class ConsoleProxyGCThread extends Thread {
|
|||
}
|
||||
|
||||
synchronized (connMap) {
|
||||
removedSessions.add(client.getSessionUuid());
|
||||
connMap.remove(key);
|
||||
bReportLoad = true;
|
||||
}
|
||||
|
|
@ -100,7 +106,9 @@ public class ConsoleProxyGCThread extends Thread {
|
|||
|
||||
if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) {
|
||||
// report load changes
|
||||
String loadInfo = new ConsoleProxyClientStatsCollector(connMap).getStatsReport();
|
||||
ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap);
|
||||
collector.setRemovedSessions(removedSessions);
|
||||
String loadInfo = collector.getStatsReport();
|
||||
ConsoleProxy.reportLoadInfo(loadInfo);
|
||||
lastReportTick = System.currentTimeMillis();
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ public class ConsoleProxyHttpHandlerHelper {
|
|||
|
||||
ConsoleProxyClientParam param = encryptor.decryptObject(ConsoleProxyClientParam.class, map.get("token"));
|
||||
|
||||
// make sure we get information from token only
|
||||
guardUserInput(map);
|
||||
if (param != null) {
|
||||
if (param.getClientHostAddress() != null) {
|
||||
|
|
@ -96,6 +95,12 @@ public class ConsoleProxyHttpHandlerHelper {
|
|||
if (param.getWebsocketUrl() != null) {
|
||||
map.put("websocketUrl", param.getWebsocketUrl());
|
||||
}
|
||||
if (param.getSessionUuid() != null) {
|
||||
map.put("sessionUuid", param.getSessionUuid());
|
||||
}
|
||||
if (param.getExtraSecurityToken() != null) {
|
||||
map.put("extraSecurityToken", param.getExtraSecurityToken());
|
||||
}
|
||||
} else {
|
||||
s_logger.error("Unable to decode token");
|
||||
}
|
||||
|
|
@ -104,6 +109,11 @@ public class ConsoleProxyHttpHandlerHelper {
|
|||
guardUserInput(map);
|
||||
}
|
||||
|
||||
if (map.containsKey("extra")) {
|
||||
s_logger.debug(String.format("Found extra parameter: %s for client security validation check " +
|
||||
"on the VNC server", map.get("extra")));
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
|
|||
|
||||
@OnWebSocketConnect
|
||||
public void onConnect(final Session session) throws IOException, InterruptedException {
|
||||
|
||||
String queries = session.getUpgradeRequest().getQueryString();
|
||||
Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries);
|
||||
|
||||
|
|
@ -89,6 +88,7 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
|
|||
String password = queryMap.get("password");
|
||||
String sourceIP = queryMap.get("sourceIP");
|
||||
String websocketUrl = queryMap.get("websocketUrl");
|
||||
String sessionUuid = queryMap.get("sessionUuid");
|
||||
|
||||
if (tag == null)
|
||||
tag = "";
|
||||
|
|
@ -133,6 +133,13 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
|
|||
param.setUsername(username);
|
||||
param.setPassword(password);
|
||||
param.setWebsocketUrl(websocketUrl);
|
||||
param.setSessionUuid(sessionUuid);
|
||||
if (queryMap.containsKey("extraSecurityToken")) {
|
||||
param.setExtraSecurityToken(queryMap.get("extraSecurityToken"));
|
||||
}
|
||||
if (queryMap.containsKey("extra")) {
|
||||
param.setClientProvidedExtraSecurityToken(queryMap.get("extra"));
|
||||
}
|
||||
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
|
||||
} catch (Exception e) {
|
||||
s_logger.warn("Failed to create viewer due to " + e.getMessage(), e);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
package com.cloud.consoleproxy;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import com.cloud.consoleproxy.util.Logger;
|
||||
|
|
@ -32,12 +35,25 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
|
|||
public class ConsoleProxyNoVNCServer {
|
||||
|
||||
private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVNCServer.class);
|
||||
private static final int wsPort = 8080;
|
||||
public static final int WS_PORT = 8080;
|
||||
public static final int WSS_PORT = 8443;
|
||||
private static final String VNC_CONF_FILE_LOCATION = "/root/vncport";
|
||||
|
||||
private Server server;
|
||||
|
||||
public static int getVNCPort() {
|
||||
String portStr;
|
||||
try {
|
||||
portStr = Files.readString(Path.of(VNC_CONF_FILE_LOCATION)).trim();
|
||||
} catch (IOException e) {
|
||||
s_logger.error("Cannot read the VNC port from the file " + VNC_CONF_FILE_LOCATION + " setting it to 8080", e);
|
||||
return WS_PORT;
|
||||
}
|
||||
return Integer.parseInt(portStr);
|
||||
}
|
||||
|
||||
public ConsoleProxyNoVNCServer() {
|
||||
this.server = new Server(wsPort);
|
||||
this.server = new Server(WS_PORT);
|
||||
ConsoleProxyNoVNCHandler handler = new ConsoleProxyNoVNCHandler();
|
||||
this.server.setHandler(handler);
|
||||
}
|
||||
|
|
@ -50,7 +66,7 @@ public class ConsoleProxyNoVNCServer {
|
|||
try {
|
||||
final HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(wsPort);
|
||||
httpConfig.setSecurePort(WSS_PORT);
|
||||
|
||||
final HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
|
||||
httpsConfig.addCustomizer(new SecureRequestCustomizer());
|
||||
|
|
@ -66,7 +82,7 @@ public class ConsoleProxyNoVNCServer {
|
|||
final ServerConnector sslConnector = new ServerConnector(server,
|
||||
new SslConnectionFactory(sslContextFactory, "http/1.1"),
|
||||
new HttpConnectionFactory(httpsConfig));
|
||||
sslConnector.setPort(wsPort);
|
||||
sslConnector.setPort(WSS_PORT);
|
||||
server.addConnector(sslConnector);
|
||||
} catch (Exception e) {
|
||||
s_logger.error("Unable to secure server due to exception ", e);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
|
|||
private boolean connectionAlive;
|
||||
|
||||
private ConsoleProxyClientParam clientParam;
|
||||
private String sessionUuid;
|
||||
|
||||
public ConsoleProxyNoVncClient(Session session) {
|
||||
this.session = session;
|
||||
|
|
@ -89,6 +90,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
|
|||
setClientParam(param);
|
||||
client = new NoVncClient();
|
||||
connectionAlive = true;
|
||||
this.sessionUuid = param.getSessionUuid();
|
||||
|
||||
updateFrontEndActivityTime();
|
||||
Thread worker = new Thread(new Runnable() {
|
||||
|
|
@ -192,6 +194,11 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
|
|||
ConsoleProxy.removeViewer(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionUuid() {
|
||||
return sessionUuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClientId() {
|
||||
return this.clientId;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
// 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 com.cloud.consoleproxy;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
public class ConsoleProxyHttpHandlerHelperTest {
|
||||
|
||||
@Mock
|
||||
ConsoleProxyPasswordBasedEncryptor encryptor;
|
||||
|
||||
@Test
|
||||
@PrepareForTest({ConsoleProxy.class, ConsoleProxyHttpHandlerHelper.class})
|
||||
public void testQueryMapExtraParameter() throws Exception {
|
||||
PowerMockito.mockStatic(ConsoleProxy.class);
|
||||
PowerMockito.when(ConsoleProxy.getEncryptorPassword()).thenReturn("password");
|
||||
PowerMockito.whenNew(ConsoleProxyPasswordBasedEncryptor.class).withArguments(Mockito.anyString()).thenReturn(encryptor);
|
||||
Mockito.when(encryptor.decryptObject(Mockito.eq(ConsoleProxyClientParam.class), Mockito.anyString())).thenReturn(null);
|
||||
|
||||
String extraValidationToken = "test-token";
|
||||
String query = String.format("token=SOME_TOKEN&extra=%s", extraValidationToken);
|
||||
|
||||
Map<String, String> queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(query);
|
||||
Assert.assertTrue(queryMap.containsKey("extra"));
|
||||
Assert.assertEquals(extraValidationToken, queryMap.get("extra"));
|
||||
}
|
||||
}
|
||||
|
|
@ -157,6 +157,7 @@ const UI = {
|
|||
}
|
||||
}
|
||||
|
||||
UI.initSetting('extra', window.location.extra)
|
||||
/* Populate the controls if defaults are provided in the URL */
|
||||
UI.initSetting('host', window.location.hostname);
|
||||
UI.initSetting('port', port);
|
||||
|
|
@ -997,7 +998,8 @@ const UI = {
|
|||
const host = UI.getSetting('host');
|
||||
const port = UI.getSetting('port');
|
||||
const path = UI.getSetting('path');
|
||||
const token = UI.getSetting('token')
|
||||
const token = UI.getSetting('token');
|
||||
const extra = UI.getSetting('extra');
|
||||
|
||||
if (typeof password === 'undefined') {
|
||||
password = WebUtil.getConfigVar('password');
|
||||
|
|
@ -1031,6 +1033,10 @@ const UI = {
|
|||
url += '/' + path;
|
||||
url += '?token=' + token;
|
||||
|
||||
if (extra) {
|
||||
url += '&extra=' + extra
|
||||
}
|
||||
|
||||
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
|
||||
{ shared: UI.getSetting('shared'),
|
||||
repeaterID: UI.getSetting('repeaterID'),
|
||||
|
|
@ -1116,14 +1122,13 @@ const UI = {
|
|||
UI.connected = false;
|
||||
|
||||
UI.rfb = undefined;
|
||||
|
||||
if (!e.detail.clean) {
|
||||
UI.updateVisualState('disconnected');
|
||||
if (wasConnected) {
|
||||
UI.showStatus(_("Something went wrong, connection is closed"),
|
||||
'error');
|
||||
} else {
|
||||
UI.showStatus(_("Failed to connect to server"), 'error');
|
||||
UI.showStatus(_("Failed to connect to server / access token has expired"), 'error');
|
||||
}
|
||||
} else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
|
||||
UI.updateVisualState('reconnecting');
|
||||
|
|
|
|||
|
|
@ -234,6 +234,10 @@
|
|||
<label for="noVNC_setting_token">Token:</label>
|
||||
<input id="noVNC_setting_token" type="text">
|
||||
</li>
|
||||
<li>
|
||||
<label for="noVNC_setting_extra">Extra:</label>
|
||||
<input id="noVNC_setting_extra" type="text">
|
||||
</li>
|
||||
</ul></div>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
|
|
|
|||
|
|
@ -885,6 +885,9 @@ parse_cmd_line() {
|
|||
useHttpsToUpload)
|
||||
export USEHTTPS=$VALUE
|
||||
;;
|
||||
vncport)
|
||||
export VNCPORT=$VALUE
|
||||
;;
|
||||
esac
|
||||
done
|
||||
echo -e "\n\t}\n}" >> ${CHEF_TMP_FILE}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ setup_console_proxy() {
|
|||
public_ip=`getPublicIp`
|
||||
echo "$public_ip $NAME" >> /etc/hosts
|
||||
|
||||
log_it "Applying iptables rule for VNC port ${VNCPORT}"
|
||||
sed -i 's/8080/${VNCPORT}/' /etc/iptables/rules.v4
|
||||
echo "${VNCPORT}" > /root/vncport
|
||||
log_it "Creating VNC port ${VNCPORT} file for VNC server configuration"
|
||||
|
||||
disable_rpfilter
|
||||
enable_fwding 0
|
||||
enable_irqbalance 0
|
||||
|
|
|
|||
|
|
@ -90,7 +90,13 @@ restart_services() {
|
|||
fi
|
||||
done < "$svcfile"
|
||||
if [ "$TYPE" == "consoleproxy" ]; then
|
||||
iptables -A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT
|
||||
vncport=8080
|
||||
if [ -f /root/vncport ]
|
||||
then
|
||||
vncport=`cat /root/vncport`
|
||||
log_it "vncport read: ${vncport}"
|
||||
fi
|
||||
iptables -A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport $vncport -j ACCEPT
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env python
|
||||
# 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.
|
||||
|
||||
from marvin.cloudstackTestCase import *
|
||||
from marvin.lib.utils import *
|
||||
from marvin.lib.base import *
|
||||
from marvin.lib.common import (get_domain,
|
||||
get_zone,
|
||||
get_template)
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
class TestConsoleEndpoint(cloudstackTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.testClient = super(TestConsoleEndpoint, cls).getClsTestClient()
|
||||
cls.apiclient = cls.testClient.getApiClient()
|
||||
cls.domain = get_domain(cls.apiclient)
|
||||
cls.services = cls.testClient.getParsedTestDataConfig()
|
||||
# Get Zone, Domain and templates
|
||||
cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests())
|
||||
cls.hypervisor = cls.testClient.getHypervisorInfo()
|
||||
|
||||
cls.template = get_template(
|
||||
cls.apiclient,
|
||||
cls.zone.id,
|
||||
cls.hypervisor
|
||||
)
|
||||
|
||||
if cls.template == FAILED:
|
||||
assert False, "get_template() failed to return template"
|
||||
|
||||
cls.services["virtual_machine"]["zoneid"] = cls.zone.id
|
||||
|
||||
cls.services["template"] = cls.template.id
|
||||
cls.services["zoneid"] = cls.zone.id
|
||||
|
||||
cls.account = Account.create(
|
||||
cls.apiclient,
|
||||
cls.services["account"],
|
||||
domainid=cls.domain.id
|
||||
)
|
||||
cls.service_offering = ServiceOffering.create(
|
||||
cls.apiclient,
|
||||
cls.services["service_offerings"]["tiny"]
|
||||
)
|
||||
cls.vm1 = VirtualMachine.create(
|
||||
cls.apiclient,
|
||||
cls.services["virtual_machine"],
|
||||
templateid=cls.template.id,
|
||||
accountid=cls.account.name,
|
||||
domainid=cls.account.domainid,
|
||||
serviceofferingid=cls.service_offering.id
|
||||
)
|
||||
|
||||
cls._cleanup = [
|
||||
cls.service_offering,
|
||||
cls.vm1,
|
||||
cls.account
|
||||
]
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
try:
|
||||
cleanup_resources(cls.apiclient, cls._cleanup)
|
||||
except Exception as e:
|
||||
raise Exception("Warning: Exception during cleanup : %s" % e)
|
||||
|
||||
def setUp(self):
|
||||
self.apiclient = self.testClient.getApiClient()
|
||||
self.dbclient = self.testClient.getDbConnection()
|
||||
self.cleanup = []
|
||||
return
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
# Clean up, terminate the created instance, volumes and snapshots
|
||||
cleanup_resources(self.apiclient, self.cleanup)
|
||||
except Exception as e:
|
||||
raise Exception("Warning: Exception during cleanup : %s" % e)
|
||||
return
|
||||
|
||||
@attr(tags=["basic", "advanced"], required_hardware="false")
|
||||
def test_console_endpoint_permissions(self):
|
||||
cmd = createConsoleEndpoint.createConsoleEndpointCmd()
|
||||
cmd.virtualmachineid=self.vm1.id
|
||||
endpoint = self.apiclient.createConsoleEndpoint(cmd)
|
||||
|
||||
if not endpoint:
|
||||
self.fail("Failed to get generate VM console endpoint")
|
||||
|
||||
self.assertTrue(endpoint.success)
|
||||
self.assertNotEqual(len(endpoint.url), 0, "VM console endpoint url was empty")
|
||||
|
||||
account2 = Account.create(
|
||||
self.apiclient,
|
||||
self.services["account2"],
|
||||
domainid=self.domain.id
|
||||
)
|
||||
self.cleanup.append(account2)
|
||||
account2_user = account2.user[0]
|
||||
account2ApiClient = self.testClient.getUserApiClient(account2_user.username, self.domain.name)
|
||||
|
||||
endpoint = account2ApiClient.createConsoleEndpoint(cmd)
|
||||
self.assertFalse(endpoint.success)
|
||||
self.assertTrue(endpoint.url is None)
|
||||
return
|
||||
|
|
@ -201,7 +201,8 @@ known_categories = {
|
|||
'UnmanagedInstance': 'Virtual Machine',
|
||||
'Rolling': 'Rolling Maintenance',
|
||||
'importVsphereStoragePolicies' : 'vSphere storage policies',
|
||||
'listVsphereStoragePolicies' : 'vSphere storage policies'
|
||||
'listVsphereStoragePolicies' : 'vSphere storage policies',
|
||||
'ConsoleEndpoint': 'Console Endpoint'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
"vue-router": "^4.0.14",
|
||||
"vue-web-storage": "^6.1.0",
|
||||
"vue3-clipboard": "^1.0.0",
|
||||
"vue-uuid": "^3.0.0",
|
||||
"vuedraggable": "^4.0.3",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@
|
|||
|
||||
<template>
|
||||
<a
|
||||
v-if="['vm', 'systemvm', 'router', 'ilbvm'].includes($route.meta.name) && 'listVirtualMachines' in $store.getters.apis"
|
||||
:href="server + '/console?cmd=access&vm=' + resource.id"
|
||||
target="_blank">
|
||||
v-if="['vm', 'systemvm', 'router', 'ilbvm'].includes($route.meta.name) && 'listVirtualMachines' in $store.getters.apis && 'createConsoleEndpoint' in $store.getters.apis"
|
||||
@click="consoleUrl">
|
||||
<a-button style="margin-left: 5px" shape="circle" type="dashed" :size="size" :disabled="['Stopped', 'Error', 'Destroyed'].includes(resource.state)" >
|
||||
<code-outlined />
|
||||
</a-button>
|
||||
|
|
@ -28,6 +27,9 @@
|
|||
|
||||
<script>
|
||||
import { SERVER_MANAGER } from '@/store/mutation-types'
|
||||
import { api } from '@/api'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import { uuid } from 'vue-uuid'
|
||||
|
||||
export default {
|
||||
name: 'Console',
|
||||
|
|
@ -41,6 +43,48 @@ export default {
|
|||
default: 'small'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
url: '',
|
||||
tokenValidationEnabled: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TooltipLabel
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
mounted () {
|
||||
this.verifyExtraValidationEnabled()
|
||||
},
|
||||
methods: {
|
||||
verifyExtraValidationEnabled () {
|
||||
api('listConfigurations', { name: 'consoleproxy.extra.security.validation.enabled' }).then(json => {
|
||||
this.tokenValidationEnabled = json.listconfigurationsresponse.configuration !== null && json.listconfigurationsresponse.configuration[0].value === 'true'
|
||||
})
|
||||
},
|
||||
consoleUrl () {
|
||||
const params = {}
|
||||
if (this.tokenValidationEnabled) {
|
||||
params.token = uuid.v4()
|
||||
}
|
||||
params.virtualmachineid = this.resource.id
|
||||
api('createConsoleEndpoint', params).then(json => {
|
||||
this.url = (json && json.createconsoleendpointresponse) ? json.createconsoleendpointresponse.consoleendpoint.url : '#/exception/404'
|
||||
if (json.createconsoleendpointresponse.consoleendpoint.success) {
|
||||
window.open(this.url, '_blank')
|
||||
} else {
|
||||
this.$notification.error({
|
||||
message: this.$t('error.execute.api.failed') + ' ' + 'createConsoleEndpoint',
|
||||
description: json.createconsoleendpointresponse.consoleendpoint.details
|
||||
})
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
server () {
|
||||
if (!this.$config.multipleServer) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.cloudstack.utils.consoleproxy;
|
||||
|
||||
public class ConsoleAccessUtils {
|
||||
|
||||
private ConsoleAccessUtils() {
|
||||
}
|
||||
|
||||
public static final String CLIENT_INET_ADDRESS_KEY = "client-inet-address";
|
||||
}
|
||||
Loading…
Reference in New Issue