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.

PushHandler.java 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /*
  2. * Copyright 2000-2018 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.util.Collection;
  20. import java.util.logging.Level;
  21. import java.util.logging.Logger;
  22. import org.atmosphere.cpr.AtmosphereRequest;
  23. import org.atmosphere.cpr.AtmosphereResource;
  24. import org.atmosphere.cpr.AtmosphereResource.TRANSPORT;
  25. import org.atmosphere.cpr.AtmosphereResourceEvent;
  26. import org.atmosphere.cpr.AtmosphereResourceImpl;
  27. import com.vaadin.server.ErrorEvent;
  28. import com.vaadin.server.ErrorHandler;
  29. import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException;
  30. import com.vaadin.server.ServiceException;
  31. import com.vaadin.server.ServletPortletHelper;
  32. import com.vaadin.server.SessionExpiredException;
  33. import com.vaadin.server.SystemMessages;
  34. import com.vaadin.server.VaadinRequest;
  35. import com.vaadin.server.VaadinService;
  36. import com.vaadin.server.VaadinServletRequest;
  37. import com.vaadin.server.VaadinServletService;
  38. import com.vaadin.server.VaadinSession;
  39. import com.vaadin.shared.ApplicationConstants;
  40. import com.vaadin.shared.JsonConstants;
  41. import com.vaadin.shared.communication.PushMode;
  42. import com.vaadin.ui.UI;
  43. import com.vaadin.util.CurrentInstance;
  44. import elemental.json.JsonException;
  45. /**
  46. * Handles incoming push connections and messages and dispatches them to the
  47. * correct {@link UI}/ {@link AtmospherePushConnection}.
  48. *
  49. * @author Vaadin Ltd
  50. * @since 7.1
  51. */
  52. public class PushHandler {
  53. private int longPollingSuspendTimeout = -1;
  54. private final ServerRpcHandler rpcHandler = createRpcHandler();
  55. /**
  56. * Callback interface used internally to process an event with the
  57. * corresponding UI properly locked.
  58. */
  59. private interface PushEventCallback {
  60. public void run(AtmosphereResource resource, UI ui) throws IOException;
  61. }
  62. /**
  63. * Callback used when we receive a request to establish a push channel for a
  64. * UI. Associate the AtmosphereResource with the UI and leave the connection
  65. * open by calling resource.suspend(). If there is a pending push, send it
  66. * now.
  67. */
  68. private final PushEventCallback establishCallback = (
  69. AtmosphereResource resource, UI ui) -> {
  70. getLogger().log(Level.FINER,
  71. "New push connection for resource {0} with transport {1}",
  72. new Object[] { resource.uuid(), resource.transport() });
  73. resource.getResponse().setContentType("text/plain; charset=UTF-8");
  74. VaadinSession session = ui.getSession();
  75. if (resource.transport() == TRANSPORT.STREAMING) {
  76. // Must ensure that the streaming response contains
  77. // "Connection: close", otherwise iOS 6 will wait for the
  78. // response to this request before sending another request to
  79. // the same server (as it will apparently try to reuse the same
  80. // connection)
  81. resource.getResponse().addHeader("Connection", "close");
  82. }
  83. String requestToken = resource.getRequest()
  84. .getParameter(ApplicationConstants.PUSH_ID_PARAMETER);
  85. if (!isPushIdValid(session, requestToken)) {
  86. getLogger().log(Level.WARNING,
  87. "Invalid identifier in new connection received from {0}",
  88. resource.getRequest().getRemoteHost());
  89. // Refresh on client side, create connection just for
  90. // sending a message
  91. sendRefreshAndDisconnect(resource);
  92. return;
  93. }
  94. suspend(resource);
  95. AtmospherePushConnection connection = getConnectionForUI(ui);
  96. assert (connection != null);
  97. connection.connect(resource);
  98. };
  99. /**
  100. * Callback used when we receive a UIDL request through Atmosphere. If the
  101. * push channel is bidirectional (websockets), the request was sent via the
  102. * same channel. Otherwise, the client used a separate AJAX request. Handle
  103. * the request and send changed UI state via the push channel (we do not
  104. * respond to the request directly.)
  105. */
  106. private final PushEventCallback receiveCallback = (
  107. AtmosphereResource resource, UI ui) -> {
  108. getLogger().log(Level.FINER, "Received message from resource {0}",
  109. resource.uuid());
  110. AtmosphereRequest req = resource.getRequest();
  111. AtmospherePushConnection connection = getConnectionForUI(ui);
  112. assert connection != null : "Got push from the client "
  113. + "even though the connection does not seem to be "
  114. + "valid. This might happen if a HttpSession is "
  115. + "serialized and deserialized while the push "
  116. + "connection is kept open or if the UI has a "
  117. + "connection of unexpected type.";
  118. Reader reader = connection.receiveMessage(req.getReader());
  119. if (reader == null) {
  120. // The whole message was not yet received
  121. return;
  122. }
  123. // Should be set up by caller
  124. VaadinRequest vaadinRequest = VaadinService.getCurrentRequest();
  125. assert vaadinRequest != null;
  126. try {
  127. rpcHandler.handleRpc(ui, reader, vaadinRequest);
  128. connection.push(false);
  129. } catch (JsonException e) {
  130. getLogger().log(Level.SEVERE, "Error writing JSON to response", e);
  131. // Refresh on client side
  132. sendRefreshAndDisconnect(resource);
  133. } catch (InvalidUIDLSecurityKeyException e) {
  134. getLogger().log(Level.WARNING,
  135. "Invalid security key received from {0}",
  136. resource.getRequest().getRemoteHost());
  137. // Refresh on client side
  138. sendRefreshAndDisconnect(resource);
  139. }
  140. };
  141. private final VaadinServletService service;
  142. public PushHandler(VaadinServletService service) {
  143. this.service = service;
  144. }
  145. /**
  146. * Creates the ServerRpcHandler to use.
  147. *
  148. * @return the ServerRpcHandler to use
  149. * @since 8.5
  150. */
  151. protected ServerRpcHandler createRpcHandler() {
  152. return new ServerRpcHandler();
  153. }
  154. /**
  155. * Suspends the given resource.
  156. *
  157. * @since 7.6
  158. * @param resource
  159. * the resource to suspend
  160. */
  161. protected void suspend(AtmosphereResource resource) {
  162. if (resource.transport() == TRANSPORT.LONG_POLLING) {
  163. resource.suspend(getLongPollingSuspendTimeout());
  164. } else {
  165. resource.suspend(-1);
  166. }
  167. }
  168. /**
  169. * Find the UI for the atmosphere resource, lock it and invoke the callback.
  170. *
  171. * @param resource
  172. * the atmosphere resource for the current request
  173. * @param callback
  174. * the push callback to call when a UI is found and locked
  175. */
  176. private void callWithUi(final AtmosphereResource resource,
  177. final PushEventCallback callback) {
  178. AtmosphereRequest req = resource.getRequest();
  179. VaadinServletRequest vaadinRequest = new VaadinServletRequest(req,
  180. service);
  181. VaadinSession session = null;
  182. boolean isWebsocket = resource.transport() == TRANSPORT.WEBSOCKET;
  183. if (isWebsocket) {
  184. // For any HTTP request we have already started the request in the
  185. // servlet
  186. service.requestStart(vaadinRequest, null);
  187. }
  188. try {
  189. try {
  190. session = service.findVaadinSession(vaadinRequest);
  191. assert VaadinSession.getCurrent() == session;
  192. } catch (ServiceException e) {
  193. getLogger().log(Level.SEVERE,
  194. "Could not get session. This should never happen", e);
  195. return;
  196. } catch (SessionExpiredException e) {
  197. SystemMessages msg = service
  198. .getSystemMessages(ServletPortletHelper.findLocale(null,
  199. null, vaadinRequest), vaadinRequest);
  200. sendNotificationAndDisconnect(resource,
  201. VaadinService.createCriticalNotificationJSON(
  202. msg.getSessionExpiredCaption(),
  203. msg.getSessionExpiredMessage(), null,
  204. msg.getSessionExpiredURL()));
  205. return;
  206. }
  207. UI ui = null;
  208. session.lock();
  209. try {
  210. ui = service.findUI(vaadinRequest);
  211. assert UI.getCurrent() == ui;
  212. if (ui == null) {
  213. sendNotificationAndDisconnect(resource, UidlRequestHandler
  214. .getUINotFoundErrorJSON(service, vaadinRequest));
  215. } else {
  216. callback.run(resource, ui);
  217. }
  218. } catch (final IOException e) {
  219. callErrorHandler(session, e);
  220. } catch (final Exception e) {
  221. SystemMessages msg = service
  222. .getSystemMessages(ServletPortletHelper.findLocale(null,
  223. null, vaadinRequest), vaadinRequest);
  224. AtmosphereResource errorResource = resource;
  225. if (ui != null && ui.getPushConnection() != null) {
  226. // We MUST use the opened push connection if there is one.
  227. // Otherwise we will write the response to the wrong request
  228. // when using streaming (the client -> server request
  229. // instead of the opened push channel)
  230. errorResource = ((AtmospherePushConnection) ui
  231. .getPushConnection()).getResource();
  232. }
  233. sendNotificationAndDisconnect(errorResource,
  234. VaadinService.createCriticalNotificationJSON(
  235. msg.getInternalErrorCaption(),
  236. msg.getInternalErrorMessage(), null,
  237. msg.getInternalErrorURL()));
  238. callErrorHandler(session, e);
  239. } finally {
  240. try {
  241. session.unlock();
  242. } catch (Exception e) {
  243. getLogger().log(Level.WARNING,
  244. "Error while unlocking session", e);
  245. // can't call ErrorHandler, we (hopefully) don't have a lock
  246. }
  247. }
  248. } finally {
  249. try {
  250. if (isWebsocket) {
  251. service.requestEnd(vaadinRequest, null, session);
  252. }
  253. } catch (Exception e) {
  254. getLogger().log(Level.WARNING, "Error while ending request", e);
  255. // can't call ErrorHandler, we don't have a lock
  256. }
  257. }
  258. }
  259. /**
  260. * Call the session's {@link ErrorHandler}, if it has one, with the given
  261. * exception wrapped in an {@link ErrorEvent}.
  262. */
  263. private void callErrorHandler(VaadinSession session, Exception e) {
  264. try {
  265. ErrorHandler errorHandler = ErrorEvent.findErrorHandler(session);
  266. if (errorHandler != null) {
  267. errorHandler.error(new ErrorEvent(e));
  268. }
  269. } catch (Exception ex) {
  270. // Let's not allow error handling to cause trouble; log fails
  271. getLogger().log(Level.WARNING, "ErrorHandler call failed", ex);
  272. }
  273. }
  274. private static AtmospherePushConnection getConnectionForUI(UI ui) {
  275. PushConnection pushConnection = ui.getPushConnection();
  276. if (pushConnection instanceof AtmospherePushConnection) {
  277. return (AtmospherePushConnection) pushConnection;
  278. } else {
  279. return null;
  280. }
  281. }
  282. void connectionLost(AtmosphereResourceEvent event) {
  283. VaadinSession session = null;
  284. try {
  285. session = handleConnectionLost(event);
  286. } finally {
  287. if (session != null) {
  288. session.access(new Runnable() {
  289. @Override
  290. public void run() {
  291. CurrentInstance.clearAll();
  292. }
  293. });
  294. }
  295. }
  296. }
  297. private VaadinSession handleConnectionLost(AtmosphereResourceEvent event) {
  298. // We don't want to use callWithUi here, as it assumes there's a client
  299. // request active and does requestStart and requestEnd among other
  300. // things.
  301. if (event == null) {
  302. getLogger().log(Level.SEVERE,
  303. "Could not get event. This should never happen.");
  304. return null;
  305. }
  306. AtmosphereResource resource = event.getResource();
  307. if (resource == null) {
  308. getLogger().log(Level.SEVERE,
  309. "Could not get resource. This should never happen.");
  310. return null;
  311. }
  312. VaadinServletRequest vaadinRequest = new VaadinServletRequest(
  313. resource.getRequest(), service);
  314. VaadinSession session = null;
  315. try {
  316. session = service.findVaadinSession(vaadinRequest);
  317. } catch (ServiceException e) {
  318. getLogger().log(Level.SEVERE,
  319. "Could not get session. This should never happen", e);
  320. return null;
  321. } catch (SessionExpiredException e) {
  322. // This happens at least if the server is restarted without
  323. // preserving the session. After restart the client reconnects, gets
  324. // a session expired notification and then closes the connection and
  325. // ends up here
  326. getLogger().log(Level.FINER,
  327. "Session expired before push disconnect event was received",
  328. e);
  329. return session;
  330. }
  331. UI ui = null;
  332. session.lock();
  333. try {
  334. VaadinSession.setCurrent(session);
  335. // Sets UI.currentInstance
  336. ui = service.findUI(vaadinRequest);
  337. if (ui == null) {
  338. /*
  339. * UI not found, could be because FF has asynchronously closed
  340. * the websocket connection and Atmosphere has already done
  341. * cleanup of the request attributes. In that case, we still
  342. * have a chance of finding the right UI by iterating through
  343. * the UIs in the session looking for one using the same
  344. * AtmosphereResource.
  345. */
  346. ui = findUiUsingResource(resource, session.getUIs());
  347. if (ui == null) {
  348. getLogger().log(Level.FINE,
  349. "Could not get UI. This should never happen,"
  350. + " except when reloading in Firefox and Chrome -"
  351. + " see https://github.com/vaadin/framework/issues/5449.");
  352. return session;
  353. } else {
  354. getLogger().log(Level.INFO,
  355. "No UI was found based on data in the request,"
  356. + " but a slower lookup based on the AtmosphereResource succeeded."
  357. + " See https://github.com/vaadin/framework/issues/5449 for more details.");
  358. }
  359. }
  360. PushMode pushMode = ui.getPushConfiguration().getPushMode();
  361. AtmospherePushConnection pushConnection = getConnectionForUI(ui);
  362. String id = resource.uuid();
  363. if (pushConnection == null) {
  364. getLogger().log(Level.WARNING,
  365. "Could not find push connection to close: {0} with transport {1}",
  366. new Object[] { id, resource.transport() });
  367. } else {
  368. if (!pushMode.isEnabled()) {
  369. /*
  370. * The client is expected to close the connection after push
  371. * mode has been set to disabled.
  372. */
  373. getLogger().log(Level.FINER,
  374. "Connection closed for resource {0}", id);
  375. } else {
  376. /*
  377. * Unexpected cancel, e.g. if the user closes the browser
  378. * tab.
  379. */
  380. getLogger().log(Level.FINER,
  381. "Connection unexpectedly closed for resource {0} with transport {1}",
  382. new Object[] { id, resource.transport() });
  383. }
  384. pushConnection.connectionLost();
  385. }
  386. } catch (final Exception e) {
  387. callErrorHandler(session, e);
  388. } finally {
  389. try {
  390. session.unlock();
  391. } catch (Exception e) {
  392. getLogger().log(Level.WARNING, "Error while unlocking session",
  393. e);
  394. // can't call ErrorHandler, we (hopefully) don't have a lock
  395. }
  396. }
  397. return session;
  398. }
  399. private static UI findUiUsingResource(AtmosphereResource resource,
  400. Collection<UI> uIs) {
  401. for (UI ui : uIs) {
  402. PushConnection pushConnection = ui.getPushConnection();
  403. if (pushConnection instanceof AtmospherePushConnection) {
  404. if (((AtmospherePushConnection) pushConnection)
  405. .getResource() == resource) {
  406. return ui;
  407. }
  408. }
  409. }
  410. return null;
  411. }
  412. /**
  413. * Sends a refresh message to the given atmosphere resource. Uses an
  414. * AtmosphereResource instead of an AtmospherePushConnection even though it
  415. * might be possible to look up the AtmospherePushConnection from the UI to
  416. * ensure border cases work correctly, especially when there temporarily are
  417. * two push connections which try to use the same UI. Using the
  418. * AtmosphereResource directly guarantees the message goes to the correct
  419. * recipient.
  420. *
  421. * @param resource
  422. * The atmosphere resource to send refresh to
  423. *
  424. */
  425. private static void sendRefreshAndDisconnect(AtmosphereResource resource)
  426. throws IOException {
  427. sendNotificationAndDisconnect(resource, VaadinService
  428. .createCriticalNotificationJSON(null, null, null, null));
  429. }
  430. /**
  431. * Tries to send a critical notification to the client and close the
  432. * connection. Does nothing if the connection is already closed.
  433. */
  434. private static void sendNotificationAndDisconnect(
  435. AtmosphereResource resource, String notificationJson) {
  436. // TODO Implemented differently from sendRefreshAndDisconnect
  437. try {
  438. if (resource instanceof AtmosphereResourceImpl
  439. && !((AtmosphereResourceImpl) resource).isInScope()) {
  440. // The resource is no longer valid so we should not write
  441. // anything to it
  442. getLogger().fine(
  443. "sendNotificationAndDisconnect called for resource no longer in scope");
  444. return;
  445. }
  446. resource.getResponse()
  447. .setContentType(JsonConstants.JSON_CONTENT_TYPE);
  448. resource.getResponse().getWriter().write(notificationJson);
  449. resource.resume();
  450. } catch (Exception e) {
  451. getLogger().log(Level.FINEST,
  452. "Failed to send critical notification to client", e);
  453. }
  454. }
  455. private static final Logger getLogger() {
  456. return Logger.getLogger(PushHandler.class.getName());
  457. }
  458. /**
  459. * Checks whether a given push id matches the session's push id.
  460. *
  461. * @param session
  462. * the vaadin session for which the check should be done
  463. * @param requestPushId
  464. * the push id provided in the request
  465. * @return {@code true} if the id is valid, {@code false} otherwise
  466. */
  467. private static boolean isPushIdValid(VaadinSession session,
  468. String requestPushId) {
  469. String sessionPushId = session.getPushId();
  470. if (requestPushId == null || !requestPushId.equals(sessionPushId)) {
  471. return false;
  472. }
  473. return true;
  474. }
  475. /**
  476. * Called when a new push connection is requested to be opened by the client
  477. *
  478. * @since 7.5.0
  479. * @param resource
  480. * The related atmosphere resources
  481. */
  482. void onConnect(AtmosphereResource resource) {
  483. callWithUi(resource, establishCallback);
  484. }
  485. /**
  486. * Called when a message is received through the push connection
  487. *
  488. * @since 7.5.0
  489. * @param resource
  490. * The related atmosphere resources
  491. */
  492. void onMessage(AtmosphereResource resource) {
  493. callWithUi(resource, receiveCallback);
  494. }
  495. /**
  496. * Sets the timeout used for suspend calls when using long polling.
  497. *
  498. * If you are using a proxy with a defined idle timeout, set the suspend
  499. * timeout to a value smaller than the proxy timeout so that the server is
  500. * aware of a reconnect taking place.
  501. *
  502. * @since 7.6
  503. * @param longPollingSuspendTimeout
  504. * the timeout to use for suspended AtmosphereResources
  505. */
  506. public void setLongPollingSuspendTimeout(int longPollingSuspendTimeout) {
  507. this.longPollingSuspendTimeout = longPollingSuspendTimeout;
  508. }
  509. /**
  510. * Gets the timeout used for suspend calls when using long polling.
  511. *
  512. * @since 7.6
  513. * @return the timeout to use for suspended AtmosphereResources
  514. */
  515. public int getLongPollingSuspendTimeout() {
  516. return longPollingSuspendTimeout;
  517. }
  518. }