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 20KB

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