All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.server.communication.ServerRpcHandler Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * 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 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy