123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- /*
- * Copyright 2000-2018 Vaadin Ltd.
- *
- * Licensed 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.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.server.ClientConnector;
- import com.vaadin.server.Constants;
- import com.vaadin.server.JsonCodec;
- import com.vaadin.server.LegacyCommunicationManager;
- 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
- */
- 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.
- * <p>
- * <em>Note:</em> 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;
- }
-
- /**
- * 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
- * @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);
- }
-
- }
-
- /**
- * 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.
- * <p>
- * The invocations data can contain any number of RPC calls, including
- * legacy variable change calls that are processed separately.
- * <p>
- * 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<Connector> enabledConnectors = new HashSet<>();
-
- List<MethodInvocation> invocations = parseInvocations(
- ui.getConnectorTracker(), invocationsData,
- lastSyncIdSeenByClient);
- 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<String, Object> 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<String, Object> 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.
- * @param lastSyncIdSeenByClient
- * the most recent sync id the client has seen at the time the
- * request was sent
- * @return list of MethodInvocation to perform
- */
- private List<MethodInvocation> parseInvocations(
- ConnectorTracker connectorTracker, JsonArray invocationsJson,
- int lastSyncIdSeenByClient) {
- int invocationCount = invocationsJson.length();
- List<MethodInvocation> 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,
- lastSyncIdSeenByClient);
- 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, long lastSyncIdSeenByClient) {
- 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,
- interfaceName, methodName,
- (LegacyChangeVariablesInvocation) previousInvocation,
- parametersJson, connectorTracker);
- } else {
- return parseServerRpcInvocation(connectorId, interfaceName,
- methodName, parametersJson, connectorTracker);
- }
-
- }
-
- private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation(
- String connectorId, String interfaceName, String methodName,
- 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<String, Object> 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 to something
- * ('what') with a connector which is disabled or invisible.
- *
- * @since 7.1.8
- * @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;
- }
- }
|