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

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