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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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. if(event == null){
  277. getLogger().log(Level.SEVERE,
  278. "Could not get event. This should never happen.");
  279. return;
  280. }
  281. AtmosphereResource resource = event.getResource();
  282. if(resource == null){
  283. getLogger().log(Level.SEVERE,
  284. "Could not get resource. This should never happen.");
  285. return;
  286. }
  287. VaadinServletRequest vaadinRequest = new VaadinServletRequest(
  288. resource.getRequest(), service);
  289. VaadinSession session = null;
  290. try {
  291. session = service.findVaadinSession(vaadinRequest);
  292. } catch (ServiceException e) {
  293. getLogger().log(Level.SEVERE,
  294. "Could not get session. This should never happen", e);
  295. return;
  296. } catch (SessionExpiredException e) {
  297. // This happens at least if the server is restarted without
  298. // preserving the session. After restart the client reconnects, gets
  299. // a session expired notification and then closes the connection and
  300. // ends up here
  301. getLogger().log(Level.FINER,
  302. "Session expired before push disconnect event was received",
  303. e);
  304. return;
  305. }
  306. UI ui = null;
  307. session.lock();
  308. try {
  309. VaadinSession.setCurrent(session);
  310. // Sets UI.currentInstance
  311. ui = service.findUI(vaadinRequest);
  312. if (ui == null) {
  313. /*
  314. * UI not found, could be because FF has asynchronously closed
  315. * the websocket connection and Atmosphere has already done
  316. * cleanup of the request attributes.
  317. *
  318. * In that case, we still have a chance of finding the right UI
  319. * by iterating through the UIs in the session looking for one
  320. * using the same AtmosphereResource.
  321. */
  322. ui = findUiUsingResource(resource, session.getUIs());
  323. if (ui == null) {
  324. getLogger().log(Level.FINE,
  325. "Could not get UI. This should never happen,"
  326. + " except when reloading in Firefox and Chrome -"
  327. + " see http://dev.vaadin.com/ticket/14251.");
  328. return;
  329. } else {
  330. getLogger().log(Level.INFO,
  331. "No UI was found based on data in the request,"
  332. + " but a slower lookup based on the AtmosphereResource succeeded."
  333. + " See http://dev.vaadin.com/ticket/14251 for more details.");
  334. }
  335. }
  336. PushMode pushMode = ui.getPushConfiguration().getPushMode();
  337. AtmospherePushConnection pushConnection = getConnectionForUI(ui);
  338. String id = resource.uuid();
  339. if (pushConnection == null) {
  340. getLogger().log(Level.WARNING,
  341. "Could not find push connection to close: {0} with transport {1}",
  342. new Object[] { id, resource.transport() });
  343. } else {
  344. if (!pushMode.isEnabled()) {
  345. /*
  346. * The client is expected to close the connection after push
  347. * mode has been set to disabled.
  348. */
  349. getLogger().log(Level.FINER,
  350. "Connection closed for resource {0}", id);
  351. } else {
  352. /*
  353. * Unexpected cancel, e.g. if the user closes the browser
  354. * tab.
  355. */
  356. getLogger().log(Level.FINER,
  357. "Connection unexpectedly closed for resource {0} with transport {1}",
  358. new Object[] { id, resource.transport() });
  359. }
  360. pushConnection.connectionLost();
  361. }
  362. } catch (final Exception e) {
  363. callErrorHandler(session, e);
  364. } finally {
  365. try {
  366. session.unlock();
  367. } catch (Exception e) {
  368. getLogger().log(Level.WARNING, "Error while unlocking session",
  369. e);
  370. // can't call ErrorHandler, we (hopefully) don't have a lock
  371. }
  372. }
  373. }
  374. private static UI findUiUsingResource(AtmosphereResource resource,
  375. Collection<UI> uIs) {
  376. for (UI ui : uIs) {
  377. PushConnection pushConnection = ui.getPushConnection();
  378. if (pushConnection instanceof AtmospherePushConnection) {
  379. if (((AtmospherePushConnection) pushConnection)
  380. .getResource() == resource) {
  381. return ui;
  382. }
  383. }
  384. }
  385. return null;
  386. }
  387. /**
  388. * Sends a refresh message to the given atmosphere resource. Uses an
  389. * AtmosphereResource instead of an AtmospherePushConnection even though it
  390. * might be possible to look up the AtmospherePushConnection from the UI to
  391. * ensure border cases work correctly, especially when there temporarily are
  392. * two push connections which try to use the same UI. Using the
  393. * AtmosphereResource directly guarantees the message goes to the correct
  394. * recipient.
  395. *
  396. * @param resource
  397. * The atmosphere resource to send refresh to
  398. *
  399. */
  400. private static void sendRefreshAndDisconnect(AtmosphereResource resource)
  401. throws IOException {
  402. sendNotificationAndDisconnect(resource, VaadinService
  403. .createCriticalNotificationJSON(null, null, null, null));
  404. }
  405. /**
  406. * Tries to send a critical notification to the client and close the
  407. * connection. Does nothing if the connection is already closed.
  408. */
  409. private static void sendNotificationAndDisconnect(
  410. AtmosphereResource resource, String notificationJson) {
  411. // TODO Implemented differently from sendRefreshAndDisconnect
  412. try {
  413. if (resource instanceof AtmosphereResourceImpl
  414. && !((AtmosphereResourceImpl) resource).isInScope()) {
  415. // The resource is no longer valid so we should not write
  416. // anything to it
  417. getLogger().fine(
  418. "sendNotificationAndDisconnect called for resource no longer in scope");
  419. return;
  420. }
  421. resource.getResponse().getWriter().write(notificationJson);
  422. resource.resume();
  423. } catch (Exception e) {
  424. getLogger().log(Level.FINEST,
  425. "Failed to send critical notification to client", e);
  426. }
  427. }
  428. private static final Logger getLogger() {
  429. return Logger.getLogger(PushHandler.class.getName());
  430. }
  431. /**
  432. * Called when a new push connection is requested to be opened by the client
  433. *
  434. * @since 7.5.0
  435. * @param resource
  436. * The related atmosphere resources
  437. */
  438. void onConnect(AtmosphereResource resource) {
  439. callWithUi(resource, establishCallback, false);
  440. }
  441. /**
  442. * Called when a message is received through the push connection
  443. *
  444. * @since 7.5.0
  445. * @param resource
  446. * The related atmosphere resources
  447. */
  448. void onMessage(AtmosphereResource resource) {
  449. callWithUi(resource, receiveCallback,
  450. resource.transport() == TRANSPORT.WEBSOCKET);
  451. }
  452. /**
  453. * Sets the timeout used for suspend calls when using long polling.
  454. *
  455. * If you are using a proxy with a defined idle timeout, set the suspend
  456. * timeout to a value smaller than the proxy timeout so that the server is
  457. * aware of a reconnect taking place.
  458. *
  459. * @since 7.6
  460. * @param longPollingSuspendTimeout
  461. * the timeout to use for suspended AtmosphereResources
  462. */
  463. public void setLongPollingSuspendTimeout(int longPollingSuspendTimeout) {
  464. this.longPollingSuspendTimeout = longPollingSuspendTimeout;
  465. }
  466. /**
  467. * Gets the timeout used for suspend calls when using long polling.
  468. *
  469. * @since 7.6
  470. * @return the timeout to use for suspended AtmosphereResources
  471. */
  472. public int getLongPollingSuspendTimeout() {
  473. return longPollingSuspendTimeout;
  474. }
  475. }