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

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