com.vaadin.server.communication.ServerRpcHandler Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.server.communication;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.vaadin.annotations.PreserveOnRefresh;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.Constants;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException;
import com.vaadin.server.ServerRpcManager;
import com.vaadin.server.ServerRpcManager.RpcInvocationException;
import com.vaadin.server.ServerRpcMethodInvocation;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VariableOwner;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.Connector;
import com.vaadin.shared.Version;
import com.vaadin.shared.communication.LegacyChangeVariablesInvocation;
import com.vaadin.shared.communication.MethodInvocation;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.communication.UidlValue;
import com.vaadin.shared.data.DataRequestRpc;
import com.vaadin.ui.Component;
import com.vaadin.ui.ConnectorTracker;
import com.vaadin.ui.UI;
import elemental.json.JsonArray;
import elemental.json.JsonException;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
import elemental.json.impl.JsonUtil;
/**
* Handles a client-to-server message containing serialized {@link ServerRpc
* server RPC} invocations.
*
* @author Vaadin Ltd
* @since 7.1
*/
@SuppressWarnings("deprecation")
public class ServerRpcHandler implements Serializable {
/**
* A data transfer object representing an RPC request sent by the client
* side.
*
* @since 7.2
* @author Vaadin Ltd
*/
public static class RpcRequest implements Serializable {
private final String csrfToken;
private final JsonArray invocations;
private final int syncId;
private final JsonObject json;
private final boolean resynchronize;
private final int clientToServerMessageId;
private String widgetsetVersion = null;
public RpcRequest(String jsonString, VaadinRequest request) {
json = JsonUtil.parse(jsonString);
JsonValue token = json.get(ApplicationConstants.CSRF_TOKEN);
if (token == null) {
csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
} else {
String csrfToken = token.asString();
if (csrfToken.isEmpty()) {
csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
}
this.csrfToken = csrfToken;
}
if (request.getService().getDeploymentConfiguration()
.isSyncIdCheckEnabled()) {
syncId = (int) json
.getNumber(ApplicationConstants.SERVER_SYNC_ID);
} else {
syncId = -1;
}
if (json.hasKey(ApplicationConstants.RESYNCHRONIZE_ID)) {
resynchronize = json
.getBoolean(ApplicationConstants.RESYNCHRONIZE_ID);
} else {
resynchronize = false;
}
if (json.hasKey(ApplicationConstants.WIDGETSET_VERSION_ID)) {
widgetsetVersion = json
.getString(ApplicationConstants.WIDGETSET_VERSION_ID);
}
if (json.hasKey(ApplicationConstants.CLIENT_TO_SERVER_ID)) {
clientToServerMessageId = (int) json
.getNumber(ApplicationConstants.CLIENT_TO_SERVER_ID);
} else {
getLogger()
.warning("Server message without client id received");
clientToServerMessageId = -1;
}
invocations = json.getArray(ApplicationConstants.RPC_INVOCATIONS);
}
/**
* Gets the CSRF security token (double submit cookie) for this request.
*
* @return the CSRF security token for this current change request
*/
public String getCsrfToken() {
return csrfToken;
}
/**
* Gets the data to recreate the RPC as requested by the client side.
*
* @return the data describing which RPC should be made, and all their
* data
*/
public JsonArray getRpcInvocationsData() {
return invocations;
}
/**
* Gets the sync id last seen by the client.
*
* @return the last sync id given by the server, according to the
* client's request
*/
public int getSyncId() {
return syncId;
}
/**
* Checks if this is a request to resynchronize the client side.
*
* @return true if this is a resynchronization request, false otherwise
*/
public boolean isResynchronize() {
return resynchronize;
}
/**
* Gets the id of the client to server message.
*
* @since 7.6
* @return the server message id
*/
public int getClientToServerId() {
return clientToServerMessageId;
}
/**
* Gets the entire request in JSON format, as it was received from the
* client.
*
* Note: This is a shared reference - any modifications made
* will be shared.
*
* @return the raw JSON object that was received from the client
*
*/
public JsonObject getRawJson() {
return json;
}
/**
* Check if request contains an unload request through Beacon API
*
* @since 8.23
*/
public boolean isUnloadBeaconRequest() {
return json.hasKey(ApplicationConstants.UNLOAD_BEACON);
}
/**
* Gets the widget set version reported by the client.
*
* @since 7.6
* @return The widget set version reported by the client or null if the
* message did not contain a widget set version
*/
public String getWidgetsetVersion() {
return widgetsetVersion;
}
}
private static final int MAX_BUFFER_SIZE = 64 * 1024;
/**
* Reads JSON containing zero or more serialized RPC calls (including legacy
* variable changes) and executes the calls.
*
* @param ui
* The {@link UI} receiving the calls. Cannot be null.
* @param reader
* The {@link Reader} used to read the JSON.
* @param request
* The {@link VaadinRequest} to handle.
* @throws IOException
* If reading the message fails.
* @throws InvalidUIDLSecurityKeyException
* If the received security key does not match the one stored in
* the session.
*/
public void handleRpc(UI ui, Reader reader, VaadinRequest request)
throws IOException, InvalidUIDLSecurityKeyException {
ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());
String changeMessage = getMessage(reader);
if (changeMessage == null || changeMessage.isEmpty()) {
// The client sometimes sends empty messages, this is probably a bug
return;
}
RpcRequest rpcRequest = new RpcRequest(changeMessage, request);
// Security: double cookie submission pattern unless disabled by
// property
if (!VaadinService.isCsrfTokenValid(ui.getSession(),
rpcRequest.getCsrfToken())) {
throw new InvalidUIDLSecurityKeyException("");
}
checkWidgetsetVersion(rpcRequest.getWidgetsetVersion());
int expectedId = ui.getLastProcessedClientToServerId() + 1;
if (rpcRequest.getClientToServerId() != -1
&& rpcRequest.getClientToServerId() != expectedId) {
// Invalid message id, skip RPC processing but force a full
// re-synchronization of the client as it might have not received
// the previous response (e.g. due to a bad connection)
// Must resync also for duplicate messages because the server might
// have generated a response for the first message but the response
// did not reach the client. When the client re-sends the message,
// it would only get an empty response (because the dirty flags have
// been cleared on the server) and would be out of sync
ui.getSession().getCommunicationManager().repaintAll(ui);
if (rpcRequest.getClientToServerId() < expectedId) {
// Just a duplicate message due to a bad connection or similar
// It has already been handled by the server so it is safe to
// ignore
getLogger()
.fine("Ignoring old message from the client. Expected: "
+ expectedId + ", got: "
+ rpcRequest.getClientToServerId());
} else {
getLogger().warning(
"Unexpected message id from the client. Expected: "
+ expectedId + ", got: "
+ rpcRequest.getClientToServerId());
}
} else {
// Message id ok, process RPCs
ui.setLastProcessedClientToServerId(expectedId);
handleInvocations(ui, rpcRequest.getSyncId(),
rpcRequest.getRpcInvocationsData());
}
if (rpcRequest.isResynchronize()) {
ui.getSession().getCommunicationManager().repaintAll(ui);
getLogger().warning("Resynchronizing UI by client's request. "
+ "A network message was lost before reaching the client and the client is reloading the full UI state. "
+ "This typically happens because of a bad network connection with packet loss or because of some part of"
+ " the network infrastructure (load balancer, proxy) terminating a push (websocket or long-polling) connection."
+ " If you are using push with a proxy, make sure the push timeout is set to be smaller than the proxy connection timeout");
}
if (rpcRequest.isUnloadBeaconRequest()) {
if (isPreserveOnRefreshTarget(ui)) {
getLogger().finer(
"Eager UI close ignored for @PreserveOnRefresh view");
} else {
ui.close();
getLogger().finer("UI closed with a beacon request");
}
}
}
private static boolean isPreserveOnRefreshTarget(final UI ui) {
return ui.getClass().isAnnotationPresent(PreserveOnRefresh.class);
}
/**
* Checks that the version reported by the client (widgetset) matches that
* of the server.
*
* @param widgetsetVersion
* the widget set version reported by the client or null
*/
private void checkWidgetsetVersion(String widgetsetVersion) {
if (widgetsetVersion == null) {
// Only check when the widgetset version is reported. It is reported
// in the first UIDL request (not the initial request as it is a
// plain GET /)
return;
}
if (!Version.getFullVersion().equals(widgetsetVersion)) {
getLogger().warning(String.format(Constants.WIDGETSET_MISMATCH_INFO,
Version.getFullVersion(), widgetsetVersion));
}
}
/**
* Processes invocations data received from the client.
*
* The invocations data can contain any number of RPC calls, including
* legacy variable change calls that are processed separately.
*
* Consecutive changes to the value of the same variable are combined and
* changeVariables() is only called once for them. This preserves the Vaadin
* 6 semantics for components and add-ons that do not use Vaadin 7 RPC
* directly.
*
* @param ui
* the UI receiving the invocations data
* @param lastSyncIdSeenByClient
* the most recent sync id the client has seen at the time the
* request was sent
* @param invocationsData
* JSON containing all information needed to execute all
* requested RPC calls.
* @since 7.7
*/
protected void handleInvocations(UI ui, int lastSyncIdSeenByClient,
JsonArray invocationsData) {
try {
ConnectorTracker connectorTracker = ui.getConnectorTracker();
Set enabledConnectors = new HashSet<>();
List invocations = parseInvocations(
ui.getConnectorTracker(), invocationsData);
for (MethodInvocation invocation : invocations) {
final ClientConnector connector = connectorTracker
.getConnector(invocation.getConnectorId());
if (connector != null && connector.isConnectorEnabled()) {
enabledConnectors.add(connector);
}
}
for (MethodInvocation invocation : invocations) {
final ClientConnector connector = connectorTracker
.getConnector(invocation.getConnectorId());
if (connector == null) {
logUnknownConnector(invocation.getConnectorId(),
invocation.getInterfaceName(),
invocation.getMethodName());
continue;
}
if (!enabledConnectors.contains(connector)) {
if (invocation instanceof LegacyChangeVariablesInvocation) {
LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
// TODO convert window close to a separate RPC call and
// handle above - not a variable change
// Handle special case where window-close is called
// after the window has been removed from the
// application or the application has closed
Map changes = legacyInvocation
.getVariableChanges();
if (changes.size() == 1 && changes.containsKey("close")
&& Boolean.TRUE.equals(changes.get("close"))) {
// Silently ignore this
continue;
}
} else if (invocation instanceof ServerRpcMethodInvocation) {
ServerRpcMethodInvocation rpc = (ServerRpcMethodInvocation) invocation;
// special case for data communicator requesting more
// data
if (DataRequestRpc.class.getName()
.equals(rpc.getInterfaceClass().getName())) {
handleInvocation(ui, connector, rpc);
}
continue;
}
// Connector is disabled, log a warning and move to the next
getLogger().warning(
getIgnoredDisabledError("RPC call", connector));
continue;
}
// DragAndDropService has null UI
if (connector.getUI() != null
&& connector.getUI().isClosing()) {
String msg = "Ignoring RPC call for connector "
+ connector.getClass().getName();
if (connector instanceof Component) {
String caption = ((Component) connector).getCaption();
if (caption != null) {
msg += ", caption=" + caption;
}
}
msg += " in closed UI";
getLogger().warning(msg);
continue;
}
if (invocation instanceof ServerRpcMethodInvocation) {
handleInvocation(ui, connector,
(ServerRpcMethodInvocation) invocation);
} else {
LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
handleInvocation(ui, connector, legacyInvocation);
}
}
} catch (JsonException e) {
getLogger().warning("Unable to parse RPC call from the client: "
+ e.getMessage());
throw new RuntimeException(e);
}
}
private void logUnknownConnector(String connectorId, String interfaceName,
String methodName) {
getLogger().log(Level.FINE,
"Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})",
new Object[] { connectorId, interfaceName, methodName });
}
/**
* Handles the given RPC method invocation for the given connector.
*
* @since 7.7
* @param ui
* the UI containing the connector
* @param connector
* the connector the RPC is targeted to
* @param invocation
* information about the rpc to invoke
*/
protected void handleInvocation(UI ui, ClientConnector connector,
ServerRpcMethodInvocation invocation) {
try {
ServerRpcManager.applyInvocation(connector, invocation);
} catch (RpcInvocationException e) {
ui.getSession().getCommunicationManager()
.handleConnectorRelatedException(connector, e);
}
}
/**
* Handles the given Legacy variable change RPC method invocation for the
* given connector.
*
* @since 7.7
* @param ui
* the UI containing the connector
* @param connector
* the connector the RPC is targeted to
* @param legacyInvocation
* information about the rpc to invoke
*/
protected void handleInvocation(UI ui, ClientConnector connector,
LegacyChangeVariablesInvocation legacyInvocation) {
Map changes = legacyInvocation.getVariableChanges();
try {
if (connector instanceof VariableOwner) {
// The source parameter is never used anywhere
changeVariables(null, (VariableOwner) connector, changes);
} else {
throw new IllegalStateException(
"Received a legacy variable change for "
+ connector.getClass().getName() + " ("
+ connector.getConnectorId()
+ ") which is not a VariableOwner. The client-side connector sent these legacy variables: "
+ changes.keySet());
}
} catch (Exception e) {
ui.getSession().getCommunicationManager()
.handleConnectorRelatedException(connector, e);
}
}
/**
* Parse JSON from the client into a list of MethodInvocation instances.
*
* @param connectorTracker
* The ConnectorTracker used to lookup connectors
* @param invocationsJson
* JSON containing all information needed to execute all
* requested RPC calls.
* @return list of MethodInvocation to perform
*/
private List parseInvocations(
ConnectorTracker connectorTracker, JsonArray invocationsJson) {
int invocationCount = invocationsJson.length();
List invocations = new ArrayList<>(invocationCount);
MethodInvocation previousInvocation = null;
// parse JSON to MethodInvocations
for (int i = 0; i < invocationCount; ++i) {
JsonArray invocationJson = invocationsJson.getArray(i);
MethodInvocation invocation = parseInvocation(invocationJson,
previousInvocation, connectorTracker);
if (invocation != null) {
// Can be null if the invocation was a legacy invocation and it
// was merged with the previous one or if the invocation was
// rejected because of an error.
invocations.add(invocation);
previousInvocation = invocation;
}
}
return invocations;
}
private MethodInvocation parseInvocation(JsonArray invocationJson,
MethodInvocation previousInvocation,
ConnectorTracker connectorTracker) {
String connectorId = invocationJson.getString(0);
String interfaceName = invocationJson.getString(1);
String methodName = invocationJson.getString(2);
JsonArray parametersJson = invocationJson.getArray(3);
if (LegacyChangeVariablesInvocation
.isLegacyVariableChange(interfaceName, methodName)) {
if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) {
previousInvocation = null;
}
return parseLegacyChangeVariablesInvocation(connectorId,
(LegacyChangeVariablesInvocation) previousInvocation,
parametersJson, connectorTracker);
} else {
return parseServerRpcInvocation(connectorId, interfaceName,
methodName, parametersJson, connectorTracker);
}
}
private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation(
String connectorId,
LegacyChangeVariablesInvocation previousInvocation,
JsonArray parametersJson, ConnectorTracker connectorTracker) {
if (parametersJson.length() != 2) {
throw new JsonException(
"Invalid parameters in legacy change variables call. Expected 2, was "
+ parametersJson.length());
}
String variableName = parametersJson.getString(0);
UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType(
UidlValue.class, true, parametersJson.get(1), connectorTracker);
Object value = uidlValue.getValue();
if (previousInvocation != null
&& previousInvocation.getConnectorId().equals(connectorId)) {
previousInvocation.setVariableChange(variableName, value);
return null;
} else {
return new LegacyChangeVariablesInvocation(connectorId,
variableName, value);
}
}
private ServerRpcMethodInvocation parseServerRpcInvocation(
String connectorId, String interfaceName, String methodName,
JsonArray parametersJson, ConnectorTracker connectorTracker)
throws JsonException {
ClientConnector connector = connectorTracker.getConnector(connectorId);
if (connector == null) {
logUnknownConnector(connectorId, interfaceName, methodName);
return null;
}
ServerRpcManager> rpcManager = connector.getRpcManager(interfaceName);
if (rpcManager == null) {
/*
* Security: Don't even decode the json parameters if no RpcManager
* corresponding to the received method invocation has been
* registered.
*/
String message = "Ignoring RPC call to " + interfaceName + "."
+ methodName + " in connector "
+ connector.getClass().getName() + "(" + connectorId
+ ") as no RPC implementation is registered";
assert rpcManager != null : message;
getLogger().warning(message);
return null;
}
// Use interface from RpcManager instead of loading the class based on
// the string name to avoid problems with OSGi
Class extends ServerRpc> rpcInterface = rpcManager.getRpcInterface();
ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(
connectorId, rpcInterface, methodName, parametersJson.length());
Object[] parameters = new Object[parametersJson.length()];
Type[] declaredRpcMethodParameterTypes = invocation.getMethod()
.getGenericParameterTypes();
for (int j = 0; j < parametersJson.length(); ++j) {
JsonValue parameterValue = parametersJson.get(j);
Type parameterType = declaredRpcMethodParameterTypes[j];
parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType,
parameterValue, connectorTracker);
}
invocation.setParameters(parameters);
return invocation;
}
protected void changeVariables(Object source, VariableOwner owner,
Map m) {
owner.changeVariables(source, m);
}
protected String getMessage(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
char[] buffer = new char[MAX_BUFFER_SIZE];
while (true) {
int read = reader.read(buffer);
if (read == -1) {
break;
}
sb.append(buffer, 0, read);
}
return sb.toString();
}
private static final Logger getLogger() {
return Logger.getLogger(ServerRpcHandler.class.getName());
}
/**
* Generates an error message when the client is trying to do something
* ('what') with a connector which is disabled or invisible.
*
* @since 7.1.8
* @param what
* the ignored operation
* @param connector
* the connector which is disabled (or invisible)
* @return an error message
*/
public static String getIgnoredDisabledError(String what,
ClientConnector connector) {
String msg = "Ignoring " + what + " for disabled connector "
+ connector.getClass().getName();
if (connector instanceof Component) {
String caption = ((Component) connector).getCaption();
if (caption != null) {
msg += ", caption=" + caption;
}
}
return msg;
}
}