Bug 1292639 - Ask user permission before allowing navigator.publishServer. r=mconley r=ehsan r=sebastian
authorKannan Vijayan <kvijayan@mozilla.com>
Thu, 08 Sep 2016 14:36:53 -0400
changeset 313336 89a168219747b20bb0effe9abe49bcd403f27266
parent 313335 8302d9c4a62b11f93aac2ea9d721e3b3aaa429e1
child 313337 c0322637f5ea9eb486408092ff2e166d84c30fb0
push id30679
push usercbook@mozilla.com
push dateFri, 09 Sep 2016 10:03:06 +0000
treeherdermozilla-central@feff79e5b137 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, ehsan, sebastian
bugs1292639
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1292639 - Ask user permission before allowing navigator.publishServer. r=mconley r=ehsan r=sebastian
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/browser.properties
dom/flyweb/FlyWebPublishedServer.cpp
dom/flyweb/FlyWebPublishedServer.h
dom/flyweb/FlyWebPublishedServerIPC.h
dom/flyweb/FlyWebService.cpp
mobile/android/chrome/content/PermissionsHelper.js
mobile/android/components/ContentPermissionPrompt.js
mobile/android/locales/en-US/chrome/browser.properties
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2630,16 +2630,55 @@ ContentPermissionPrompt.prototype = {
     }
 
     secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST);
 
     this._showPrompt(aRequest, message, "geo", actions, "geolocation",
                      "geo-notification-icon", options);
   },
 
+  _promptFlyWebPublishServer : function(aRequest) {
+    var message = "Would you like to let this site start a server accessible to nearby devices and people?";
+    var actions = [
+      {
+        stringId: "flyWebPublishServer.allowPublishServer",
+        action: Ci.nsIPermissionManager.ALLOW_ACTION,
+        expireType: Ci.nsIPermissionManager.EXPIRE_SESSION
+      },
+      {
+        stringId: "flyWebPublishServer.denyPublishServer",
+        action: Ci.nsIPermissionManager.DENY_ACTION,
+        expireType: Ci.nsIPermissionManager.EXPIRE_SESSION
+      }
+    ];
+
+    let options = {
+      learnMoreURL: "https://flyweb.github.io",
+      popupIconURL: "chrome://flyweb/skin/icon-64.png"
+    };
+
+    let browser = this._getBrowserForRequest(aRequest);
+    let chromeDoc = browser.ownerDocument;
+    let iconElem = chromeDoc.getElementById("flyweb-publish-server-notification-icon");
+    if (!iconElem) {
+      let notificationPopupBox = chromeDoc.getElementById("notification-popup-box");
+      let notificationIcon = chromeDoc.createElement("image");
+      notificationIcon.setAttribute("id", "flyweb-publish-server-notification-icon");
+      notificationIcon.setAttribute("src", "chrome://flyweb/skin/icon-64.png");
+      notificationIcon.setAttribute("class", "notification-anchor-icon flyweb-publish-server-icon");
+      notificationIcon.setAttribute("style", "filter: url(chrome://browser/skin/filters.svg#fill); fill: currentColor; opacity: .4;");
+      notificationIcon.setAttribute("role", "button");
+      notificationIcon.setAttribute("aria-label", "View the publish-server request");
+      notificationPopupBox.appendChild(notificationIcon);
+    }
+
+    this._showPrompt(aRequest, message, "flyweb-publish-server", actions, "flyweb-publish-server",
+                     "flyweb-publish-server-notification-icon", options);
+  },
+
   _promptWebNotifications : function(aRequest) {
     var message = gBrowserBundle.GetStringFromName("webNotifications.receiveFromSite");
 
     var actions;
 
     var browser = this._getBrowserForRequest(aRequest);
     // Only show "allow for session" in PB mode, we don't
     // support "allow for session" in non-PB mode.
@@ -2694,17 +2733,18 @@ ContentPermissionPrompt.prototype = {
     let types = request.types.QueryInterface(Ci.nsIArray);
     if (types.length != 1) {
       request.cancel();
       return;
     }
     let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
 
     const kFeatureKeys = { "geolocation" : "geo",
-                           "desktop-notification" : "desktop-notification"
+                           "desktop-notification" : "desktop-notification",
+                           "flyweb-publish-server": "flyweb-publish-server"
                          };
 
     // Make sure that we support the request.
     if (!(perm.type in kFeatureKeys)) {
       return;
     }
 
     var requestingPrincipal = request.principal;
@@ -2737,16 +2777,21 @@ ContentPermissionPrompt.prototype = {
     // Show the prompt.
     switch (perm.type) {
     case "geolocation":
       this._promptGeo(request);
       break;
     case "desktop-notification":
       this._promptWebNotifications(request);
       break;
+    case "flyweb-publish-server":
+      if (AppConstants.NIGHTLY_BUILD) {
+        this._promptFlyWebPublishServer(request);
+      }
+      break;
     }
   },
 
 };
 
 var DefaultBrowserCheck = {
   get OPTIONPOPUP() { return "defaultBrowserNotificationPopup" },
   _setAsDefaultTimer: null,
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -373,16 +373,26 @@ geolocation.shareLocation=Share Location
 geolocation.shareLocation.accesskey=a
 geolocation.alwaysShareLocation=Always Share Location
 geolocation.alwaysShareLocation.accesskey=A
 geolocation.neverShareLocation=Never Share Location
 geolocation.neverShareLocation.accesskey=N
 geolocation.shareWithSite2=Would you like to share your location with this site?
 geolocation.shareWithFile2=Would you like to share your location with this file?
 
+# FlyWeb UI
+# LOCALIZATION NOTE (flyWebPublishServer.allowPublishServer): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.allowPublishServer=Allow Server
+# LOCALIZATION NOTE (flyWebPublishServer.allowPublishServer.accessKey): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.allowPublishServer.accesskey=A
+# LOCALIZATION NOTE (flyWebPublishServer.denyPublishServer): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.denyPublishServer=Block Server
+# LOCALIZATION NOTE (flyWebPublishServer.denyPublishServer.accessKey): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.denyPublishServer.accesskey=B
+
 webNotifications.receiveForSession=Receive for this session
 webNotifications.receiveForSession.accesskey=s
 webNotifications.alwaysReceive=Always Receive Notifications
 webNotifications.alwaysReceive.accesskey=A
 webNotifications.neverShow=Always Block Notifications
 webNotifications.neverShow.accesskey=N
 webNotifications.receiveFromSite=Would you like to receive notifications from this site?
 # LOCALIZATION NOTE (webNotifications.upgradeTitle): When using native notifications on OS X, the title may be truncated around 32 characters.
--- a/dom/flyweb/FlyWebPublishedServer.cpp
+++ b/dom/flyweb/FlyWebPublishedServer.cpp
@@ -166,16 +166,26 @@ NS_IMPL_ISUPPORTS_INHERITED0(FlyWebPubli
 
 FlyWebPublishedServerImpl::FlyWebPublishedServerImpl(nsPIDOMWindowInner* aOwner,
                                                      const nsAString& aName,
                                                      const FlyWebPublishOptions& aOptions)
   : FlyWebPublishedServer(aOwner, aName, aOptions)
   , mHttpServer(new HttpServer())
 {
   LOG_I("FlyWebPublishedServerImpl::FlyWebPublishedServerImpl(%p)", this);
+}
+
+void
+FlyWebPublishedServerImpl::PermissionGranted(bool aGranted)
+{
+  LOG_I("FlyWebPublishedServerImpl::PermissionGranted(%b)", aGranted);
+  if (!aGranted) {
+    PublishedServerStarted(NS_ERROR_FAILURE);
+    return;
+  }
 
   mHttpServer->Init(-1, Preferences::GetBool("flyweb.use-tls", false), this);
 }
 
 void
 FlyWebPublishedServerImpl::Close()
 {
   FlyWebPublishedServer::Close();
@@ -247,72 +257,84 @@ FlyWebPublishedServerImpl::OnWebSocketAc
 }
 
 /******** FlyWebPublishedServerChild ********/
 
 FlyWebPublishedServerChild::FlyWebPublishedServerChild(nsPIDOMWindowInner* aOwner,
                                                        const nsAString& aName,
                                                        const FlyWebPublishOptions& aOptions)
   : FlyWebPublishedServer(aOwner, aName, aOptions)
-  , mActorDestroyed(false)
+  , mActorExists(false)
 {
   LOG_I("FlyWebPublishedServerChild::FlyWebPublishedServerChild(%p)", this);
 
-  ContentChild::GetSingleton()->
-    SendPFlyWebPublishedServerConstructor(this,
-                                          PromiseFlatString(aName),
-                                          aOptions);
-
   // The matching release happens when the actor is destroyed, in
   // ContentChild::DeallocPFlyWebPublishedServerChild
   NS_ADDREF_THIS();
 }
 
+void
+FlyWebPublishedServerChild::PermissionGranted(bool aGranted)
+{
+  if (!aGranted) {
+    PublishedServerStarted(NS_ERROR_FAILURE);
+    return;
+  }
+
+  mActorExists = true;
+  FlyWebPublishOptions options;
+  options.mUiUrl = mUiUrl;
+
+  // Proceed with initialization.
+  ContentChild::GetSingleton()->
+    SendPFlyWebPublishedServerConstructor(this, mName, options);
+}
+
 bool
 FlyWebPublishedServerChild::RecvServerReady(const nsresult& aStatus)
 {
   LOG_I("FlyWebPublishedServerChild::RecvServerReady(%p)", this);
-  MOZ_ASSERT(!mActorDestroyed);
+  MOZ_ASSERT(mActorExists);
 
   PublishedServerStarted(aStatus);
   return true;
 }
 
 bool
 FlyWebPublishedServerChild::RecvServerClose()
 {
   LOG_I("FlyWebPublishedServerChild::RecvServerClose(%p)", this);
-  MOZ_ASSERT(!mActorDestroyed);
+  MOZ_ASSERT(mActorExists);
 
   Close();
 
   return true;
 }
 
 bool
 FlyWebPublishedServerChild::RecvFetchRequest(const IPCInternalRequest& aRequest,
                                              const uint64_t& aRequestId)
 {
   LOG_I("FlyWebPublishedServerChild::RecvFetchRequest(%p)", this);
-  MOZ_ASSERT(!mActorDestroyed);
+  MOZ_ASSERT(mActorExists);
 
   RefPtr<InternalRequest> request = new InternalRequest(aRequest);
   mPendingRequests.Put(request, aRequestId);
   FireFetchEvent(request);
 
   return true;
 }
 
 bool
 FlyWebPublishedServerChild::RecvWebSocketRequest(const IPCInternalRequest& aRequest,
                                                  const uint64_t& aRequestId,
                                                  PTransportProviderChild* aProvider)
 {
   LOG_I("FlyWebPublishedServerChild::RecvWebSocketRequest(%p)", this);
-  MOZ_ASSERT(!mActorDestroyed);
+  MOZ_ASSERT(mActorExists);
 
   RefPtr<InternalRequest> request = new InternalRequest(aRequest);
   mPendingRequests.Put(request, aRequestId);
 
   // Not addreffing here. The addref was already done when the
   // PTransportProvider child constructor original ran.
   mPendingTransportProviders.Put(aRequestId,
     dont_AddRef(static_cast<TransportProviderChild*>(aProvider)));
@@ -322,26 +344,26 @@ FlyWebPublishedServerChild::RecvWebSocke
   return true;
 }
 
 void
 FlyWebPublishedServerChild::ActorDestroy(ActorDestroyReason aWhy)
 {
   LOG_I("FlyWebPublishedServerChild::ActorDestroy(%p)", this);
 
-  mActorDestroyed = true;
+  mActorExists = false;
 }
 
 void
 FlyWebPublishedServerChild::OnFetchResponse(InternalRequest* aRequest,
                                             InternalResponse* aResponse)
 {
   LOG_I("FlyWebPublishedServerChild::OnFetchResponse(%p)", this);
 
-  if (mActorDestroyed) {
+  if (!mActorExists) {
     LOG_I("FlyWebPublishedServerChild::OnFetchResponse(%p) - No actor!", this);
     return;
   }
 
   uint64_t id = mPendingRequests.Get(aRequest);
   MOZ_ASSERT(id);
   mPendingRequests.Remove(aRequest);
 
@@ -356,17 +378,17 @@ FlyWebPublishedServerChild::OnFetchRespo
 
 already_AddRefed<nsITransportProvider>
 FlyWebPublishedServerChild::OnWebSocketAcceptInternal(InternalRequest* aRequest,
                                                       const Optional<nsAString>& aProtocol,
                                                       ErrorResult& aRv)
 {
   LOG_I("FlyWebPublishedServerChild::OnWebSocketAcceptInternal(%p)", this);
 
-  if (mActorDestroyed) {
+  if (!mActorExists) {
     LOG_I("FlyWebPublishedServerChild::OnWebSocketAcceptInternal(%p) - No actor!", this);
     return nullptr;
   }
 
   uint64_t id = mPendingRequests.Get(aRequest);
   MOZ_ASSERT(id);
   mPendingRequests.Remove(aRequest);
 
@@ -395,17 +417,17 @@ FlyWebPublishedServerChild::OnWebSocketA
 }
 
 void
 FlyWebPublishedServerChild::OnWebSocketResponse(InternalRequest* aRequest,
                                                 InternalResponse* aResponse)
 {
   LOG_I("FlyWebPublishedServerChild::OnFetchResponse(%p)", this);
 
-  if (mActorDestroyed) {
+  if (!mActorExists) {
     LOG_I("FlyWebPublishedServerChild::OnFetchResponse(%p) - No actor!", this);
     return;
   }
 
   uint64_t id = mPendingRequests.Get(aRequest);
   MOZ_ASSERT(id);
   mPendingRequests.Remove(aRequest);
 
@@ -423,17 +445,17 @@ FlyWebPublishedServerChild::OnWebSocketR
 
 void
 FlyWebPublishedServerChild::Close()
 {
   LOG_I("FlyWebPublishedServerChild::Close(%p)", this);
 
   FlyWebPublishedServer::Close();
 
-  if (!mActorDestroyed) {
+  if (mActorExists) {
     LOG_I("FlyWebPublishedServerChild::Close - sending __delete__ (%p)", this);
 
     Send__delete__(this);
   }
 }
 
 /******** FlyWebPublishedServerParent ********/
 
--- a/dom/flyweb/FlyWebPublishedServer.h
+++ b/dom/flyweb/FlyWebPublishedServer.h
@@ -52,16 +52,18 @@ public:
     return mName;
   }
 
   void GetUiUrl(nsAString& aUiUrl)
   {
     aUiUrl = mUiUrl;
   }
 
+  virtual void PermissionGranted(bool aGranted) = 0;
+
   virtual void OnFetchResponse(InternalRequest* aRequest,
                                InternalResponse* aResponse) = 0;
   already_AddRefed<WebSocket>
     OnWebSocketAccept(InternalRequest* aConnectRequest,
                       const Optional<nsAString>& aProtocol,
                       ErrorResult& aRv);
   virtual void OnWebSocketResponse(InternalRequest* aConnectRequest,
                                    InternalResponse* aResponse) = 0;
--- a/dom/flyweb/FlyWebPublishedServerIPC.h
+++ b/dom/flyweb/FlyWebPublishedServerIPC.h
@@ -45,16 +45,17 @@ public:
   void GetCertKey(nsACString& aKey) {
     if (mHttpServer) {
       mHttpServer->GetCertKey(aKey);
     } else {
       aKey.Truncate();
     }
   }
 
+  virtual void PermissionGranted(bool aGranted) override;
   virtual void OnFetchResponse(InternalRequest* aRequest,
                                InternalResponse* aResponse) override;
   virtual void OnWebSocketResponse(InternalRequest* aConnectRequest,
                                    InternalResponse* aResponse) override;
   virtual already_AddRefed<nsITransportProvider>
     OnWebSocketAcceptInternal(InternalRequest* aConnectRequest,
                               const Optional<nsAString>& aProtocol,
                               ErrorResult& aRv) override;
@@ -93,16 +94,17 @@ private:
 class FlyWebPublishedServerChild final : public FlyWebPublishedServer
                                        , public PFlyWebPublishedServerChild
 {
 public:
   FlyWebPublishedServerChild(nsPIDOMWindowInner* aOwner,
                              const nsAString& aName,
                              const FlyWebPublishOptions& aOptions);
 
+  virtual void PermissionGranted(bool aGranted) override;
   virtual bool RecvServerReady(const nsresult& aStatus) override;
   virtual bool RecvServerClose() override;
   virtual bool RecvFetchRequest(const IPCInternalRequest& aRequest,
                                 const uint64_t& aRequestId) override;
   virtual bool RecvWebSocketRequest(const IPCInternalRequest& aRequest,
                                     const uint64_t& aRequestId,
                                     PTransportProviderChild* aProvider) override;
 
@@ -120,17 +122,17 @@ public:
   virtual void ActorDestroy(ActorDestroyReason aWhy) override;
 
 private:
   ~FlyWebPublishedServerChild() {}
 
   nsDataHashtable<nsRefPtrHashKey<InternalRequest>, uint64_t> mPendingRequests;
   nsRefPtrHashtable<nsUint64HashKey, TransportProviderChild>
     mPendingTransportProviders;
-  bool mActorDestroyed;
+  bool mActorExists;
 };
 
 class FlyWebPublishedServerParent final : public PFlyWebPublishedServerParent
                                         , public nsIDOMEventListener
 {
 public:
   FlyWebPublishedServerParent(const nsAString& aName,
                               const FlyWebPublishOptions& aOptions);
--- a/dom/flyweb/FlyWebService.cpp
+++ b/dom/flyweb/FlyWebService.cpp
@@ -4,46 +4,160 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/FlyWebService.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/FlyWebPublishedServerIPC.h"
+#include "mozilla/AddonPathService.h"
 #include "nsISocketTransportService.h"
 #include "mdns/libmdns/nsDNSServiceInfo.h"
 #include "nsIUUIDGenerator.h"
 #include "nsStandardURL.h"
 #include "mozilla/Services.h"
 #include "nsISupportsPrimitives.h"
 #include "mozilla/dom/FlyWebDiscoveryManagerBinding.h"
 #include "prnetdb.h"
 #include "DNS.h"
+#include "nsContentPermissionHelper.h"
 #include "nsSocketTransportService2.h"
 #include "nsSocketTransport2.h"
 #include "nsHashPropertyBag.h"
 #include "nsNetUtil.h"
 #include "nsISimpleEnumerator.h"
 #include "nsIProperty.h"
 #include "nsICertOverrideService.h"
 
 namespace mozilla {
 namespace dom {
 
 struct FlyWebPublishOptions;
 
 static LazyLogModule gFlyWebServiceLog("FlyWebService");
 #undef LOG_I
 #define LOG_I(...) MOZ_LOG(mozilla::dom::gFlyWebServiceLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
 #undef LOG_E
 #define LOG_E(...) MOZ_LOG(mozilla::dom::gFlyWebServiceLog, mozilla::LogLevel::Error, (__VA_ARGS__))
+
 #undef LOG_TEST_I
 #define LOG_TEST_I(...) MOZ_LOG_TEST(mozilla::dom::gFlyWebServiceLog, mozilla::LogLevel::Debug)
 
+class FlyWebPublishServerPermissionCheck final
+  : public nsIContentPermissionRequest
+  , public nsIRunnable
+{
+public:
+  NS_DECL_ISUPPORTS
+
+  FlyWebPublishServerPermissionCheck(const nsCString& aServiceName, uint64_t aWindowID,
+                                     FlyWebPublishedServer* aServer)
+    : mServiceName(aServiceName)
+    , mWindowID(aWindowID)
+    , mServer(aServer)
+  {}
+
+  uint64_t WindowID() const
+  {
+    return mWindowID;
+  }
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsGlobalWindow* globalWindow = nsGlobalWindow::GetInnerWindowWithId(mWindowID);
+    if (!globalWindow) {
+      return Cancel();
+    }
+    mWindow = globalWindow->AsInner();
+    if (NS_WARN_IF(!mWindow)) {
+      return Cancel();
+    }
+
+    nsCOMPtr<nsIDocument> doc = mWindow->GetDoc();
+    if (NS_WARN_IF(!doc)) {
+      return Cancel();
+    }
+
+    mPrincipal = doc->NodePrincipal();
+    MOZ_ASSERT(mPrincipal);
+
+    mRequester = new nsContentPermissionRequester(mWindow);
+    return nsContentPermissionUtils::AskPermission(this, mWindow);
+  }
+
+  NS_IMETHOD Cancel() override
+  {
+    Resolve(false);
+    return NS_OK;
+  }
+
+  NS_IMETHOD Allow(JS::HandleValue aChoices) override
+  {
+    MOZ_ASSERT(aChoices.isUndefined());
+    Resolve(true);
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetTypes(nsIArray** aTypes) override
+  {
+    nsTArray<nsString> emptyOptions;
+    return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("flyweb-publish-server"),
+                                                           NS_LITERAL_CSTRING("unused"), emptyOptions, aTypes);
+  }
+
+  NS_IMETHOD GetRequester(nsIContentPermissionRequester** aRequester) override
+  {
+    NS_ENSURE_ARG_POINTER(aRequester);
+    nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
+    requester.forget(aRequester);
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetPrincipal(nsIPrincipal** aRequestingPrincipal) override
+  {
+    NS_IF_ADDREF(*aRequestingPrincipal = mPrincipal);
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetWindow(mozIDOMWindow** aRequestingWindow) override
+  {
+    NS_IF_ADDREF(*aRequestingWindow = mWindow);
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetElement(nsIDOMElement** aRequestingElement) override
+  {
+    *aRequestingElement = nullptr;
+    return NS_OK;
+  }
+
+private:
+  void Resolve(bool aResolve)
+  {
+    mServer->PermissionGranted(aResolve);
+  }
+
+  virtual ~FlyWebPublishServerPermissionCheck() = default;
+
+  nsCString mServiceName;
+  uint64_t mWindowID;
+  RefPtr<FlyWebPublishedServer> mServer;
+  nsCOMPtr<nsPIDOMWindowInner> mWindow;
+  nsCOMPtr<nsIPrincipal> mPrincipal;
+  nsCOMPtr<nsIContentPermissionRequester> mRequester;
+};
+
+NS_IMPL_ISUPPORTS(FlyWebPublishServerPermissionCheck,
+                  nsIContentPermissionRequest,
+                  nsIRunnable)
+
 class FlyWebMDNSService final
   : public nsIDNSServiceDiscoveryListener
   , public nsIDNSServiceResolveListener
   , public nsIDNSRegistrationListener
   , public nsITimerCallback
 {
   friend class FlyWebService;
 
@@ -836,39 +950,88 @@ FlyWebService::Init()
       mMDNSFlywebService = nullptr;
       rv.SuppressException();
     }
   }
 
   return ErrorResult(NS_OK);
 }
 
+static already_AddRefed<FlyWebPublishPromise>
+MakeRejectionPromise(const char* name)
+{
+    MozPromiseHolder<FlyWebPublishPromise> holder;
+    RefPtr<FlyWebPublishPromise> promise = holder.Ensure(name);
+    holder.Reject(NS_ERROR_FAILURE, name);
+    return promise.forget();
+}
+
 already_AddRefed<FlyWebPublishPromise>
 FlyWebService::PublishServer(const nsAString& aName,
                              const FlyWebPublishOptions& aOptions,
                              nsPIDOMWindowInner* aWindow)
 {
   // Scan uiUrl for illegal characters
 
   RefPtr<FlyWebPublishedServer> existingServer =
     FlyWebService::GetOrCreate()->FindPublishedServerByName(aName);
   if (existingServer) {
     LOG_I("PublishServer: Trying to publish server with already-existing name %s.",
           NS_ConvertUTF16toUTF8(aName).get());
-    MozPromiseHolder<FlyWebPublishPromise> holder;
-    RefPtr<FlyWebPublishPromise> promise = holder.Ensure(__func__);
-    holder.Reject(NS_ERROR_FAILURE, __func__);
-    return promise.forget();
+    return MakeRejectionPromise(__func__);
   }
 
   RefPtr<FlyWebPublishedServer> server;
   if (XRE_GetProcessType() == GeckoProcessType_Content) {
     server = new FlyWebPublishedServerChild(aWindow, aName, aOptions);
   } else {
     server = new FlyWebPublishedServerImpl(aWindow, aName, aOptions);
+
+    // Before proceeding, ensure that the FlyWeb system addon exists.
+    nsresult rv;
+    nsCOMPtr<nsIURI> uri;
+    rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("chrome://flyweb/skin/icon-64.png"));
+    if (NS_FAILED(rv)) {
+      return MakeRejectionPromise(__func__);
+    }
+
+    JSAddonId *addonId = MapURIToAddonID(uri);
+    if (!addonId) {
+      LOG_E("PublishServer: Failed to find FlyWeb system addon.");
+      return MakeRejectionPromise(__func__);
+    }
+
+    JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(addonId));
+    nsAutoString addonIdString;
+    AssignJSFlatString(addonIdString, flat);
+    if (!addonIdString.EqualsLiteral("flyweb@mozilla.org")) {
+      nsCString addonIdCString = NS_ConvertUTF16toUTF8(addonIdString);
+      LOG_E("PublishServer: FlyWeb resource found on wrong system addon: %s.", addonIdCString.get());
+      return MakeRejectionPromise(__func__);
+    }
+  }
+
+  if (aWindow) {
+    nsresult rv;
+
+    MOZ_ASSERT(NS_IsMainThread());
+    rv = NS_DispatchToCurrentThread(
+      MakeAndAddRef<FlyWebPublishServerPermissionCheck>(
+        NS_ConvertUTF16toUTF8(aName), aWindow->WindowID(), server));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      LOG_E("PublishServer: Failed to dispatch permission check runnable for %s",
+            NS_ConvertUTF16toUTF8(aName).get());
+      return MakeRejectionPromise(__func__);
+    }
+  } else {
+    // If aWindow is null, we're definitely in the e10s parent process.
+    // In this case, we know that permission has already been granted
+    // by the user because of content-process prompt.
+    MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+    server->PermissionGranted(true);
   }
 
   mServers.AppendElement(server);
 
   return server->GetPublishPromise();
 }
 
 already_AddRefed<FlyWebPublishedServer>
--- a/mobile/android/chrome/content/PermissionsHelper.js
+++ b/mobile/android/chrome/content/PermissionsHelper.js
@@ -1,27 +1,33 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 var PermissionsHelper = {
   _permissonTypes: ["password", "geolocation", "popup", "indexedDB",
-                    "offline-app", "desktop-notification", "plugins", "native-intent"],
+                    "offline-app", "desktop-notification", "plugins", "native-intent",
+                    "flyweb-publish-server"],
   _permissionStrings: {
     "password": {
       label: "password.logins",
       allowed: "password.save",
       denied: "password.dontSave"
     },
     "geolocation": {
       label: "geolocation.location",
       allowed: "geolocation.allow",
       denied: "geolocation.dontAllow"
     },
+    "flyweb-publish-server": {
+      label: "flyWebPublishServer.publishServer",
+      allowed: "flyWebPublishServer.allow",
+      denied: "flyWebPublishServer.dontAllow"
+    },
     "popup": {
       label: "blockPopups.label2",
       allowed: "popup.show",
       denied: "popup.dontShow"
     },
     "indexedDB": {
       label: "offlineApps.offlineData",
       allowed: "offlineApps.allow",
--- a/mobile/android/components/ContentPermissionPrompt.js
+++ b/mobile/android/components/ContentPermissionPrompt.js
@@ -13,22 +13,24 @@ Cu.import("resource://gre/modules/Servic
 const kEntities = {
   "contacts": "contacts",
   "desktop-notification": "desktopNotification2",
   "device-storage:music": "deviceStorageMusic",
   "device-storage:pictures": "deviceStoragePictures",
   "device-storage:sdcard": "deviceStorageSdcard",
   "device-storage:videos": "deviceStorageVideos",
   "geolocation": "geolocation",
+  "flyweb-publish-server": "flyWebPublishServer",
 };
 
 // For these types, prompt for permission if action is unknown.
 const PROMPT_FOR_UNKNOWN = [
   "desktop-notification",
   "geolocation",
+  "flyweb-publish-server",
 ];
 
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
   classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -126,16 +126,28 @@ geolocation.dontAskAgain=Don't ask again
 # Desktop notification UI
 desktopNotification2.allow=Always
 desktopNotification2.dontAllow=Never
 desktopNotification2.ask=Would you like to receive notifications from this site?
 # LOCALIZATION NOTE (desktopNotification.notifications): Label that will be
 # used in site settings dialog.
 desktopNotification.notifications=Notifications
 
+# FlyWeb UI
+# LOCALIZATION NOTE (flyWebPublishServer.allow): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.allow=Allow
+# LOCALIZATION NOTE (flyWebPublishServer.dontAllow): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.dontAllow=Deny
+# LOCALIZATION NOTE (flyWebPublishServer.ask): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.ask=Would you like to let this site start a server accessible to nearby devices and people?
+# LOCALIZATION NOTE (flyWebPublishServer.dontAskAgain): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.dontAskAgain=Don't ask again for this site
+# LOCALIZATION NOTE (flyWebPublishServer.publishServer): This is an experimental feature only shipping in Nightly, and doesn't need translation.
+flyWebPublishServer.publishServer=Publish Server
+
 # Imageblocking
 imageblocking.downloadedImage=Image unblocked
 imageblocking.showAllImages=Show All
 
 # Device Storage API
 deviceStorageMusic.allow=Allow
 deviceStorageMusic.dontAllow=Don't allow
 deviceStorageMusic.ask=Allow %S access to your music?