@Subscribe
  public void onConnectivityChanged(final ConnectivityChangedEvent e) {
    Connectivity connectivity = model.getConnectivity();
    if (!e.isConnected()) {
      connectivity.setInternet(false);
      Events.sync(SyncPath.CONNECTIVITY_INTERNET, false);
      return;
    }
    InetAddress ip = e.getNewIp();
    connectivity.setIp(ip.getHostAddress());

    connectivity.setInternet(true);
    Events.sync(SyncPath.CONNECTIVITY, model.getConnectivity());

    Settings set = model.getSettings();

    if (set.getMode() == null || set.getMode() == Mode.unknown) {
      if (censored.isCensored()) {
        set.setMode(Mode.get);
      } else {
        set.setMode(Mode.give);
      }
    } else if (set.getMode() == Mode.give && censored.isCensored()) {
      // want to set the mode to get now so that we don't mistakenly
      // proxy any more than necessary
      set.setMode(Mode.get);
      log.info("Disconnected; setting giveModeForbidden");
      Events.syncModal(model, Modal.giveModeForbidden);
    }
  }
  @Subscribe
  public void onLocationChanged(final LocationChangedEvent e) {
    Events.sync(SyncPath.LOCATION, e.getNewLocation());

    if (censored.isCountryCodeCensored(e.getNewCountry())) {
      if (!censored.isCountryCodeCensored(e.getOldCountry())) {
        // moving from uncensored to censored
        if (model.getSettings().getMode() == Mode.give) {
          Events.syncModal(model, Modal.giveModeForbidden);
        }
      }
    }
  }
 private void quit() {
   LOG.debug("quit called.");
   Events.eventBus().post(new QuitEvent());
   // LanternHub.display().dispose();
   // this.xmppHandler.disconnect();
   System.exit(0);
 }
  /**
   * Fetches user's e-mail - only public for testing.
   *
   * @param allToks OAuth tokens.
   * @param httpClient The HTTP client.
   */
  public int fetchEmail(final Map<String, String> allToks, final HttpClient httpClient) {
    final String endpoint = "https://www.googleapis.com/oauth2/v1/userinfo";
    final String accessToken = allToks.get("access_token");
    final HttpGet get = new HttpGet(endpoint);
    get.setHeader(HttpHeaders.Names.AUTHORIZATION, "Bearer " + accessToken);

    try {
      log.debug("About to execute get!");
      final HttpResponse response = httpClient.execute(get);
      final StatusLine line = response.getStatusLine();
      log.debug("Got response status: {}", line);
      final HttpEntity entity = response.getEntity();
      final String body = IOUtils.toString(entity.getContent(), "UTF-8");
      EntityUtils.consume(entity);
      log.debug("GOT RESPONSE BODY FOR EMAIL:\n" + body);

      final int code = line.getStatusCode();
      if (code < 200 || code > 299) {
        log.error("OAuth error?\n" + line);
        return code;
      }

      final Profile profile = JsonUtils.OBJECT_MAPPER.readValue(body, Profile.class);
      this.model.setProfile(profile);
      Events.sync(SyncPath.PROFILE, profile);
      // final String email = profile.getEmail();
      // this.model.getSettings().setEmail(email);
      return code;
    } catch (final IOException e) {
      log.warn("Could not connect to Google?", e);
    } finally {
      get.reset();
    }
    return -1;
  }
 private void handleReset() {
   // This posts the reset event to any classes that need to take action,
   // avoiding coupling this class to those classes.
   Events.eventBus().post(new ResetEvent());
   if (LanternClientConstants.DEFAULT_MODEL_FILE.isFile()) {
     try {
       FileUtils.forceDelete(LanternClientConstants.DEFAULT_MODEL_FILE);
     } catch (final IOException e) {
       log.warn("Could not delete model file?");
     }
   }
   final Model base = new Model(model.getCountryService());
   model.setEverGetMode(false);
   model.setLaunchd(base.isLaunchd());
   model.setModal(base.getModal());
   model.setNodeId(base.getNodeId());
   model.setProfile(base.getProfile());
   model.setNproxiedSitesMax(base.getNproxiedSitesMax());
   // we need to keep clientID and clientSecret, because they are application-level settings
   String clientID = model.getSettings().getClientID();
   String clientSecret = model.getSettings().getClientSecret();
   model.setSettings(base.getSettings());
   model.getSettings().setClientID(clientID);
   model.getSettings().setClientSecret(clientSecret);
   model.setSetupComplete(base.isSetupComplete());
   model.setShowVis(base.isShowVis());
   // model.setFriends(base.getFriends());
   model.clearNotifications();
   modelIo.write();
 }
Exemple #6
0
 @Inject
 public DefaultPeerFactory(
     final GeoIpLookupService geoIpLookupService, final Model model, final Roster roster) {
   this.geoIpLookupService = geoIpLookupService;
   this.model = model;
   this.roster = roster;
   Events.register(this);
 }
  private void connectToGoogleTalk(final Map<String, String> allToks) {
    final String accessToken = allToks.get("access_token");
    final String refreshToken = allToks.get("refresh_token");

    if (StringUtils.isBlank(accessToken) || StringUtils.isBlank(refreshToken)) {
      log.warn("Not access or refresh token -- not logging in!!");
      return;
    } else {
      // Treat this the same as a credential exception? I.e. what
      // happens if the user cancels?
    }

    this.model.getSettings().setAccessToken(accessToken);
    this.model.getSettings().setRefreshToken(refreshToken);
    this.model.getSettings().setUseGoogleOAuth2(true);
    this.modelIo.write();
    Events.asyncEventBus().post(new RefreshTokenEvent(refreshToken));

    // We kick this off on another thread, as otherwise it would be
    // a Jetty thread, and we're about to kill the server. When the
    // server is killed, the connecting thread would otherwise be
    // interrupted.
    final Thread t =
        new Thread(
            new Runnable() {

              @Override
              public void run() {
                try {
                  xmppHandler.connect();
                  log.debug("Setting gtalk authorized");
                  model.getConnectivity().setGtalkAuthorized(true);
                  internalState.setNotInvited(false);
                  internalState.setModalCompleted(Modal.authorize);
                  internalState.advanceModal(null);
                } catch (final CredentialException e) {
                  log.error("Could not log in with OAUTH?", e);
                  Events.syncModal(model, Modal.authorize);
                } catch (final NotInClosedBetaException e) {
                  log.info("This user is not invited");
                  internalState.setNotInvited(true);
                  Events.syncModal(model, Modal.notInvited);
                } catch (final IOException e) {
                  log.info("We can't connect (internet connection died?).  Retry.", e);
                  Events.syncModal(model, Modal.authorize);
                }
              }
            },
            "Google-Talk-Connect-From-Oauth-Servlet-Thread");
    t.setDaemon(true);
    t.start();
  }
Exemple #8
0
 public void invite(Friend friend) {
   String email = friend.getEmail();
   try {
     if (xmppHandler.sendInvite(friend, false)) {
       // we need to mark this email as pending, in case
       // our invite gets lost.
       model.addPendingInvite(email);
     }
   } catch (Exception e) {
     log.debug("failed to send invite: ", e);
     model.addPendingInvite(email);
   }
   Events.sync(SyncPath.FRIENDS, model.getFriends().getFriends());
 }
 @Inject
 public InteractionServlet(
     final Model model,
     final ModelService modelService,
     final InternalState internalState,
     final ModelIo modelIo,
     final Censored censored,
     final LanternFeedback lanternFeedback,
     final FriendsHandler friender) {
   this.model = model;
   this.modelService = modelService;
   this.internalState = internalState;
   this.modelIo = modelIo;
   this.censored = censored;
   this.lanternFeedback = lanternFeedback;
   this.friender = friender;
   Events.register(this);
 }
 private boolean handleClose(String json) {
   if (StringUtils.isBlank(json)) {
     return false;
   }
   final ObjectMapper om = new ObjectMapper();
   Map<String, Object> map;
   try {
     map = om.readValue(json, Map.class);
     final String notification = (String) map.get("notification");
     model.closeNotification(Integer.parseInt(notification));
     Events.sync(SyncPath.NOTIFICATIONS, model.getNotifications());
     return true;
   } catch (JsonParseException e) {
     log.warn("Exception closing notifications {}", e);
   } catch (JsonMappingException e) {
     log.warn("Exception closing notifications {}", e);
   } catch (IOException e) {
     log.warn("Exception closing notifications {}", e);
   }
   return false;
 }
 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;
 }
Exemple #12
0
  public void loadOAuth2UserCredentialsFile(final String filename, final Settings set) {
    if (StringUtils.isBlank(filename)) {
      log.error("No filename specified");
      throw new NullPointerException("No filename specified!");
    }
    final File file = new File(filename);
    if (!(file.exists() && file.canRead())) {
      log.error("Unable to read user credentials from {}", filename);
      throw new IllegalArgumentException("File does not exist! " + filename);
    }
    log.info("Reading user credentials from file \"{}\"", filename);
    try {
      final String json = FileUtils.readFileToString(file, "US-ASCII");
      final JSONObject obj = (JSONObject) JSONValue.parse(json);
      final String username = (String) obj.get("username");
      final String accessToken = (String) obj.get("access_token");
      final String refreshToken = (String) obj.get("refresh_token");
      // Access token is not strictly necessary, so we allow it to be
      // null.
      if (StringUtils.isBlank(username) || StringUtils.isBlank(refreshToken)) {
        log.error("Failed to parse user credentials file \"{}\"", filename);
        throw new Error("Could not load username or refresh_token");
      } else {
        set.setAccessToken(accessToken);
        set.setRefreshToken(refreshToken);
        set.setUseGoogleOAuth2(true);

        // We have to be careful here because classes simply haven't
        // registered as listeners at this point, so listeners have
        // to make sure to also check for an existing refresh token
        // in the settings.
        Events.asyncEventBus().post(new RefreshTokenEvent(refreshToken));
      }
    } catch (final IOException e) {
      log.error("Failed to read file \"{}\"", filename);
      throw new Error("Could not load oauth credentials", e);
    }
  }
Exemple #13
0
 public void setSystemProxy(final boolean systemProxy) {
   log.info("Setting system proxy...");
   this.systemProxy = systemProxy;
   Events.inOrderAsyncEventBus().post(new SystemProxyChangedEvent(systemProxy));
 }
Exemple #14
0
  /**
   * We need to make sure to set the server port before anything is injected -- otherwise we run the
   * risk of running on a completely different port than what is passed on the command line!
   *
   * @param cmd The command line.
   * @param read The model
   */
  private void processCommandLine(final CommandLine cmd, final Model model) {

    if (cmd == null) {
      // Can be true for testing.
      log.error("No command line?");
      return;
    }
    final Settings set = model.getSettings();
    if (cmd.hasOption(Cli.OPTION_SERVER_PORT)) {
      final String serverPortStr = cmd.getOptionValue(Cli.OPTION_SERVER_PORT);
      log.debug("Using command-line proxy port: " + serverPortStr);
      final int serverPort = Integer.parseInt(serverPortStr);
      set.setServerPort(serverPort);
    } else {
      final int existing = set.getServerPort();
      if (existing < 1024) {
        log.debug("Using random give mode proxy port...");
        set.setServerPort(LanternUtils.randomPort());
      }
    }
    log.info("Running give mode proxy on port: {}", set.getServerPort());

    if (cmd.hasOption(Cli.OPTION_KEYSTORE)) {
      LanternUtils.setFallbackKeystorePath(cmd.getOptionValue(Cli.OPTION_KEYSTORE));
    }

    final String ctrlOpt = Cli.OPTION_CONTROLLER_ID;
    if (cmd.hasOption(ctrlOpt)) {
      LanternClientConstants.setControllerId(cmd.getOptionValue(ctrlOpt));
    }

    final String insOpt = Cli.OPTION_INSTANCE_ID;
    if (cmd.hasOption(insOpt)) {
      model.setInstanceId(cmd.getOptionValue(insOpt));
    }

    final String fbOpt = Cli.OPTION_AS_FALLBACK;
    if (cmd.hasOption(fbOpt)) {
      LanternUtils.setFallbackProxy(true);
    }

    final String secOpt = Cli.OPTION_OAUTH2_CLIENT_SECRETS_FILE;
    if (cmd.hasOption(secOpt)) {
      loadOAuth2ClientSecretsFile(cmd.getOptionValue(secOpt), set);
    }

    final String credOpt = Cli.OPTION_OAUTH2_USER_CREDENTIALS_FILE;
    if (cmd.hasOption(credOpt)) {
      loadOAuth2UserCredentialsFile(cmd.getOptionValue(credOpt), set);
    }

    final String ripOpt = Cli.OPTION_REPORT_IP;
    if (cmd.hasOption(ripOpt)) {
      model.setReportIp(cmd.getOptionValue(ripOpt));
    }

    // final Settings set = LanternHub.settings();

    set.setUseTrustedPeers(parseOptionDefaultTrue(cmd, Cli.OPTION_TRUSTED_PEERS));
    set.setUseAnonymousPeers(parseOptionDefaultTrue(cmd, Cli.OPTION_ANON_PEERS));
    set.setUseLaeProxies(parseOptionDefaultTrue(cmd, Cli.OPTION_LAE));
    set.setUseCentralProxies(parseOptionDefaultTrue(cmd, Cli.OPTION_CENTRAL));
    set.setUdpProxyPriority(
        cmd.getOptionValue(Cli.OPTION_UDP_PROXY_PRIORITY, "lower").toUpperCase());

    final boolean tcp = parseOptionDefaultTrue(cmd, Cli.OPTION_TCP);
    final boolean udp = parseOptionDefaultTrue(cmd, Cli.OPTION_UDP);
    IceConfig.setTcp(tcp);
    IceConfig.setUdp(udp);
    set.setTcp(tcp);
    set.setUdp(udp);

    /*
    if (cmd.hasOption(OPTION_USER)) {
        set.setUserId(cmd.getOptionValue(OPTION_USER));
    }
    if (cmd.hasOption(OPTION_PASS)) {
        set.(cmd.getOptionValue(OPTION_PASS));
    }
    */

    if (cmd.hasOption(Cli.OPTION_ACCESS_TOK)) {
      set.setAccessToken(cmd.getOptionValue(Cli.OPTION_ACCESS_TOK));
    }

    if (cmd.hasOption(Cli.OPTION_REFRESH_TOK)) {
      final String refresh = cmd.getOptionValue(Cli.OPTION_REFRESH_TOK);
      set.setRefreshToken(refresh);
      Events.asyncEventBus().post(new RefreshTokenEvent(refresh));
    }
    // option to disable use of keychains in local privacy
    if (cmd.hasOption(Cli.OPTION_DISABLE_KEYCHAIN)) {
      log.info("Disabling use of system keychains");
      set.setKeychainEnabled(false);
    } else {
      set.setKeychainEnabled(true);
    }

    if (cmd.hasOption(Cli.OPTION_PASSWORD_FILE)) {
      loadLocalPasswordFile(cmd.getOptionValue(Cli.OPTION_PASSWORD_FILE));
    }

    if (cmd.hasOption(Cli.OPTION_PUBLIC_API)) {
      set.setBindToLocalhost(false);
    }

    log.info("Running API on port: {}", StaticSettings.getApiPort());
    if (cmd.hasOption(Cli.OPTION_LAUNCHD)) {
      log.debug("Running from launchd or launchd set on command line");
      model.setLaunchd(true);
    } else {
      model.setLaunchd(false);
    }

    if (cmd.hasOption(Cli.OPTION_GIVE)) {
      model.getSettings().setMode(Mode.give);
    } else if (cmd.hasOption(Cli.OPTION_GET)) {
      model.getSettings().setMode(Mode.get);
    }
  }
Exemple #15
0
 public static void sync(final SyncPath path, final Object value) {
   Events.asyncEventBus().post(new SyncEvent(path, value));
 }
Exemple #16
0
 public static void syncRosterEntry(final LanternRosterEntry entry, final int index) {
   final String path = SyncPath.ROSTER.getPath() + "." + index;
   LOG.debug("Syncing roster entry at path {} with entry {}", path, entry);
   Events.eventBus().post(new SyncEvent(path, entry));
 }
Exemple #17
0
 /** Convenience method for syncing the current modal with the frontend. */
 public static void syncRoster(final Roster roster) {
   // This is done synchronously because we need the roster array on the
   // frontend to be in sync with the backend in order to index into it
   // on roster updates.
   Events.eventBus().post(new SyncEvent(SyncPath.ROSTER, roster.getEntries()));
 }
Exemple #18
0
 /**
  * Convenience method for syncing the current modal with the frontend.
  *
  * @param model The state model.
  */
 public static void syncModal(final Model model) {
   Events.asyncEventBus().post(new SyncEvent(SyncPath.MODAL, model.getModal()));
 }
  @Override
  public void createTray() {

    /*uniqueApp = libunique.unique_app_new("org.lantern.lantern", null);
    if (libunique.unique_app_is_running(uniqueApp)) {
        LOG.error("Already running!");
        System.exit(0);
        // could signal to open dashboard
    }*/

    menu = libgtk.gtk_menu_new();

    connectionStatusItem = libgtk.gtk_menu_item_new_with_label(LABEL_DISCONNECTED);
    libgtk.gtk_widget_set_sensitive(connectionStatusItem, Gtk.FALSE);
    libgtk.gtk_menu_shell_append(menu, connectionStatusItem);
    libgtk.gtk_widget_show_all(connectionStatusItem);

    dashboardItem = libgtk.gtk_menu_item_new_with_label("Open Dashboard");
    dashboardItemCallback =
        new Gobject.GCallback() {
          @Override
          public void callback(Pointer instance, Pointer data) {
            LOG.debug("openDashboardItem callback called");
            openDashboard();
          }
        };
    libgobject.g_signal_connect_data(
        dashboardItem, "activate", dashboardItemCallback, null, null, 0);
    libgtk.gtk_menu_shell_append(menu, dashboardItem);
    libgtk.gtk_widget_show_all(dashboardItem);

    // updateItem = Gtk.gtk_menu_item_new_with_label();

    quitItem = libgtk.gtk_menu_item_new_with_label("Quit");
    quitItemCallback =
        new Gobject.GCallback() {
          @Override
          public void callback(Pointer instance, Pointer data) {
            LOG.debug("quitItemCallback called");
            quit();
          }
        };
    libgobject.g_signal_connect_data(quitItem, "activate", quitItemCallback, null, null, 0);
    libgtk.gtk_menu_shell_append(menu, quitItem);
    libgtk.gtk_widget_show_all(quitItem);

    appIndicator =
        libappindicator.app_indicator_new(
            "lantern", "indicator-messages-new", AppIndicator.CATEGORY_APPLICATION_STATUS);

    /* XXX basically a hack -- we should subclass the AppIndicator
       type and override the fallback entry in the 'vtable', instead we just
       hack the app indicator class itself. Not an issue unless we need other
       appindicators.
    */
    AppIndicator.AppIndicatorClassStruct aiclass =
        new AppIndicator.AppIndicatorClassStruct(appIndicator.parent.g_type_instance.g_class);

    AppIndicator.Fallback replacementFallback =
        new AppIndicator.Fallback() {
          @Override
          public Pointer callback(final AppIndicator.AppIndicatorInstanceStruct self) {
            fallback();
            return null;
          }
        };

    aiclass.fallback = replacementFallback;
    aiclass.write();

    libappindicator.app_indicator_set_menu(appIndicator, menu);

    changeIcon(ICON_DISCONNECTED, LABEL_DISCONNECTED);
    libappindicator.app_indicator_set_status(appIndicator, AppIndicator.STATUS_ACTIVE);

    Events.register(this);
    this.active = true;
  }
Exemple #20
0
 @Inject
 public InviteQueue(final XmppHandler handler, final Model model) {
   this.xmppHandler = handler;
   this.model = model;
   Events.register(this);
 }
 public DefaultXmppHandlerTest() {
   SASLAuthentication.registerSASLMechanism("X-OAUTH2", LanternSaslGoogleOAuth2Mechanism.class);
   Events.register(this);
 }
  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();
  }
 private void handleSetModeWelcome(final Mode mode) {
   this.model.setModal(Modal.authorize);
   this.internalState.setModalCompleted(Modal.welcome);
   this.modelService.setMode(mode);
   Events.syncModal(model);
 }