private boolean handleExceptionalInteractions( final Modal modal, final Interaction inter, final String json) { boolean handled = false; Map<String, Object> map; Boolean notify; switch (inter) { case EXCEPTION: handleException(json); handled = true; break; case UNEXPECTEDSTATERESET: log.debug("Handling unexpected state reset."); backupSettings(); handleReset(); Events.syncModel(this.model); // fall through because this should be done in both cases: case UNEXPECTEDSTATEREFRESH: try { map = jsonToMap(json); } catch (Exception e) { log.error("Bad json payload in inter '{}': {}", inter, json); return true; } notify = (Boolean) map.get("notify"); if (notify) { try { lanternFeedback.submit((String) map.get("report"), this.model.getProfile().getEmail()); } catch (Exception e) { log.error( "Could not submit unexpected state report: {}\n {}", e.getMessage(), (String) map.get("report")); } } handled = true; break; } return handled; }
protected void processRequest(final HttpServletRequest req, final HttpServletResponse resp) { LanternUtils.addCSPHeader(resp); final String uri = req.getRequestURI(); log.debug("Received URI: {}", uri); final String interactionStr = StringUtils.substringAfterLast(uri, "/"); if (StringUtils.isBlank(interactionStr)) { log.debug("blank interaction"); HttpUtils.sendClientError(resp, "blank interaction"); return; } log.debug("Headers: " + HttpUtils.getRequestHeaders(req)); if (!"XMLHttpRequest".equals(req.getHeader("X-Requested-With"))) { log.debug("invalid X-Requested-With"); HttpUtils.sendClientError(resp, "invalid X-Requested-With"); return; } if (!SecurityUtils.constantTimeEquals(model.getXsrfToken(), req.getHeader("X-XSRF-TOKEN"))) { log.debug( "X-XSRF-TOKEN wrong: got {} expected {}", req.getHeader("X-XSRF-TOKEN"), model.getXsrfToken()); HttpUtils.sendClientError(resp, "invalid X-XSRF-TOKEN"); return; } final int cl = req.getContentLength(); String json = ""; if (cl > 0) { try { json = IOUtils.toString(req.getInputStream()); } catch (final IOException e) { log.error("Could not parse json?"); } } log.debug("Body: '" + json + "'"); final Interaction inter = Interaction.valueOf(interactionStr.toUpperCase()); if (inter == Interaction.CLOSE) { if (handleClose(json)) { return; } } if (inter == Interaction.URL) { final String url = JsonUtils.getValueFromJson("url", json); if (!StringUtils.startsWith(url, "http://") && !StringUtils.startsWith(url, "https://")) { log.error("http(s) url expected, got {}", url); HttpUtils.sendClientError(resp, "http(s) urls only"); return; } try { new URL(url); } catch (MalformedURLException e) { log.error("invalid url: {}", url); HttpUtils.sendClientError(resp, "invalid url"); return; } final String cmd; if (SystemUtils.IS_OS_MAC_OSX) { cmd = "open"; } else if (SystemUtils.IS_OS_LINUX) { cmd = "gnome-open"; } else if (SystemUtils.IS_OS_WINDOWS) { cmd = "start"; } else { log.error("unsupported OS"); HttpUtils.sendClientError(resp, "unsupported OS"); return; } try { if (SystemUtils.IS_OS_WINDOWS) { // On Windows, we have to quote the url to allow for // e.g. ? and & characters in query string params. // To quote the url, we supply a dummy first argument, // since otherwise start treats the first argument as a // title for the new console window when it's quoted. LanternUtils.runCommand(cmd, "\"\"", "\"" + url + "\""); } else { // on OS X and Linux, special characters in the url make // it through this call without our having to quote them. LanternUtils.runCommand(cmd, url); } } catch (IOException e) { log.error("open url failed"); HttpUtils.sendClientError(resp, "open url failed"); return; } return; } final Modal modal = this.model.getModal(); log.debug( "processRequest: modal = {}, inter = {}, mode = {}", modal, inter, this.model.getSettings().getMode()); if (handleExceptionalInteractions(modal, inter, json)) { return; } Modal switchTo = null; try { // XXX a map would make this more robust switchTo = Modal.valueOf(interactionStr); } catch (IllegalArgumentException e) { } if (switchTo != null && switchModals.contains(switchTo)) { if (!switchTo.equals(modal)) { if (!switchModals.contains(modal)) { this.internalState.setLastModal(modal); } Events.syncModal(model, switchTo); } return; } switch (modal) { case welcome: this.model.getSettings().setMode(Mode.unknown); switch (inter) { case GET: log.debug("Setting get mode"); handleSetModeWelcome(Mode.get); break; case GIVE: log.debug("Setting give mode"); handleSetModeWelcome(Mode.give); break; } break; case authorize: log.debug("Processing authorize modal..."); this.internalState.setModalCompleted(Modal.authorize); this.internalState.advanceModal(null); break; case finished: this.internalState.setCompletedTo(Modal.finished); switch (inter) { case CONTINUE: log.debug("Processing continue"); this.model.setShowVis(true); Events.sync(SyncPath.SHOWVIS, true); this.internalState.setModalCompleted(Modal.finished); this.internalState.advanceModal(null); break; case SET: log.debug("Processing set in finished modal...applying JSON\n{}", json); applyJson(json); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError( resp, "Interaction not handled for modal: " + modal + " and interaction: " + inter); break; } break; case firstInviteReceived: log.error("Processing invite received..."); break; case lanternFriends: this.internalState.setCompletedTo(Modal.lanternFriends); switch (inter) { case FRIEND: this.friender.addFriend(email(json)); break; case REJECT: this.friender.removeFriend(email(json)); break; case CONTINUE: // This dialog always passes continue as of this writing and // not close. case CLOSE: log.debug("Processing continue/close for friends dialog"); if (this.model.isSetupComplete()) { Events.syncModal(model, Modal.none); } else { this.internalState.setModalCompleted(Modal.lanternFriends); this.internalState.advanceModal(null); } break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError( resp, "Interaction not handled for modal: " + modal + " and interaction: " + inter); break; } break; case none: break; case notInvited: switch (inter) { case RETRY: Events.syncModal(model, Modal.authorize); break; // not currently implemented: // case REQUESTINVITE: // Events.syncModal(model, Modal.requestInvite); // break; default: log.error("Unexpected interaction: " + inter); break; } break; case proxiedSites: this.internalState.setCompletedTo(Modal.proxiedSites); switch (inter) { case CONTINUE: if (this.model.isSetupComplete()) { Events.syncModal(model, Modal.none); } else { this.internalState.setModalCompleted(Modal.proxiedSites); this.internalState.advanceModal(null); } break; case LANTERNFRIENDS: log.debug("Processing lanternFriends from proxiedSites"); Events.syncModal(model, Modal.lanternFriends); break; case SET: if (!model.getSettings().isSystemProxy()) { String msg = "Because you are using manual proxy " + "configuration, you may have to restart your " + "browser for your updated proxied sites list " + "to take effect."; model.addNotification(msg, MessageType.info, 30); Events.sync(SyncPath.NOTIFICATIONS, model.getNotifications()); } applyJson(json); break; case SETTINGS: log.debug("Processing settings from proxiedSites"); Events.syncModal(model, Modal.settings); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError(resp, "unexpected interaction for proxied sites"); break; } break; case requestInvite: log.info("Processing request invite"); switch (inter) { case CANCEL: this.internalState.setModalCompleted(Modal.requestInvite); this.internalState.advanceModal(Modal.notInvited); break; case CONTINUE: applyJson(json); this.internalState.setModalCompleted(Modal.proxiedSites); // TODO: need to do something here this.internalState.advanceModal(null); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError(resp, "unexpected interaction for request invite"); break; } break; case requestSent: log.debug("Process request sent"); break; case settings: switch (inter) { case GET: log.debug("Setting get mode"); // Only deal with a mode change if the mode has changed! if (modelService.getMode() == Mode.give) { // Break this out because it's set in the subsequent // setMode call final boolean everGet = model.isEverGetMode(); this.modelService.setMode(Mode.get); if (!everGet) { // need to do more setup to switch to get mode from // give mode model.setSetupComplete(false); model.setModal(Modal.proxiedSites); Events.syncModel(model); } else { // This primarily just triggers a setup complete event, // which triggers connecting to proxies, setting up // the local system proxy, etc. model.setSetupComplete(true); } } break; case GIVE: log.debug("Setting give mode"); this.modelService.setMode(Mode.give); break; case CLOSE: log.debug("Processing settings close"); Events.syncModal(model, Modal.none); break; case SET: log.debug("Processing set in setting...applying JSON\n{}", json); applyJson(json); break; case RESET: log.debug("Processing reset"); Events.syncModal(model, Modal.confirmReset); break; case PROXIEDSITES: log.debug("Processing proxied sites in settings"); Events.syncModal(model, Modal.proxiedSites); break; case LANTERNFRIENDS: log.debug("Processing friends in settings"); Events.syncModal(model, Modal.lanternFriends); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError( resp, "Interaction not handled for modal: " + modal + " and interaction: " + inter); break; } break; case settingsLoadFailure: switch (inter) { case RETRY: modelIo.reload(); Events.sync(SyncPath.NOTIFICATIONS, model.getNotifications()); Events.syncModal(model, model.getModal()); break; case RESET: backupSettings(); Events.syncModal(model, Modal.welcome); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); break; } break; case systemProxy: this.internalState.setCompletedTo(Modal.systemProxy); switch (inter) { case CONTINUE: log.debug("Processing continue in systemProxy", json); applyJson(json); Events.sync(SyncPath.SYSTEMPROXY, model.getSettings().isSystemProxy()); this.internalState.setModalCompleted(Modal.systemProxy); this.internalState.advanceModal(null); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError(resp, "error setting system proxy pref"); break; } break; case updateAvailable: switch (inter) { case CLOSE: this.internalState.setModalCompleted(Modal.updateAvailable); this.internalState.advanceModal(null); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); break; } break; case authorizeLater: log.error("Did not handle interaction {} for modal {}", inter, modal); break; case confirmReset: log.debug("Handling confirm reset interaction"); switch (inter) { case CANCEL: log.debug("Processing cancel"); Events.syncModal(model, Modal.settings); break; case RESET: handleReset(); Events.syncModel(this.model); break; default: log.error("Did not handle interaction {} for modal {}", inter, modal); HttpUtils.sendClientError( resp, "Interaction not handled for modal: " + modal + " and interaction: " + inter); } break; case about: // fall through on purpose case sponsor: switch (inter) { case CLOSE: Events.syncModal(model, this.internalState.getLastModal()); break; default: HttpUtils.sendClientError(resp, "invalid interaction " + inter); } break; case contact: switch (inter) { case CONTINUE: String msg; MessageType messageType; try { lanternFeedback.submit(json, this.model.getProfile().getEmail()); msg = "Thank you for contacting Lantern."; messageType = MessageType.info; } catch (Exception e) { log.error("Error submitting contact form: {}", e); msg = "Error sending message. Please check your " + "connection and try again."; messageType = MessageType.error; } model.addNotification(msg, messageType, 30); Events.sync(SyncPath.NOTIFICATIONS, model.getNotifications()); // fall through because this should be done in both cases: case CANCEL: Events.syncModal(model, this.internalState.getLastModal()); break; default: HttpUtils.sendClientError(resp, "invalid interaction " + inter); } break; case giveModeForbidden: if (inter == Interaction.CONTINUE) { // need to do more setup to switch to get mode from give mode model.getSettings().setMode(Mode.get); model.setSetupComplete(false); this.internalState.advanceModal(null); Events.syncModal(model, Modal.proxiedSites); Events.sync(SyncPath.SETUPCOMPLETE, false); } break; default: log.error("No matching modal for {}", modal); } this.modelIo.write(); }