private Map<String, String> loadAllToks(final String code, int port, final HttpClient httpClient)
      throws IOException {
    final HttpPost post = new HttpPost("https://accounts.google.com/o/oauth2/token");
    try {
      final List<? extends NameValuePair> nvps =
          Arrays.asList(
              new BasicNameValuePair("code", code),
              new BasicNameValuePair("client_id", model.getSettings().getClientID()),
              new BasicNameValuePair("client_secret", model.getSettings().getClientSecret()),
              new BasicNameValuePair("redirect_uri", OauthUtils.getRedirectUrl(port)),
              new BasicNameValuePair("grant_type", "authorization_code"));
      final HttpEntity entity = new UrlEncodedFormEntity(nvps, LanternConstants.UTF8);
      post.setEntity(entity);

      log.debug("About to execute post!");
      final HttpResponse response = httpClient.execute(post);

      log.debug("Got response status: {}", response.getStatusLine());
      final HttpEntity responseEntity = response.getEntity();
      final String body = IOUtils.toString(responseEntity.getContent());
      EntityUtils.consume(responseEntity);

      final Map<String, String> oauthToks = JsonUtils.OBJECT_MAPPER.readValue(body, Map.class);
      log.debug("Got oath data: {}", oauthToks);
      return oauthToks;
    } finally {
      post.reset();
    }
  }
Example #2
0
  @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);
    }
  }
Example #3
0
 @Inject
 public LogglyHelper(Model model) {
   this.model = model;
   this.loggly =
       new Loggly(
           model.isDev(),
           model.getSettings().getMode() == Mode.get
               ? LanternConstants.LANTERN_LOCALHOST_ADDR
               : null);
 }
Example #4
0
 public void submit(String json) {
   String reporterId = "(" + model.getInstanceId() + ")";
   String email = model.getProfile().getEmail();
   if (!StringUtils.isBlank(email)) {
     reporterId = email + " " + reporterId;
   }
   LogglyMessage msg =
       new LogglyMessage(reporterId, "Lantern Feedback", new Date())
           .setExtraFromJson(json)
           .sanitized(false);
   loggly.log(msg);
   LOG.info("submitted to Loggly: %s", json);
 }
  /**
   * Make sure we're getting messages back from the controller.
   *
   * @throws Exception If anything goes wrong.
   */
  @Test
  public void testControllerMessages() throws Exception {
    this.closedBetaEvent = null;

    final Censored censored = new DefaultCensored();
    final CountryService countryService = new CountryService(censored);
    final Model model = new Model(countryService); // .getModel();
    final org.lantern.state.Settings settings = model.getSettings();

    settings.setMode(Mode.get);
    settings.setAccessToken(TestingUtils.accessToken());
    settings.setRefreshToken(TestingUtils.getRefreshToken());
    settings.setUseGoogleOAuth2(true);

    TestingUtils.doWithGetModeProxy(
        new Callable<Void>() {
          @Override
          public Void call() throws Exception {
            final XmppHandler handler = TestingUtils.newXmppHandler(censored, model);
            // handler.start();
            // The handler could have already been created and connected, so
            // make sure we disconnect.
            handler.disconnect();
            handler.connect();

            assertTrue(handler.isLoggedIn());

            LOG.debug("Checking for proxies in settings: {}", settings);
            int count = 0;
            while (closedBetaEvent == null && count < 200) {
              Thread.sleep(120);
              count++;
            }

            handler.stop();
            return null;
          }
        });

    assertTrue("Should have received event from the controller", this.closedBetaEvent != null);
  }
  @Test
  public void testMessages() throws Exception {
    final Model model = new Model();
    final Messages msgs = new Messages(model);
    assertEquals(0, model.getNotifications().size());
    final String email = "*****@*****.**";
    msgs.info(MessageKey.ALREADY_ADDED, email);

    int tries = 0;
    while (tries < 30) {
      if (model.getNotifications().size() > 0) {
        break;
      }
      Thread.sleep(50);
      tries++;
    }
    assertEquals(1, model.getNotifications().size());
    final Notification note = model.getNotifications().get(new Integer(0));

    assertEquals("You have already added [email protected].", note.getMessage());
  }
Example #7
0
 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;
 }
Example #8
0
  @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);
        }
      }
    }
  }
Example #9
0
  public static void load(final boolean start) {
    if (loaded) {
      LOG.warn("ALREADY LOADED. HOW THE HECK DOES SUREFIRE CLASSLOADING WORK?");
      if (!started) {
        start(start);
      }
      return;
    }
    loaded = true;
    final Module lm = newTestLanternModule();

    injector = Guice.createInjector(lm);

    xmppHandler = instance(DefaultXmppHandler.class);
    socketsUtil = instance(LanternSocketsUtil.class);
    ksm = instance(LanternKeyStoreManager.class);
    lanternXmppUtil = instance(LanternXmppUtil.class);
    localCipherProvider = instance(LocalCipherProvider.class);
    encryptedFileService = instance(EncryptedFileService.class);
    model = instance(Model.class);
    jettyLauncher = instance(JettyLauncher.class);
    messageService = instance(MessageService.class);
    statsTracker = instance(ClientStats.class);
    roster = instance(Roster.class);
    modelService = instance(ModelService.class);
    proxifier = instance(Proxifier.class);
    modelUtils = instance(ModelUtils.class);
    modelIo = instance(ModelIo.class);
    proxyTracker = instance(DefaultProxyTracker.class);
    trustStore = instance(LanternTrustStore.class);

    httpClientFactory = instance(HttpClientFactory.class);

    geoIpLookupService = instance(GeoIpLookupService.class);
    countryService = instance(CountryService.class);

    final Settings set = model.getSettings();
    LOG.debug("setting oauth token values...");
    LOG.debug("secure env vars available? {}", System.getenv("TRAVIS_SECURE_ENV_VARS"));
    set.setAccessToken(getAccessToken());
    set.setRefreshToken(getRefreshToken());
    set.setUseGoogleOAuth2(true);
    start(start);
  }
Example #10
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()));
 }
Example #11
0
 /**
  * Convenience method for syncing a new modal both with the state model and with the frontend.
  *
  * @param model The state model.
  * @param modal The modal to set.
  */
 public static void syncModal(final Model model, final Modal modal) {
   model.setModal(modal);
   syncModal(model);
 }
Example #12
0
 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();
 }
Example #13
0
  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();
  }
Example #14
0
  protected synchronized void addMapping(
      final PortMappingProtocol prot,
      final int externalPortRequested,
      int localPort,
      final PortMapListener portMapListener) {

    ByteBuffer lanaddr = ByteBuffer.allocate(16);
    ByteBuffer intClient = ByteBuffer.allocate(16);
    ByteBuffer intPort = ByteBuffer.allocate(6);
    ByteBuffer desc = ByteBuffer.allocate(80);
    ByteBuffer enabled = ByteBuffer.allocate(4);
    ByteBuffer leaseDuration = ByteBuffer.allocate(16);
    int ret;

    final UPNPUrls urls = new UPNPUrls();
    final IGDdatas data = new IGDdatas();

    UPNPDev devlist =
        MiniupnpcLibrary.INSTANCE.upnpDiscover(
            UPNP_DELAY, (String) null, (String) null, 0, 0, IntBuffer.allocate(1));
    if (devlist == null) {
      MiniupnpcLibrary.INSTANCE.FreeUPNPUrls(urls);
      portMapListener.onPortMapError();
      return;
    }
    ret = MiniupnpcLibrary.INSTANCE.UPNP_GetValidIGD(devlist, urls, data, lanaddr, 16);
    if (ret == 0) {
      log.debug("No valid UPNP Internet Gateway Device found.");
      portMapListener.onPortMapError();
      MiniupnpcLibrary.INSTANCE.FreeUPNPUrls(urls);
      devlist.setAutoRead(false);
      MiniupnpcLibrary.INSTANCE.freeUPNPDevlist(devlist);
      return;
    }
    try {

      logIGDResponse(ret, urls);

      log.debug("Local LAN ip address : " + zeroTerminatedString(lanaddr.array()));
      ByteBuffer externalAddress = ByteBuffer.allocate(16);
      MiniupnpcLibrary.INSTANCE.UPNP_GetExternalIPAddress(
          urls.controlURL.getString(0),
          zeroTerminatedString(data.first.servicetype),
          externalAddress);
      publicIp = zeroTerminatedString(externalAddress.array());
      log.debug("ExternalIPAddress = " + publicIp);

      ret =
          MiniupnpcLibrary.INSTANCE.UPNP_AddPortMapping(
              urls.controlURL.getString(0), // controlURL
              zeroTerminatedString(data.first.servicetype), // servicetype
              "" + externalPortRequested, // external Port
              "" + localPort, // internal Port
              zeroTerminatedString(lanaddr.array()), // internal client
              "added via MiniupnpcLibrary.INSTANCE/JAVA !", // description
              prot.toString(), // protocol UDP or TCP
              null, // remote host (useless)
              "0"); // leaseDuration

      if (ret != MiniupnpcLibrary.UPNPCOMMAND_SUCCESS) {
        portMapListener.onPortMapError();
        return;
      }

      // get the local port (but didn't we request one?)
      ret =
          MiniupnpcLibrary.INSTANCE.UPNP_GetSpecificPortMappingEntry(
              urls.controlURL.getString(0),
              zeroTerminatedString(data.first.servicetype),
              "" + externalPortRequested,
              prot.toString(),
              intClient,
              intPort,
              desc,
              enabled,
              leaseDuration);

      log.debug(
          "InternalIP:Port = "
              + zeroTerminatedString(intClient.array())
              + ":"
              + zeroTerminatedString(intPort.array())
              + " ("
              + zeroTerminatedString(desc.array())
              + ")");

      model.getInstanceStats().setUsingUPnP(true);

      final UpnpMapping mapping = new UpnpMapping();
      mapping.prot = prot;
      mapping.internalPort = localPort;
      mapping.externalPort = externalPortRequested;
      mappings.add(mapping);
      log.debug("Added mapping. Mappings now: {}", mappings);
    } finally {
      MiniupnpcLibrary.INSTANCE.FreeUPNPUrls(urls);
      devlist.setAutoRead(false);
      MiniupnpcLibrary.INSTANCE.freeUPNPDevlist(devlist);
    }
    portMapListener.onPortMap(externalPortRequested);
  }