You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ServerRpcHandler.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.server.communication;
  17. import java.io.IOException;
  18. import java.io.Reader;
  19. import java.io.Serializable;
  20. import java.lang.reflect.Type;
  21. import java.util.ArrayList;
  22. import java.util.HashSet;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.logging.Level;
  27. import java.util.logging.Logger;
  28. import com.vaadin.server.ClientConnector;
  29. import com.vaadin.server.JsonCodec;
  30. import com.vaadin.server.LegacyCommunicationManager;
  31. import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException;
  32. import com.vaadin.server.ServerRpcManager;
  33. import com.vaadin.server.ServerRpcManager.RpcInvocationException;
  34. import com.vaadin.server.ServerRpcMethodInvocation;
  35. import com.vaadin.server.VaadinRequest;
  36. import com.vaadin.server.VaadinService;
  37. import com.vaadin.server.VariableOwner;
  38. import com.vaadin.shared.ApplicationConstants;
  39. import com.vaadin.shared.Connector;
  40. import com.vaadin.shared.communication.LegacyChangeVariablesInvocation;
  41. import com.vaadin.shared.communication.MethodInvocation;
  42. import com.vaadin.shared.communication.ServerRpc;
  43. import com.vaadin.shared.communication.UidlValue;
  44. import com.vaadin.ui.Component;
  45. import com.vaadin.ui.ConnectorTracker;
  46. import com.vaadin.ui.UI;
  47. import elemental.json.JsonArray;
  48. import elemental.json.JsonException;
  49. import elemental.json.JsonObject;
  50. import elemental.json.JsonValue;
  51. import elemental.json.impl.JsonUtil;
  52. /**
  53. * Handles a client-to-server message containing serialized {@link ServerRpc
  54. * server RPC} invocations.
  55. *
  56. * @author Vaadin Ltd
  57. * @since 7.1
  58. */
  59. public class ServerRpcHandler implements Serializable {
  60. /**
  61. * A data transfer object representing an RPC request sent by the client
  62. * side.
  63. *
  64. * @since 7.2
  65. * @author Vaadin Ltd
  66. */
  67. public static class RpcRequest implements Serializable {
  68. private final String csrfToken;
  69. private final JsonArray invocations;
  70. private final int syncId;
  71. private final JsonObject json;
  72. public RpcRequest(String jsonString, VaadinRequest request) {
  73. json = JsonUtil.parse(jsonString);
  74. JsonValue token = json.get(ApplicationConstants.CSRF_TOKEN);
  75. if (token == null) {
  76. this.csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
  77. } else {
  78. String csrfToken = token.asString();
  79. if (csrfToken.equals("")) {
  80. csrfToken = ApplicationConstants.CSRF_TOKEN_DEFAULT_VALUE;
  81. }
  82. this.csrfToken = csrfToken;
  83. }
  84. if (request.getService().getDeploymentConfiguration()
  85. .isSyncIdCheckEnabled()) {
  86. syncId = (int) json.getNumber(ApplicationConstants.SERVER_SYNC_ID);
  87. } else {
  88. syncId = -1;
  89. }
  90. invocations = json.getArray(ApplicationConstants.RPC_INVOCATIONS);
  91. }
  92. /**
  93. * Gets the CSRF security token (double submit cookie) for this request.
  94. *
  95. * @return the CSRF security token for this current change request
  96. */
  97. public String getCsrfToken() {
  98. return csrfToken;
  99. }
  100. /**
  101. * Gets the data to recreate the RPC as requested by the client side.
  102. *
  103. * @return the data describing which RPC should be made, and all their
  104. * data
  105. */
  106. public JsonArray getRpcInvocationsData() {
  107. return invocations;
  108. }
  109. /**
  110. * Gets the sync id last seen by the client.
  111. *
  112. * @return the last sync id given by the server, according to the
  113. * client's request
  114. */
  115. public int getSyncId() {
  116. return syncId;
  117. }
  118. /**
  119. * Gets the entire request in JSON format, as it was received from the
  120. * client.
  121. * <p>
  122. * <em>Note:</em> This is a shared reference - any modifications made
  123. * will be shared.
  124. *
  125. * @return the raw JSON object that was received from the client
  126. *
  127. */
  128. public JsonObject getRawJson() {
  129. return json;
  130. }
  131. }
  132. private static final int MAX_BUFFER_SIZE = 64 * 1024;
  133. /**
  134. * Reads JSON containing zero or more serialized RPC calls (including legacy
  135. * variable changes) and executes the calls.
  136. *
  137. * @param ui
  138. * The {@link UI} receiving the calls. Cannot be null.
  139. * @param reader
  140. * The {@link Reader} used to read the JSON.
  141. * @param request
  142. * @throws IOException
  143. * If reading the message fails.
  144. * @throws InvalidUIDLSecurityKeyException
  145. * If the received security key does not match the one stored in
  146. * the session.
  147. */
  148. public void handleRpc(UI ui, Reader reader, VaadinRequest request)
  149. throws IOException, InvalidUIDLSecurityKeyException {
  150. ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());
  151. String changeMessage = getMessage(reader);
  152. if (changeMessage == null || changeMessage.equals("")) {
  153. // The client sometimes sends empty messages, this is probably a bug
  154. return;
  155. }
  156. RpcRequest rpcRequest = new RpcRequest(changeMessage, request);
  157. // Security: double cookie submission pattern unless disabled by
  158. // property
  159. if (!VaadinService.isCsrfTokenValid(ui.getSession(),
  160. rpcRequest.getCsrfToken())) {
  161. throw new InvalidUIDLSecurityKeyException("");
  162. }
  163. handleInvocations(ui, rpcRequest.getSyncId(),
  164. rpcRequest.getRpcInvocationsData());
  165. ui.getConnectorTracker().cleanConcurrentlyRemovedConnectorIds(
  166. rpcRequest.getSyncId());
  167. }
  168. /**
  169. * Processes invocations data received from the client.
  170. * <p>
  171. * The invocations data can contain any number of RPC calls, including
  172. * legacy variable change calls that are processed separately.
  173. * <p>
  174. * Consecutive changes to the value of the same variable are combined and
  175. * changeVariables() is only called once for them. This preserves the Vaadin
  176. * 6 semantics for components and add-ons that do not use Vaadin 7 RPC
  177. * directly.
  178. *
  179. * @param uI
  180. * the UI receiving the invocations data
  181. * @param lastSyncIdSeenByClient
  182. * the most recent sync id the client has seen at the time the
  183. * request was sent
  184. * @param invocationsData
  185. * JSON containing all information needed to execute all
  186. * requested RPC calls.
  187. */
  188. private void handleInvocations(UI uI, int lastSyncIdSeenByClient,
  189. JsonArray invocationsData) {
  190. // TODO PUSH Refactor so that this is not needed
  191. LegacyCommunicationManager manager = uI.getSession()
  192. .getCommunicationManager();
  193. try {
  194. ConnectorTracker connectorTracker = uI.getConnectorTracker();
  195. Set<Connector> enabledConnectors = new HashSet<Connector>();
  196. List<MethodInvocation> invocations = parseInvocations(
  197. uI.getConnectorTracker(), invocationsData,
  198. lastSyncIdSeenByClient);
  199. for (MethodInvocation invocation : invocations) {
  200. final ClientConnector connector = connectorTracker
  201. .getConnector(invocation.getConnectorId());
  202. if (connector != null && connector.isConnectorEnabled()) {
  203. enabledConnectors.add(connector);
  204. }
  205. }
  206. for (int i = 0; i < invocations.size(); i++) {
  207. MethodInvocation invocation = invocations.get(i);
  208. final ClientConnector connector = connectorTracker
  209. .getConnector(invocation.getConnectorId());
  210. if (connector == null) {
  211. getLogger()
  212. .log(Level.WARNING,
  213. "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})",
  214. new Object[] { invocation.getConnectorId(),
  215. invocation.getInterfaceName(),
  216. invocation.getMethodName() });
  217. continue;
  218. }
  219. if (!enabledConnectors.contains(connector)) {
  220. if (invocation instanceof LegacyChangeVariablesInvocation) {
  221. LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
  222. // TODO convert window close to a separate RPC call and
  223. // handle above - not a variable change
  224. // Handle special case where window-close is called
  225. // after the window has been removed from the
  226. // application or the application has closed
  227. Map<String, Object> changes = legacyInvocation
  228. .getVariableChanges();
  229. if (changes.size() == 1 && changes.containsKey("close")
  230. && Boolean.TRUE.equals(changes.get("close"))) {
  231. // Silently ignore this
  232. continue;
  233. }
  234. }
  235. // Connector is disabled, log a warning and move to the next
  236. getLogger().warning(
  237. getIgnoredDisabledError("RPC call", connector));
  238. continue;
  239. }
  240. // DragAndDropService has null UI
  241. if (connector.getUI() != null && connector.getUI().isClosing()) {
  242. String msg = "Ignoring RPC call for connector "
  243. + connector.getClass().getName();
  244. if (connector instanceof Component) {
  245. String caption = ((Component) connector).getCaption();
  246. if (caption != null) {
  247. msg += ", caption=" + caption;
  248. }
  249. }
  250. msg += " in closed UI";
  251. getLogger().warning(msg);
  252. continue;
  253. }
  254. if (invocation instanceof ServerRpcMethodInvocation) {
  255. try {
  256. ServerRpcManager.applyInvocation(connector,
  257. (ServerRpcMethodInvocation) invocation);
  258. } catch (RpcInvocationException e) {
  259. manager.handleConnectorRelatedException(connector, e);
  260. }
  261. } else {
  262. // All code below is for legacy variable changes
  263. LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation;
  264. Map<String, Object> changes = legacyInvocation
  265. .getVariableChanges();
  266. try {
  267. if (connector instanceof VariableOwner) {
  268. // The source parameter is never used anywhere
  269. changeVariables(null, (VariableOwner) connector,
  270. changes);
  271. } else {
  272. throw new IllegalStateException(
  273. "Received legacy variable change for "
  274. + connector.getClass().getName()
  275. + " ("
  276. + connector.getConnectorId()
  277. + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: "
  278. + changes.keySet());
  279. }
  280. } catch (Exception e) {
  281. manager.handleConnectorRelatedException(connector, e);
  282. }
  283. }
  284. }
  285. } catch (JsonException e) {
  286. getLogger().warning(
  287. "Unable to parse RPC call from the client: "
  288. + e.getMessage());
  289. throw new RuntimeException(e);
  290. }
  291. }
  292. /**
  293. * Parse JSON from the client into a list of MethodInvocation instances.
  294. *
  295. * @param connectorTracker
  296. * The ConnectorTracker used to lookup connectors
  297. * @param invocationsJson
  298. * JSON containing all information needed to execute all
  299. * requested RPC calls.
  300. * @param lastSyncIdSeenByClient
  301. * the most recent sync id the client has seen at the time the
  302. * request was sent
  303. * @return list of MethodInvocation to perform
  304. */
  305. private List<MethodInvocation> parseInvocations(
  306. ConnectorTracker connectorTracker, JsonArray invocationsJson,
  307. int lastSyncIdSeenByClient) {
  308. int invocationCount = invocationsJson.length();
  309. ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(
  310. invocationCount);
  311. MethodInvocation previousInvocation = null;
  312. // parse JSON to MethodInvocations
  313. for (int i = 0; i < invocationCount; ++i) {
  314. JsonArray invocationJson = invocationsJson.getArray(i);
  315. MethodInvocation invocation = parseInvocation(invocationJson,
  316. previousInvocation, connectorTracker,
  317. lastSyncIdSeenByClient);
  318. if (invocation != null) {
  319. // Can be null if the invocation was a legacy invocation and it
  320. // was merged with the previous one or if the invocation was
  321. // rejected because of an error.
  322. invocations.add(invocation);
  323. previousInvocation = invocation;
  324. }
  325. }
  326. return invocations;
  327. }
  328. private MethodInvocation parseInvocation(JsonArray invocationJson,
  329. MethodInvocation previousInvocation,
  330. ConnectorTracker connectorTracker, long lastSyncIdSeenByClient) {
  331. String connectorId = invocationJson.getString(0);
  332. String interfaceName = invocationJson.getString(1);
  333. String methodName = invocationJson.getString(2);
  334. if (connectorTracker.getConnector(connectorId) == null
  335. && !connectorId.equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) {
  336. if (!connectorTracker.connectorWasPresentAsRequestWasSent(
  337. connectorId, lastSyncIdSeenByClient)) {
  338. getLogger()
  339. .log(Level.WARNING,
  340. "RPC call to "
  341. + interfaceName
  342. + "."
  343. + methodName
  344. + " received for connector "
  345. + connectorId
  346. + " but no such connector could be found. Resynchronizing client.");
  347. // This is likely an out of sync issue (client tries to update a
  348. // connector which is not present). Force resync.
  349. connectorTracker.markAllConnectorsDirty();
  350. }
  351. return null;
  352. }
  353. JsonArray parametersJson = invocationJson.getArray(3);
  354. if (LegacyChangeVariablesInvocation.isLegacyVariableChange(
  355. interfaceName, methodName)) {
  356. if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) {
  357. previousInvocation = null;
  358. }
  359. return parseLegacyChangeVariablesInvocation(connectorId,
  360. interfaceName, methodName,
  361. (LegacyChangeVariablesInvocation) previousInvocation,
  362. parametersJson, connectorTracker);
  363. } else {
  364. return parseServerRpcInvocation(connectorId, interfaceName,
  365. methodName, parametersJson, connectorTracker);
  366. }
  367. }
  368. private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation(
  369. String connectorId, String interfaceName, String methodName,
  370. LegacyChangeVariablesInvocation previousInvocation,
  371. JsonArray parametersJson, ConnectorTracker connectorTracker) {
  372. if (parametersJson.length() != 2) {
  373. throw new JsonException(
  374. "Invalid parameters in legacy change variables call. Expected 2, was "
  375. + parametersJson.length());
  376. }
  377. String variableName = parametersJson.getString(0);
  378. UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType(
  379. UidlValue.class, true, parametersJson.get(1), connectorTracker);
  380. Object value = uidlValue.getValue();
  381. if (previousInvocation != null
  382. && previousInvocation.getConnectorId().equals(connectorId)) {
  383. previousInvocation.setVariableChange(variableName, value);
  384. return null;
  385. } else {
  386. return new LegacyChangeVariablesInvocation(connectorId,
  387. variableName, value);
  388. }
  389. }
  390. private ServerRpcMethodInvocation parseServerRpcInvocation(
  391. String connectorId, String interfaceName, String methodName,
  392. JsonArray parametersJson, ConnectorTracker connectorTracker)
  393. throws JsonException {
  394. ClientConnector connector = connectorTracker.getConnector(connectorId);
  395. ServerRpcManager<?> rpcManager = connector.getRpcManager(interfaceName);
  396. if (rpcManager == null) {
  397. /*
  398. * Security: Don't even decode the json parameters if no RpcManager
  399. * corresponding to the received method invocation has been
  400. * registered.
  401. */
  402. getLogger().warning(
  403. "Ignoring RPC call to " + interfaceName + "." + methodName
  404. + " in connector " + connector.getClass().getName()
  405. + "(" + connectorId
  406. + ") as no RPC implementation is registered");
  407. return null;
  408. }
  409. // Use interface from RpcManager instead of loading the class based on
  410. // the string name to avoid problems with OSGi
  411. Class<? extends ServerRpc> rpcInterface = rpcManager.getRpcInterface();
  412. ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(
  413. connectorId, rpcInterface, methodName, parametersJson.length());
  414. Object[] parameters = new Object[parametersJson.length()];
  415. Type[] declaredRpcMethodParameterTypes = invocation.getMethod()
  416. .getGenericParameterTypes();
  417. for (int j = 0; j < parametersJson.length(); ++j) {
  418. JsonValue parameterValue = parametersJson.get(j);
  419. Type parameterType = declaredRpcMethodParameterTypes[j];
  420. parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType,
  421. parameterValue, connectorTracker);
  422. }
  423. invocation.setParameters(parameters);
  424. return invocation;
  425. }
  426. protected void changeVariables(Object source, VariableOwner owner,
  427. Map<String, Object> m) {
  428. owner.changeVariables(source, m);
  429. }
  430. protected String getMessage(Reader reader) throws IOException {
  431. StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
  432. char[] buffer = new char[MAX_BUFFER_SIZE];
  433. while (true) {
  434. int read = reader.read(buffer);
  435. if (read == -1) {
  436. break;
  437. }
  438. sb.append(buffer, 0, read);
  439. }
  440. return sb.toString();
  441. }
  442. private static final Logger getLogger() {
  443. return Logger.getLogger(ServerRpcHandler.class.getName());
  444. }
  445. /**
  446. * Generates an error message when the client is trying to to something
  447. * ('what') with a connector which is disabled or invisible.
  448. *
  449. * @since 7.1.8
  450. * @param connector
  451. * the connector which is disabled (or invisible)
  452. * @return an error message
  453. */
  454. public static String getIgnoredDisabledError(String what,
  455. ClientConnector connector) {
  456. String msg = "Ignoring " + what + " for disabled connector "
  457. + connector.getClass().getName();
  458. if (connector instanceof Component) {
  459. String caption = ((Component) connector).getCaption();
  460. if (caption != null) {
  461. msg += ", caption=" + caption;
  462. }
  463. }
  464. return msg;
  465. }
  466. }