Bug 1155898 - Expose fetch on JS sandbox
authorMartin Thomson <martin.thomson@gmail.com>
Mon, 18 May 2015 09:52:33 -0700
changeset 265197 39d75574a70eddb0466555e1c1c6b49ccfc8dbe0
parent 265196 39fdf623231bdf7d726a57db488a078ce7ebbe6c
child 265198 4d63ef04b3db5758889a5f780069cfbfd28f7603
push id2102
push usermartin.thomson@gmail.com
push dateMon, 18 May 2015 17:35:37 +0000
bugs1155898
milestone41.0a1
Bug 1155898 - Expose fetch on JS sandbox
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/tests/mochitest/mochitest.ini
js/xpconnect/tests/mochitest/test_sandbox_fetch.html
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -28,21 +28,26 @@
 #include "XPCWrapper.h"
 #include "XrayWrapper.h"
 #include "Crypto.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/BlobBinding.h"
 #include "mozilla/dom/CSSBinding.h"
 #include "mozilla/dom/indexedDB/IndexedDatabaseManager.h"
 #include "mozilla/dom/FileBinding.h"
+#include "mozilla/dom/Fetch.h"
+#include "mozilla/dom/HeadersBinding.h"
 #include "mozilla/dom/PromiseBinding.h"
+#include "mozilla/dom/RequestBinding.h"
+#include "mozilla/dom/ResponseBinding.h"
 #include "mozilla/dom/RTCIdentityProviderRegistrar.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/TextDecoderBinding.h"
 #include "mozilla/dom/TextEncoderBinding.h"
+#include "mozilla/dom/UnionConversions.h"
 #include "mozilla/dom/URLBinding.h"
 #include "mozilla/dom/URLSearchParamsBinding.h"
 
 using namespace mozilla;
 using namespace JS;
 using namespace js;
 using namespace xpc;
 
@@ -232,16 +237,94 @@ SandboxCreateRTCIdentityProvider(JSConte
 
     dom::RTCIdentityProviderRegistrar* registrar =
             new dom::RTCIdentityProviderRegistrar(nativeGlobal);
     JS::RootedObject wrapped(cx, registrar->WrapObject(cx, JS::NullPtr()));
     return JS_DefineProperty(cx, obj, "rtcIdentityProvider", wrapped, JSPROP_ENUMERATE);
 }
 
 static bool
+SetFetchRequestFromValue(JSContext *cx, RequestOrUSVString& request,
+                         const MutableHandleValue& requestOrUrl)
+{
+    RequestOrUSVStringArgument requestHolder(request);
+    bool noMatch = true;
+    if (requestOrUrl.isObject() &&
+        !requestHolder.TrySetToRequest(cx, requestOrUrl, noMatch, false)) {
+        return false;
+    }
+    if (noMatch &&
+        !requestHolder.TrySetToUSVString(cx, requestOrUrl, noMatch)) {
+        return false;
+    }
+    if (noMatch) {
+        return false;
+    }
+    return true;
+}
+
+static bool
+SandboxFetch(JSContext* cx, JS::HandleObject scope, const CallArgs& args)
+{
+    if (args.length() < 1) {
+        JS_ReportError(cx, "fetch requires at least 1 argument");
+        return false;
+    }
+
+    RequestOrUSVString request;
+    if (!SetFetchRequestFromValue(cx, request, args[0])) {
+        JS_ReportError(cx, "fetch requires a string or Request in argument 1");
+        return false;
+    }
+    RootedDictionary<dom::RequestInit> options(cx);
+    if (!options.Init(cx, args.hasDefined(1) ? args[1] : JS::NullHandleValue,
+                      "Argument 2 of fetch", false)) {
+        return false;
+    }
+    nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(scope);
+    if (!global) {
+        return false;
+    }
+    ErrorResult rv;
+    nsRefPtr<dom::Promise> response =
+        FetchRequest(global, Constify(request), Constify(options), rv);
+    rv.WouldReportJSException();
+    if (rv.Failed()) {
+        return ThrowMethodFailedWithDetails(cx, rv, "Sandbox", "fetch");
+    }
+    if (!GetOrCreateDOMReflector(cx, scope, response, args.rval())) {
+        return false;
+    }
+    return true;
+}
+
+static bool SandboxFetchPromise(JSContext* cx, unsigned argc, jsval* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedObject callee(cx, &args.callee());
+    RootedObject scope(cx, JS::CurrentGlobalOrNull(cx));
+    if (SandboxFetch(cx, scope, args)) {
+        return true;
+    }
+    return ConvertExceptionToPromise(cx, scope, args.rval());
+}
+
+
+static bool
+SandboxCreateFetch(JSContext* cx, HandleObject obj)
+{
+    MOZ_ASSERT(JS_IsGlobalObject(obj));
+
+    return JS_DefineFunction(cx, obj, "fetch", SandboxFetchPromise, 2, 0) &&
+        dom::RequestBinding::GetConstructorObject(cx, obj) &&
+        dom::ResponseBinding::GetConstructorObject(cx, obj) &&
+        dom::HeadersBinding::GetConstructorObject(cx, obj);
+}
+
+static bool
 SandboxIsProxy(JSContext* cx, unsigned argc, jsval* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     if (args.length() < 1) {
         JS_ReportError(cx, "Function requires at least 1 argument");
         return false;
     }
     if (!args[0].isObject()) {
@@ -813,16 +896,18 @@ xpc::GlobalProperties::Parse(JSContext* 
         } else if (!strcmp(name.ptr(), "Blob")) {
             Blob = true;
         } else if (!strcmp(name.ptr(), "File")) {
             File = true;
         } else if (!strcmp(name.ptr(), "crypto")) {
             crypto = true;
         } else if (!strcmp(name.ptr(), "rtcIdentityProvider")) {
             rtcIdentityProvider = true;
+        } else if (!strcmp(name.ptr(), "fetch")) {
+            fetch = true;
         } else {
             JS_ReportError(cx, "Unknown property name: %s", name.ptr());
             return false;
         }
     }
     return true;
 }
 
@@ -873,16 +958,19 @@ xpc::GlobalProperties::Define(JSContext*
         return false;
 
     if (crypto && !SandboxCreateCrypto(cx, obj))
         return false;
 
     if (rtcIdentityProvider && !SandboxCreateRTCIdentityProvider(cx, obj))
         return false;
 
+    if (fetch && !SandboxCreateFetch(cx, obj))
+        return false;
+
     return true;
 }
 
 nsresult
 xpc::CreateSandboxObject(JSContext* cx, MutableHandleValue vp, nsISupports* prinOrSop,
                          SandboxOptions& options)
 {
     // Create the sandbox global object
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3395,16 +3395,17 @@ struct GlobalProperties {
     bool URL : 1;
     bool URLSearchParams : 1;
     bool atob : 1;
     bool btoa : 1;
     bool Blob : 1;
     bool File : 1;
     bool crypto : 1;
     bool rtcIdentityProvider : 1;
+    bool fetch : 1;
 };
 
 // Infallible.
 already_AddRefed<nsIXPCComponents_utils_Sandbox>
 NewSandboxConstructor();
 
 // Returns true if class of 'obj' is SandboxClass.
 bool
--- a/js/xpconnect/tests/mochitest/mochitest.ini
+++ b/js/xpconnect/tests/mochitest/mochitest.ini
@@ -100,8 +100,11 @@ skip-if= buildapp == 'mulet'
 [test_crossOriginObjects.html]
 [test_crosscompartment_weakmap.html]
 [test_frameWrapping.html]
 # The JS test component we use below is only available in debug builds.
 [test_getWebIDLCaller.html]
 skip-if = (debug == false || os == "android")
 [test_nac.xhtml]
 [test_sameOriginPolicy.html]
+[test_sandbox_fetch.html]
+  support-files =
+    ../../../../dom/tests/mochitest/fetch/test_fetch_basic.js
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/mochitest/test_sandbox_fetch.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<html>
+<head>
+  <title>Fetch In JS Sandbox</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"></link>
+  <script src="test_fetch_basic.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function testHttpFetch(url) {
+  info('fetch: ' + url);
+  return fetch(new Request(url, { method: 'GET' }))
+    .then(response => {
+      is(response.status, 200, 'Response is 200');
+      is(response.url, url, 'Response URL matches');
+    });
+}
+
+function runSandboxTest(testFunc, argString) {
+  is(typeof testFunc, 'function');
+  var resolveTest;
+  var testPromise = new Promise(r => resolveTest = r);
+  var finishFuncName = 'finish_' + testFunc.name;
+  SpecialPowers.Cu.exportFunction(_ => resolvePromise(), sb,
+                                  { defineAs: finishFuncName });
+  SpecialPowers.Cu.evalInSandbox('(' + testFunc.toSource() + ')' +
+                                 '(' + argString + ')' +
+                                 '.then(' + finishFuncName + ');', sb);
+  return testPromise;
+}
+
+var origin = 'https://example.com';
+var properties = ['fetch', 'Blob', 'URL'];
+var sb = new SpecialPowers.Cu.Sandbox(origin,
+                                      { wantGlobalProperties: properties });
+
+sb.ok = SpecialPowers.Cu.exportFunction(ok, sb);
+sb.is = SpecialPowers.Cu.exportFunction(is, sb);
+sb.info = SpecialPowers.Cu.exportFunction(info, sb);
+
+Promise.resolve()
+  .then(_ => runSandboxTest(testHttpFetch, '"' + origin + window.location.pathname + '"'))
+  .then(_ => runSandboxTest(testAboutURL))
+  .then(_ => runSandboxTest(testDataURL))
+  .then(_ => runSandboxTest(testSameOriginBlobURL))
+  .then(_ => SimpleTest.finish());
+
+</script>
+</body>
+</html>