From ab753dea5fb74e54b47aca9ea8e47ef609b2a9ee Mon Sep 17 00:00:00 2001 From: Julian Kaindl Date: Wed, 20 Jul 2022 11:45:33 +0200 Subject: [PATCH 1/3] Initial commit --- .../truffle/js/test/builtins/FetchTest.java | 107 ++++++++++++++++++ .../js/builtins/ConstructorBuiltins.java | 44 +++++++ .../FetchResponseFunctionBuiltins.java | 83 ++++++++++++++ .../FetchResponsePrototypeBuiltins.java | 103 +++++++++++++++++ .../truffle/js/builtins/GlobalBuiltins.java | 52 +++++++++ .../js/builtins/helper/FetchHeaders.java | 40 +++++++ .../builtins/helper/FetchHttpConnection.java | 62 ++++++++++ .../js/builtins/helper/FetchRequest.java | 57 ++++++++++ .../js/builtins/helper/FetchResponse.java | 51 +++++++++ .../com/oracle/truffle/js/runtime/Errors.java | 5 + .../oracle/truffle/js/runtime/JSContext.java | 7 ++ .../truffle/js/runtime/JSErrorType.java | 4 + .../oracle/truffle/js/runtime/JSRealm.java | 6 + .../js/runtime/builtins/JSFetchResponse.java | 69 +++++++++++ .../builtins/JSFetchResponseObject.java | 80 +++++++++++++ 15 files changed, 770 insertions(+) create mode 100644 graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponseObject.java diff --git a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java new file mode 100644 index 00000000000..8fb55cbd8f1 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java @@ -0,0 +1,107 @@ +package com.oracle.truffle.js.test.builtins; + +import com.oracle.truffle.js.test.JSTest; +//import com.sun.net.httpserver.HttpServer; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; + +import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class FetchTest extends JSTest { + //private HttpServer testServer; + + @Before + public void testSetup() throws IOException { + /*HttpServer testServer = HttpServer.create(new InetSocketAddress(8080), 0); + + testServer.createContext("/echo", ctx -> { + byte[] body = ctx.getRequestBody().readAllBytes(); + ctx.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + testServer.createContext("/ok", ctx -> { + ctx.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); + ctx.close(); + }); + + testServer.createContext("/redirect/301", ctx -> { + ctx.sendResponseHeaders(HttpURLConnection.HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + testServer.start();*/ + } + + @After + public void testCleanup() { + //testServer.stop(0); + } + + @Test + public void testOk() { + try (Context context = JSTest.newContextBuilder().build()) { + Value result = context.eval(ID, "var x = fetch('http://localhost:8080/ok'); x.status === 200 && x.ok;"); + assertTrue(result.asBoolean()); + } + } + + @Test + public void testAllowGETRequest() { + try (Context context = JSTest.newContextBuilder().build()) { + Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'GET' })"); + } + } + + @Test + public void testAllowPOSTRequest() { + try (Context context = JSTest.newContextBuilder().build()) { + Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'POST' })"); + } + } + + @Test + public void testAllowPUTRequest() { + try (Context context = JSTest.newContextBuilder().build()) { + Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'PUT' })"); + } + } + + @Test + public void testAllowHEADRequest() { + try (Context context = JSTest.newContextBuilder().build()) { + Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'HEAD' })"); + } + } + + @Test + public void testAllowOPTIONSRequest() { + try (Context context = JSTest.newContextBuilder().build()) { + Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'OPTIONS' })"); + } + } + + @Test + public void testFollowRedirectCodes() { + try (Context context = JSTest.newContextBuilder().build()) { + Object result = context.eval(ID, "fetch('http://localhost:8080/redirect/301')"); + result = context.eval(ID, "fetch('http://localhost:8080/redirect/302')"); + result = context.eval(ID, "fetch('http://localhost:8080/redirect/303')"); + result = context.eval(ID, "fetch('http://localhost:8080/redirect/304')"); + result = context.eval(ID, "fetch('http://localhost:8080/redirect/307')"); + result = context.eval(ID, "fetch('http://localhost:8080/redirect/308')"); + } + } + + +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java index 4b7264fba25..caca859e1bb 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java @@ -73,6 +73,7 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallBooleanNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallCollatorNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallDateNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallFetchResponseNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallDateTimeFormatNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallNumberFormatNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallNumberNodeGen; @@ -88,6 +89,7 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructCollatorNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDataViewNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDateNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructFetchResponseNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDateTimeFormatNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDisplayNamesNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructErrorNodeGen; @@ -129,6 +131,7 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructWebAssemblyTableNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CreateDynamicFunctionNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.PromiseConstructorNodeGen; +import com.oracle.truffle.js.builtins.helper.FetchResponse; import com.oracle.truffle.js.nodes.CompileRegexNode; import com.oracle.truffle.js.nodes.JSGuards; import com.oracle.truffle.js.nodes.JavaScriptBaseNode; @@ -216,6 +219,7 @@ import com.oracle.truffle.js.runtime.builtins.JSDateObject; import com.oracle.truffle.js.runtime.builtins.JSError; import com.oracle.truffle.js.runtime.builtins.JSErrorObject; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.builtins.JSFinalizationRegistry; import com.oracle.truffle.js.runtime.builtins.JSMap; import com.oracle.truffle.js.runtime.builtins.JSNumber; @@ -303,6 +307,7 @@ public enum Constructor implements BuiltinEnum { Error(1), RangeError(1), TypeError(1), + FetchError(1), ReferenceError(1), SyntaxError(1), EvalError(1), @@ -348,6 +353,9 @@ public enum Constructor implements BuiltinEnum { Module(1), Table(1), + // Fetch + Response(2), + // Temporal PlainTime(0), PlainDate(3), @@ -448,6 +456,12 @@ protected Object createNode(JSContext context, JSBuiltin builtin, boolean constr return createCallRequiresNew(context, builtin); } + case Response: + return construct ? (newTarget + ? ConstructFetchResponseNodeGen.create(context, builtin, true, args().newTarget().varArgs().createArgumentNodes(context)) + : ConstructFetchResponseNodeGen.create(context, builtin, false, args().function().varArgs().createArgumentNodes(context))) + : CallFetchResponseNodeGen.create(context, builtin, args().createArgumentNodes(context)); + case Collator: return construct ? (newTarget ? ConstructCollatorNodeGen.create(context, builtin, true, args().newTarget().fixedArgs(2).createArgumentNodes(context)) @@ -522,6 +536,7 @@ protected Object createNode(JSContext context, JSBuiltin builtin, boolean constr case Error: case RangeError: case TypeError: + case FetchError: case ReferenceError: case SyntaxError: case EvalError: @@ -1093,6 +1108,35 @@ protected JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { } + public abstract static class CallFetchResponseNode extends JSBuiltinNode { + public CallFetchResponseNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + @TruffleBoundary + protected Object callFetchResponse() { + JSRealm realm = getRealm(); + return JSDate.toString(realm.currentTimeMillis(), realm); + } + } + + public abstract static class ConstructFetchResponseNode extends ConstructWithNewTargetNode { + public ConstructFetchResponseNode(JSContext context, JSBuiltin builtin, boolean isNewTargetCase) { + super(context, builtin, isNewTargetCase); + } + + @Specialization + protected JSDynamicObject constructFetchResponse(JSDynamicObject newTarget, @SuppressWarnings("unused") Object[] args) { + return swapPrototype(JSFetchResponse.create(getContext(), getRealm(), new FetchResponse()), newTarget); + } + + @Override + protected JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getDatePrototype(); + } + } + public abstract static class ConstructTemporalPlainDateNode extends ConstructWithNewTargetNode { protected ConstructTemporalPlainDateNode(JSContext context, JSBuiltin builtin, boolean isNewTargetCase) { diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java new file mode 100644 index 00000000000..ec24c31e02a --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java @@ -0,0 +1,83 @@ +package com.oracle.truffle.js.builtins; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.js.nodes.function.JSBuiltin; +import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; +import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltinsFactory.FetchErrorNodeGen; +import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltinsFactory.FetchJsonNodeGen; +import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltinsFactory.FetchRedirectNodeGen; + +public class FetchResponseFunctionBuiltins extends JSBuiltinsContainer.SwitchEnum { + public static final JSBuiltinsContainer BUILTINS = new FetchResponseFunctionBuiltins(); + + protected FetchResponseFunctionBuiltins() { + super(JSFetchResponse.CLASS_NAME, FetchResponseFunction.class); + } + + public enum FetchResponseFunction implements BuiltinEnum { + error(1), + json(1), + redirect(1); + + private final int length; + + FetchResponseFunction(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + } + + @Override + protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, FetchResponseFunction builtinEnum) { + switch (builtinEnum) { + case error: + return FetchErrorNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); + case json: + return FetchJsonNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); + case redirect: + return FetchRedirectNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); + } + return null; + } + + public abstract static class FetchErrorNode extends JSBuiltinNode { + public FetchErrorNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected int error() { + return 0; + } + + } + + public abstract static class FetchJsonNode extends JSBuiltinNode { + public FetchJsonNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected int json(Object a, Object b) { + return 0; + } + } + + public abstract static class FetchRedirectNode extends JSBuiltinNode { + public FetchRedirectNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected int redirect(Object a, Object b) { + return 0; + } + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java new file mode 100644 index 00000000000..4da5f52eaee --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.builtins; + +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseValueOfNodeGen; +import com.oracle.truffle.js.nodes.function.JSBuiltin; +import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; + +/** + * Contains builtins for {@linkplain JSFetchResponse}.prototype. + */ +public final class FetchResponsePrototypeBuiltins extends JSBuiltinsContainer.SwitchEnum { + + public static final JSBuiltinsContainer BUILTINS = new FetchResponsePrototypeBuiltins(); + + protected FetchResponsePrototypeBuiltins() { + super(JSFetchResponse.PROTOTYPE_NAME, FetchResponsePrototype.class); + } + + public enum FetchResponsePrototype implements BuiltinEnum { + valueOf(0); + + private final int length; + + FetchResponsePrototype(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + } + + @Override + protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, FetchResponsePrototype builtinEnum) { + switch (builtinEnum) { + case valueOf: + return JSFetchResponseValueOfNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + } + return null; + } + + public abstract static class JSFetchResponseOperation extends JSBuiltinNode { + public JSFetchResponseOperation(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + } + + public abstract static class JSFetchResponseValueOfNode extends JSFetchResponseOperation { + + public JSFetchResponseValueOfNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected double doOperation(Object thisResponse) { + return 1.0; + } + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java index fafc006501e..0ea4b4de8b6 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java @@ -97,7 +97,11 @@ import com.oracle.truffle.js.builtins.GlobalBuiltinsFactory.JSGlobalReadFullyNodeGen; import com.oracle.truffle.js.builtins.GlobalBuiltinsFactory.JSGlobalReadLineNodeGen; import com.oracle.truffle.js.builtins.GlobalBuiltinsFactory.JSGlobalUnEscapeNodeGen; +import com.oracle.truffle.js.builtins.GlobalBuiltinsFactory.JSGlobalFetchNodeGen; import com.oracle.truffle.js.builtins.commonjs.GlobalCommonJSRequireBuiltins; +import com.oracle.truffle.js.builtins.helper.FetchHttpConnection; +import com.oracle.truffle.js.builtins.helper.FetchRequest; +import com.oracle.truffle.js.builtins.helper.FetchResponse; import com.oracle.truffle.js.builtins.helper.FloatParserNode; import com.oracle.truffle.js.builtins.helper.StringEscape; import com.oracle.truffle.js.lang.JavaScriptLanguage; @@ -131,7 +135,9 @@ import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; import com.oracle.truffle.js.runtime.builtins.JSArray; import com.oracle.truffle.js.runtime.builtins.JSArrayBuffer; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.builtins.JSFunction; +import com.oracle.truffle.js.runtime.builtins.JSOrdinary; import com.oracle.truffle.js.runtime.builtins.JSURLDecoder; import com.oracle.truffle.js.runtime.builtins.JSURLEncoder; import com.oracle.truffle.js.runtime.interop.JSInteropUtil; @@ -140,6 +146,7 @@ import com.oracle.truffle.js.runtime.objects.JSObject; import com.oracle.truffle.js.runtime.objects.JSObjectUtil; import com.oracle.truffle.js.runtime.objects.Null; +import com.oracle.truffle.js.runtime.objects.Nullish; import com.oracle.truffle.js.runtime.objects.PropertyProxy; import com.oracle.truffle.js.runtime.objects.Undefined; @@ -168,6 +175,7 @@ public enum Global implements BuiltinEnum { decodeURI(1), decodeURIComponent(1), eval(1), + fetch(2), // Annex B escape(1), @@ -215,6 +223,8 @@ protected Object createNode(JSContext context, JSBuiltin builtin, boolean constr return JSGlobalUnEscapeNodeGen.create(context, builtin, false, args().fixedArgs(1).createArgumentNodes(context)); case unescape: return JSGlobalUnEscapeNodeGen.create(context, builtin, true, args().fixedArgs(1).createArgumentNodes(context)); + case fetch: + return JSGlobalFetchNodeGen.create(context, builtin, args().fixedArgs(2).createArgumentNodes(context)); } return null; } @@ -1319,6 +1329,48 @@ protected TruffleString escape(Object value) { } } + public abstract static class JSGlobalFetchNode extends JSBuiltinNode { + + protected JSGlobalFetchNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + private JSObject buildResponseObject(FetchResponse response) { + JSObject obj = JSOrdinary.create(getContext(), getRealm()); + + JSObject.set(obj, "url", tstr(response.getUrl().toExternalForm())); + JSObject.set(obj, "redirected", response.getCounter() > 0); + JSObject.set(obj, "status", response.getStatus()); + JSObject.set(obj, "ok", response.getStatus() == 200); + JSObject.set(obj, "statusText", tstr(response.getStatusText())); + + return obj; + } + + private TruffleString tstr(String s) { + return TruffleString.fromJavaStringUncached(s, TruffleString.Encoding.UTF_8); + } + + @Specialization + protected JSObject fetch(TruffleString urlString, Object options) { + try { + JSObject parsedOptions; + if (options == Null.instance || options == Undefined.instance) { + parsedOptions = JSOrdinary.create(getContext(), getRealm()); + } else { + parsedOptions = (JSObject) options; + } + FetchRequest request = new FetchRequest(urlString, parsedOptions); + FetchResponse response = FetchHttpConnection.open(request); + return buildResponseObject(response); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + } + /** * Non-standard print()/printErr() method to write to the console. */ diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java new file mode 100644 index 00000000000..196f542dfb7 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java @@ -0,0 +1,40 @@ +package com.oracle.truffle.js.builtins.helper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FetchHeaders { + + private final Map> headers; + + public FetchHeaders (Map> init) { + headers = init; + } + + public FetchHeaders() { + headers = new HashMap<>(); + } + + public void append(String name, String value) { + headers.computeIfAbsent(name, v -> new ArrayList<>()).add(value); + } + + public void delete(String name) { + headers.remove(name); + } + + public String get(String name) { + return String.join(",", headers.get(name)); + } + + public boolean has(String name) { + return headers.containsKey(name); + } + + public void set(String name, String value) { + headers.computeIfAbsent(name, v -> new ArrayList<>()).clear(); + headers.get(name).add(value); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java new file mode 100644 index 00000000000..eedce94d8bd --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java @@ -0,0 +1,62 @@ +package com.oracle.truffle.js.builtins.helper; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.List; + +import static java.net.HttpURLConnection.HTTP_MOVED_PERM; +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_SEE_OTHER; + +public class FetchHttpConnection { + // https://fetch.spec.whatwg.org/#redirect-status + private static final List REDIRECT_STATUS = Arrays.asList(HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER, 307, 308); + + public static FetchResponse open(FetchRequest request) throws Exception { + HttpURLConnection connection; + FetchHeaders headers = new FetchHeaders(); + URL s = null; + do { + connection = (HttpURLConnection) request.getUrl().openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setRequestMethod(request.getMethod().toString()); + + int status = connection.getResponseCode(); + + if (isRedirect(status)) { + String location = connection.getHeaderField("Location"); + URL locationURL = new URL(location); + + request.incrementRedirectCount(); + request.setUrl(locationURL); + + } + } while(connection.getResponseCode() != HTTP_OK); + + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + StringBuilder body = new StringBuilder(); + String line; + while ((line = in.readLine()) != null) { + body.append(line); + body.append('\n'); + } + in.close(); + + FetchResponse response = new FetchResponse(); + response.setUrl(request.getUrl()); + response.setCounter(request.getRedirectCount()); + response.setStatusText(connection.getResponseMessage()); + response.setStatus(connection.getResponseCode()); + response.setHeaders(new FetchHeaders(connection.getHeaderFields())); + + return response; + } + + private static boolean isRedirect(int status) { + return REDIRECT_STATUS.contains(status); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java new file mode 100644 index 00000000000..f899758cd51 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java @@ -0,0 +1,57 @@ +package com.oracle.truffle.js.builtins.helper; + +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.objects.JSObject; +import com.oracle.truffle.js.runtime.objects.Null; + +import java.net.MalformedURLException; +import java.net.URL; + +public class FetchRequest { + private final int maxFollow = 20; + + private URL url; + private TruffleString method; + private int redirectCount; + private FetchHeaders headers; + + public URL getUrl() { + return url; + } + + public void setUrl(URL url) { + this.url = url; + } + + public TruffleString getMethod() { + return method; + } + + public int getRedirectCount() { + return redirectCount; + } + + public void incrementRedirectCount() throws Exception { + if (redirectCount < maxFollow) { + redirectCount++; + } else { + throw new Exception(); + } + } + + public FetchRequest(TruffleString input, JSObject init) throws MalformedURLException { + this.url = new URL(input.toString()); + + if (url.getUserInfo() != null) { + throw Errors.createFetchError(url + " includes embedded credentials\""); + } + + TruffleString k = TruffleString.fromJavaStringUncached("method", TruffleString.Encoding.UTF_8); + if (JSObject.hasProperty(init, k)) { + method = (TruffleString) JSObject.get(init, k); + } else { + method = TruffleString.fromJavaStringUncached("GET", TruffleString.Encoding.UTF_8); + } + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java new file mode 100644 index 00000000000..f1bfaab5458 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java @@ -0,0 +1,51 @@ +package com.oracle.truffle.js.builtins.helper; + +import java.net.URL; + +public class FetchResponse { + private URL url; + private int status; + private String statusText; + private int counter; + private FetchHeaders headers; + + public URL getUrl() { + return url; + } + + public void setUrl(URL url) { + this.url = url; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getStatusText() { + return statusText; + } + + public void setStatusText(String statusText) { + this.statusText = statusText; + } + + public int getCounter() { + return counter; + } + + public void setCounter(int counter) { + this.counter = counter; + } + + public FetchHeaders getHeaders() { + return headers; + } + + public void setHeaders(FetchHeaders headers) { + this.headers = headers; + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java index dc0edea785e..78596962231 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java @@ -120,6 +120,11 @@ public static JSException createURIError(String message) { return JSException.create(JSErrorType.URIError, message); } + @TruffleBoundary + public static JSException createFetchError(String message) { + return JSException.create(JSErrorType.FetchError, message); + } + @TruffleBoundary public static JSException createTypeError(String message) { return JSException.create(JSErrorType.TypeError, message); diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java index 4ba368929bf..09a30ae5fce 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java @@ -93,6 +93,7 @@ import com.oracle.truffle.js.runtime.builtins.JSDate; import com.oracle.truffle.js.runtime.builtins.JSDictionary; import com.oracle.truffle.js.runtime.builtins.JSError; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.builtins.JSFinalizationRegistry; import com.oracle.truffle.js.runtime.builtins.JSFinalizationRegistryObject; import com.oracle.truffle.js.runtime.builtins.JSFunction; @@ -445,6 +446,7 @@ public enum BuiltinFunctionKey { private final JSObjectFactory stringFactory; private final JSObjectFactory regExpFactory; private final JSObjectFactory dateFactory; + private final JSObjectFactory fetchResponseFactory; private final JSObjectFactory nonStrictArgumentsFactory; private final JSObjectFactory strictArgumentsFactory; private final JSObjectFactory callSiteFactory; @@ -619,6 +621,7 @@ protected JSContext(Evaluator evaluator, JSContextOptions contextOptions, JavaSc this.stringFactory = builder.create(JSString.INSTANCE); this.regExpFactory = builder.create(JSRegExp.INSTANCE); this.dateFactory = builder.create(JSDate.INSTANCE); + this.fetchResponseFactory = builder.create(JSFetchResponse.INSTANCE); this.symbolFactory = builder.create(JSSymbol.INSTANCE); this.mapFactory = builder.create(JSMap.INSTANCE); @@ -975,6 +978,10 @@ public final JSObjectFactory getDateFactory() { return dateFactory; } + public final JSObjectFactory getFetchResponseFactory() { + return fetchResponseFactory; + } + public final JSObjectFactory getEnumerateIteratorFactory() { return enumerateIteratorFactory; } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java index 309f4ab231a..d4133b1a0f1 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java @@ -81,6 +81,10 @@ public enum JSErrorType implements PrototypeSupplier { AggregateError, + // Fetch + FetchError, + AbortError, + // WebAssembly CompileError, LinkError, diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java index 0917e089e13..12b2203e828 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java @@ -57,6 +57,7 @@ import java.util.SplittableRandom; import java.util.WeakHashMap; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.objects.Null; import org.graalvm.collections.Pair; import org.graalvm.home.HomeFinder; @@ -270,6 +271,8 @@ public class JSRealm { private final JSDynamicObject localePrototype; private final JSFunctionObject dateConstructor; private final JSDynamicObject datePrototype; + private final JSFunctionObject fetchResponseConstructor; + private final JSDynamicObject fetchResponsePrototype; @CompilationFinal(dimensions = 1) private final JSDynamicObject[] errorConstructors; @CompilationFinal(dimensions = 1) private final JSDynamicObject[] errorPrototypes; private final JSFunctionObject callSiteConstructor; @@ -609,6 +612,9 @@ protected JSRealm(JSContext context, TruffleLanguage.Env env, JSRealm parentReal this.callFunctionObject = JSDynamicObject.getOrNull(getFunctionPrototype(), Strings.CALL); JSConstructor ctor; + ctor = JSFetchResponse.createConstructor(this); + this.fetchResponseConstructor = ctor.getFunctionObject(); + this.fetchResponsePrototype = ctor.getPrototype(); ctor = JSArray.createConstructor(this); this.arrayConstructor = ctor.getFunctionObject(); this.arrayPrototype = (JSArrayObject) ctor.getPrototype(); diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java new file mode 100644 index 00000000000..50064d55f86 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java @@ -0,0 +1,69 @@ +package com.oracle.truffle.js.runtime.builtins; + +import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltins; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltins; +import com.oracle.truffle.js.builtins.helper.FetchResponse; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.JSRealm; +import com.oracle.truffle.js.runtime.Strings; +import com.oracle.truffle.js.runtime.objects.JSDynamicObject; +import com.oracle.truffle.js.runtime.objects.JSObject; +import com.oracle.truffle.js.runtime.objects.JSObjectUtil; +import com.oracle.truffle.js.runtime.objects.JSShape; + +public final class JSFetchResponse extends JSNonProxy implements JSConstructorFactory.Default.WithFunctions, PrototypeSupplier { + public static final TruffleString CLASS_NAME = Strings.constant("Response"); + public static final TruffleString PROTOTYPE_NAME = Strings.constant("Response.prototype"); + + public static final JSFetchResponse INSTANCE = new JSFetchResponse(); + + private JSFetchResponse() { + } + + @Override + public TruffleString getClassName() { + return CLASS_NAME; + } + + @Override + public TruffleString getClassName(JSDynamicObject object) { + return getClassName(); + } + + @Override + public TruffleString getBuiltinToStringTag(JSDynamicObject object) { + return getClassName(object); + } + + public static JSConstructor createConstructor(JSRealm realm) { + return INSTANCE.createConstructorAndPrototype(realm, FetchResponseFunctionBuiltins.BUILTINS); + } + + @Override + public JSDynamicObject createPrototype(JSRealm realm, JSFunctionObject ctor) { + JSContext ctx = realm.getContext(); + + JSObject responsePrototype; + if (ctx.getEcmaScriptVersion() < 6) { + Shape protoShape = JSShape.createPrototypeShape(realm.getContext(), INSTANCE, realm.getObjectPrototype()); + responsePrototype = JSFetchResponseObject.create(protoShape, new FetchResponse()); + JSObjectUtil.setOrVerifyPrototype(ctx, responsePrototype, realm.getObjectPrototype()); + } else { + responsePrototype = JSObjectUtil.createOrdinaryPrototypeObject(realm); + } + + JSObjectUtil.putConstructorProperty(ctx, responsePrototype, ctor); + JSObjectUtil.putFunctionsFromContainer(realm, responsePrototype, FetchResponsePrototypeBuiltins.BUILTINS); + + return responsePrototype; + } + + public static JSFetchResponseObject create(JSContext context, JSRealm realm, FetchResponse response) { + JSObjectFactory factory = context.getFetchResponseFactory(); + JSFetchResponseObject obj = JSFetchResponseObject.create(factory.getShape(realm), response); + factory.initProto(obj, realm); + return context.trackAllocation(obj); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponseObject.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponseObject.java new file mode 100644 index 00000000000..8c4cf23c524 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponseObject.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.runtime.builtins; + +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.helper.FetchResponse; +import com.oracle.truffle.js.runtime.objects.JSNonProxyObject; + +@ExportLibrary(InteropLibrary.class) +public final class JSFetchResponseObject extends JSNonProxyObject { + private FetchResponse value; + + protected JSFetchResponseObject(Shape shape, FetchResponse value) { + super(shape); + this.value = value; + } + + public FetchResponse getResponseMap() { + return value; + } + + public void setResponseMap(FetchResponse value) { + this.value = value; + } + + public static JSFetchResponseObject create(Shape shape, FetchResponse value) { + return new JSFetchResponseObject(shape, value); + } + + @Override + public TruffleString getClassName() { + return JSFetchResponse.CLASS_NAME; + } + + @Override + public TruffleString getBuiltinToStringTag() { + return getClassName(); + } +} From 6be5c2f1c383ae0b26d662826eb0712485669162 Mon Sep 17 00:00:00 2001 From: Julian Kaindl Date: Thu, 8 Sep 2022 13:49:47 +0200 Subject: [PATCH 2/3] Initial implementation --- graal-js/mx.graal-js/suite.py | 1 + .../src/com/oracle/js/parser/JSErrorType.java | 1 + .../js/fetch/headers.js | 168 ++++ .../js/fetch/main.js | 26 + .../js/fetch/request.js | 192 ++++ .../js/fetch/response.js | 146 +++ .../truffle/js/test/builtins/FetchTest.java | 107 -- .../test/builtins/fetch/FetchMethodTest.java | 935 ++++++++++++++++++ .../test/builtins/fetch/FetchTestServer.java | 220 +++++ .../js/builtins/ConstructorBuiltins.java | 186 +++- .../FetchHeadersPrototypeBuiltins.java | 251 +++++ .../FetchRequestPrototypeBuiltins.java | 236 +++++ .../FetchResponseFunctionBuiltins.java | 104 +- .../FetchResponsePrototypeBuiltins.java | 150 ++- .../truffle/js/builtins/GlobalBuiltins.java | 88 +- .../truffle/js/builtins/helper/FetchBody.java | 96 ++ .../js/builtins/helper/FetchHeaders.java | 157 ++- .../builtins/helper/FetchHttpConnection.java | 257 ++++- .../js/builtins/helper/FetchRequest.java | 241 ++++- .../js/builtins/helper/FetchResponse.java | 157 ++- .../com/oracle/truffle/js/runtime/Errors.java | 14 +- .../oracle/truffle/js/runtime/JSContext.java | 32 + .../truffle/js/runtime/JSErrorType.java | 1 - .../oracle/truffle/js/runtime/JSRealm.java | 39 + .../truffle/js/runtime/builtins/JSError.java | 2 + .../js/runtime/builtins/JSFetchHeaders.java | 119 +++ .../builtins/JSFetchHeadersObject.java | 80 ++ .../js/runtime/builtins/JSFetchRequest.java | 242 +++++ .../builtins/JSFetchRequestObject.java | 80 ++ .../js/runtime/builtins/JSFetchResponse.java | 201 +++- 30 files changed, 4280 insertions(+), 249 deletions(-) create mode 100644 graal-js/src/com.oracle.truffle.js.test/js/fetch/headers.js create mode 100644 graal-js/src/com.oracle.truffle.js.test/js/fetch/main.js create mode 100644 graal-js/src/com.oracle.truffle.js.test/js/fetch/request.js create mode 100644 graal-js/src/com.oracle.truffle.js.test/js/fetch/response.js delete mode 100644 graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java create mode 100644 graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java create mode 100644 graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchTestServer.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchHeadersPrototypeBuiltins.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchRequestPrototypeBuiltins.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchBody.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeaders.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeadersObject.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequest.java create mode 100644 graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequestObject.java diff --git a/graal-js/mx.graal-js/suite.py b/graal-js/mx.graal-js/suite.py index c33603b3480..db3e2582ee8 100644 --- a/graal-js/mx.graal-js/suite.py +++ b/graal-js/mx.graal-js/suite.py @@ -265,6 +265,7 @@ ], "requires" : [ "java.desktop", + "jdk.httpserver", "jdk.unsupported", ], "annotationProcessors" : ["truffle:TRUFFLE_DSL_PROCESSOR"], diff --git a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/JSErrorType.java b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/JSErrorType.java index 1214797e6af..e2db6a61e7c 100644 --- a/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/JSErrorType.java +++ b/graal-js/src/com.oracle.js.parser/src/com/oracle/js/parser/JSErrorType.java @@ -52,6 +52,7 @@ public enum JSErrorType { SyntaxError, TypeError, URIError, + FetchError, // since ES2021 AggregateError } diff --git a/graal-js/src/com.oracle.truffle.js.test/js/fetch/headers.js b/graal-js/src/com.oracle.truffle.js.test/js/fetch/headers.js new file mode 100644 index 00000000000..5eb2348bbe6 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/fetch/headers.js @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ + +/** + * Tests of the Headers class + */ + +load('../assert.js'); + +(function shouldConformToIDL() { + const h = new Headers(); + assertTrue(Reflect.has(h, 'append')); + assertTrue(Reflect.has(h, 'delete')); + assertTrue(Reflect.has(h, 'get')); + assertTrue(Reflect.has(h, 'has')); + assertTrue(Reflect.has(h, 'set')); + assertTrue(Reflect.has(h, 'forEach')); +})(); + +(function shouldProvideAppendMethod() { + const h = new Headers(); + h.append('a', '1'); + assertSame('1', h.get('a')); +})(); + +(function shouldProvideDeleteMethod() { + const h = new Headers({a: '1'}); + h.delete('a'); + assertFalse(h.has('a')); +})(); + +(function shouldProvideGetMethod() { + const h = new Headers({a: '1'}); + assertSame('1', h.get('a')); +})(); + +(function shouldProvideSetMethod() { + const h = new Headers(); + h.set('a', '1'); + assertSame('1', h.get('a')); +})(); + +(function shouldSetOverwrite() { + const h = new Headers({a: '1'}); + h.set('a', '2'); + assertSame('2', h.get('a')); +})(); + +(function shouldProvideHasMethod() { + const h = new Headers({a: '1'}); + assertTrue(h.has('a')); + assertFalse(h.has('foo')); +})(); + +(function shouldCreateListWhenAppending() { + const h = new Headers({'a': '1'}); + h.append('a', '2'); + h.append('a', '3'); + assertSame('1, 2, 3', h.get('a')); +})(); + +(function shouldConvertHeaderNamesToLowercase() { + const h = new Headers({'A': '1', 'a': '2'}); + h.append('A', '3'); + h.append('a', '4'); + h.set('Content-Type', 'application/json'); + + assertTrue(h.has('A') && h.has('a')); + assertSame('1, 2, 3, 4', h.get('a')); + + assertSame('application/json', h.get('content-type')); + assertSame('application/json', h.get('Content-Type')); +})(); + +(function shouldAllowIteratingWithForEach() { + const headers = new Headers({'a': '1', 'b': '2', 'c': '3'}); + const result = []; + headers.forEach((val, key, _) => { + result.push(`${key}: ${val}`); + }); + assertSame("a: 1", result[0]); + assertSame("b: 2", result[1]); + assertSame("c: 3", result[2]); +})(); + +(function thisShouldBeUndefinedInForEach() { + const headers = new Headers(); + headers.forEach(function() { + assertSame(undefined, this); + }); +})(); + +(function shouldAcceptThisArgArgumentInForEach() { + const headers = new Headers(); + const thisArg = {}; + headers.forEach(function() { + assertSame(thisArg, this); + }, thisArg); +})(); + +(function shouldBeSortedByHeaderName() { + const h = new Headers({'c': '3', 'a' : '1', 'd': '4'}); + h.append('b', '2'); + + const result = [] + h.forEach((v, k) => { + result.push(`${k}: ${v}`); + }) + + assertSame('a: 1', result[0]); + assertSame('b: 2', result[1]); + assertSame('c: 3', result[2]); + assertSame('d: 4', result[3]); +})(); + +(function shouldValidateHeaders() { + // invalid header + assertThrows(() => new Headers({'': 'ok'}), TypeError); + assertThrows(() => new Headers({'HE y': 'ok'}), TypeError); + assertThrows(() => new Headers({'Hé-y': 'ok'}), TypeError); + // invalid value + assertThrows(() => new Headers({'HE-y': 'ăk'}), TypeError); +})(); + +(function shouldValidateHeadersInMethods() { + const headers = new Headers(); + assertThrows(() => headers.append('', 'ok'), TypeError); + assertThrows(() => headers.append('Hé-y', 'ok'), TypeError); + assertThrows(() => headers.append('HE-y', 'ăk'), TypeError); + assertThrows(() => headers.delete('Hé-y'), TypeError); + assertThrows(() => headers.get('Hé-y'), TypeError); + assertThrows(() => headers.has('Hé-y'), TypeError); + assertThrows(() => headers.set('Hé-y', 'ok'), TypeError); + assertThrows(() => headers.set('HE-y', 'ăk'), TypeError); +})(); + +(function shouldNormalizeValues() { + const headers = new Headers({'a': ' 1', }); + headers.append('b', '2 '); + headers.set('c', ' 3 '); + assertSame('1', headers.get('a')); + assertSame('2', headers.get('b')); + assertSame('3', headers.get('c')); +})(); + +(function shouldWrapHeadersObject() { + const h1 = new Headers({'a': '1'}); + + const h2 = new Headers(h1); + h2.set('b', '1'); + + const h3 = new Headers(h2); + h3.append('a', '2'); + + assertFalse(h1.has('b')); + assertTrue(h2.has('a')); + assertSame('1, 2', h3.get('a')); +})(); + +(function shouldRejectIncorrectConstructorArguments() { + assertThrows(() => new Headers(''), TypeError); + assertThrows(() => new Headers(0), TypeError); + assertThrows(() => new Headers(false), TypeError); +})(); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/fetch/main.js b/graal-js/src/com.oracle.truffle.js.test/js/fetch/main.js new file mode 100644 index 00000000000..61fb116772e --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/fetch/main.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ + +/** + * Test non-networking behaviour of the fetch api + */ + +load('../assert.js'); + +const url = "http://localhost:8080"; + +(function shouldExposeHeaderRequestResponse() { + assertTrue(new Headers() instanceof Headers); + assertTrue(new Request(url) instanceof Request); + assertTrue(new Response() instanceof Response); +})(); + +(function shouldSupportProperStringOutput () { + assertSame('[object Headers]', new Headers().toString()); + assertSame('[object Request]', new Request(url).toString()); + assertSame('[object Response]', new Response().toString()); +})(); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/fetch/request.js b/graal-js/src/com.oracle.truffle.js.test/js/fetch/request.js new file mode 100644 index 00000000000..dec0c508334 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/fetch/request.js @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ + +/** + * Tests of the Request class + */ + +load('../assert.js'); + +const baseURL = "http://localhost:8080"; + +(function shouldConformToIDL() { + const req = new Request(baseURL); + const set = new Set(); + //request + assertTrue(Reflect.has(set, 'size')) + assertTrue(Reflect.has(req, 'method')); + assertTrue(Reflect.has(req, 'url')); + assertTrue(Reflect.has(req, 'headers')); + assertTrue(Reflect.has(req, 'redirect')); + assertTrue(Reflect.has(req, 'referrer')); + assertTrue(Reflect.has(req, 'referrerPolicy')); + assertTrue(Reflect.has(req, 'clone')); + //body + assertTrue(Reflect.has(req, 'body')); + assertTrue(Reflect.has(req, 'bodyUsed')); + assertTrue(Reflect.has(req, 'arrayBuffer')); + assertTrue(Reflect.has(req, 'text')); + assertTrue(Reflect.has(req, 'json')); + assertTrue(Reflect.has(req, 'formData')); + assertTrue(Reflect.has(req, 'blob')); +})(); + +(function shouldSetCorrectDefaults() { + const req = new Request(baseURL); + assertSame(baseURL, req.url); + assertSame('GET', req.method); + assertSame('follow', req.redirect); + assertSame('about:client', req.referrer); + assertSame('', req.referrerPolicy); + assertSame(null, req.body); + assertSame(false, req.bodyUsed); +})(); + +(function shouldSupportWrappingOtherRequest() { + const r1 = new Request(baseURL, { + method: 'POST', + }); + + const r2 = new Request(r1, { + method: 'POST2', + }); + + assertSame(baseURL, r1.url); + assertSame(baseURL, r2.url); + assertSame('POST2', r2.method); +})(); + +(function shouldThrowErrorOnGETOrHEADWithBody() { + assertThrows(() => new Request(baseURL, {body: ''}), TypeError); + assertThrows(() => new Request(baseURL, {body: 'a'}), TypeError); + assertThrows(() => new Request(baseURL, {method: 'HEAD', body: ''}), TypeError); + assertThrows(() => new Request(baseURL, {method: 'HEAD', body: 'a'}), TypeError); + assertThrows(() => new Request(baseURL, {method: 'head', body: ''}), TypeError); + assertThrows(() => new Request(baseURL, {method: 'get', body: ''}), TypeError); + assertThrows(() => new Request(new Request(baseURL, {body: ''})), TypeError); + assertThrows(() => new Request(new Request(baseURL, {body: 'a'})), TypeError); +})(); + +(function shouldThrowErrorOnInvalidUrl() { + assertThrows(() => new Request('foobar'), TypeError); +})(); + +(function shouldThrowErrorWhenUrlIncludesCredentials() { + assertThrows(() => new Request('https://user:pass@github.com/'), TypeError); +})(); + +(function shouldDefaultToNullBody() { + const req = new Request(baseURL); + assertSame(null, req.body); + return req.text().then(result => assertSame('', result)); +})(); + +(function shouldSupportParsingHeaders() { + const req = new Request(baseURL, { + headers: { + a: '1', + 'b': 2, + } + }); + assertSame(baseURL, req.url); + assertSame('1', req.headers.get('a')); + assertSame('2', req.headers.get('b')); +})(); + +(function shouldAcceptHeadersInstance() { + const headers = new Headers({ + 'a': '1', + 'b': '2', + }); + const req = new Request(baseURL, { headers }); + assertSame(baseURL, req.url); + assertSame('1', req.headers.get('a')); + assertSame('2', req.headers.get('b')); +})(); + +// https://fetch.spec.whatwg.org/#concept-method-normalize +(function shouldNormalizeMethod() { + for (const method of ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) { + const request = new Request(baseURL, { + method: method.toLowerCase() + }); + assertSame(method, request.method); + } + + for (const method of ['patch', 'FOO', 'bar']) { + const request = new Request(baseURL, {method}); + assertSame(method, request.method); + } +})(); + +(function shouldSupportArrayBufferMethod() { + const request = new Request(baseURL, { + method: 'POST', + body: 'a=1' + }); + + assertSame(baseURL, request.url); + return request.arrayBuffer().then(result => { + assertTrue(result instanceof ArrayBuffer); + const string = String.fromCharCode.apply(null, new Uint8Array(result)); + assertSame('a=1', string); + }); +})(); + +(function shouldSupportTextMethod() { + const request = new Request(baseURL, { + method: 'POST', + body: 'a=1' + }); + + assertSame(baseURL, request.url); + return request.text().then(result => { + assertSame('a=1', result); + }); +})(); + +(function shouldSupportJsonMethod() { + const request = new Request(baseURL, { + method: 'POST', + body: '{"a":1}' + }); + + assertSame(baseURL, request.url); + return request.text().then(result => { + assertSame(1, result.a); + }); +})(); + +(function shouldSupportBlobMethod() { + console.log(".blob() not implemented") +})(); + +(function shouldSupportFormDataMethod() { + console.log(".formData() not implemented") +})(); + +(function shouldSupportCloneMethod() { + const request = new Request(baseURL, { + method: 'POST', + redirect: 'manual', + headers: { + a: '1' + }, + body: 'b=2' + }); + + const clone = request.clone(); + assertFalse(request === clone); + assertSame(baseURL, clone.url); + assertSame('POST', clone.method); + assertSame('manual', clone.redirect); + assertSame('1', clone.headers.get('a')); + return Promise.all([request.text(), clone.text()]).then(results => { + assertSame('b=2', results[0]); + assertSame('b=2', results[1]); + }); +})(); diff --git a/graal-js/src/com.oracle.truffle.js.test/js/fetch/response.js b/graal-js/src/com.oracle.truffle.js.test/js/fetch/response.js new file mode 100644 index 00000000000..c941d6cbf56 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/js/fetch/response.js @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl. + */ + +/** + * Tests of the Response class + */ + +load('../assert.js'); + +(function shouldConformToIDL() { + const res = new Response(); + assertTrue(Reflect.has(res, 'type')); + assertTrue(Reflect.has(res, 'url')); + assertTrue(Reflect.has(res, 'redirected')); + assertTrue(Reflect.has(res, 'status')); + assertTrue(Reflect.has(res, 'statusText')); + assertTrue(Reflect.has(res, 'ok')); + assertTrue(Reflect.has(res, 'headers')); + assertTrue(Reflect.has(res, 'clone')); + //static + ['error', 'redirect', 'json'].forEach(name => assertTrue(Object.getOwnPropertyNames(Response).includes(name))); + //body + assertTrue(Reflect.has(res, 'body')); + assertTrue(Reflect.has(res, 'bodyUsed')); + assertTrue(Reflect.has(res, 'arrayBuffer')); + assertTrue(Reflect.has(res, 'blob')); + assertTrue(Reflect.has(res, 'formData')); + assertTrue(Reflect.has(res, 'json')); + assertTrue(Reflect.has(res, 'text')); +})(); + +(function shouldSupportEmptyOptions() { + const res = new Response('a=1'); + return res.text().then(result => { + assertSame('a=1', result); + }); +})(); + +(function shouldSupportParsingHeaders() { + const res = new Response(null, { + headers: { + 'a': '1', + 'b': '2', + } + }); + assertSame('1', res.headers.get('a')); + assertSame('2', res.headers.get('b')); +})(); + +(function shouldSupportArrayBufferMethod() { + const res = new Response('a=1'); + return res.arrayBuffer().then(result => assertSame('a=1', result)); +})(); + +(function shouldSupportFormDataMethod() { + console.log(".formData() not implemented"); +})(); + +(function shouldSupportBlobMethod() { + console.log(".blob() not implemented"); +})(); + +(function shouldSupportJsonMethod() { + const res = new Response('{"a":1}'); + return res.json().then(result => assertSame(1, result.a)); +})(); + +(function shouldSupportTextMethod() { + const res = new Response('a=1'); + return res.text().then(result => assertSame('a=1', result)); +})(); + +(function shouldSupportCloneMethod() { + const res = new Response('a=1', { + headers: { + b: '2' + }, + url: 'http://localhost:8080', + status: 346, + statusText: 'production' + }); + + const clone = res.clone(); + assertFalse(res === clone); + assertSame('2', clone.headers.get('b')); + assertSame('http://localhost:8080', clone.url); + assertSame(346, clone.status); + assertSame('production', clone.statusText); + assertFalse(clone.ok); + + return Promise.all([res.text(), clone.text()]).then(results => { + assertSame('a=1', results[0]); + assertSame('a=1', results[1]); + }); +})(); + +(function shouldDefaultToNullAsBody() { + const res = new Response(); + assertSame(null, res.body); + return res.text().then(result => assertSame('', result)); +})(); + +(function shouldDefaultTo200AsStatus() { + const res = new Response(); + assertSame(200, res.status); + assertTrue(res.ok); +})(); + +(function shouldDefaultToEmptyStringAsUrl() { + const res = new Response(); + assertSame('', res.url); +})(); + +(function shouldDefaultToEmptyStringAsUrl() { + const res = new Response(); + assertSame('', res.url); +})(); + +(function shouldSetDefaultType() { + const res = new Response(); + assertSame('default', res.type); +})(); + +(function shouldSupportStaticErrorMethod() { + const res = Response.error(); + assertTrue(res instanceof Response); + assertSame('error', res.type); + assertSame(0, res.status); + assertSame('', res.statusText); +})(); + +(function shouldSupportStaticRedirectMethod() { + const url = 'http://localhost:8080'; + const res = Response.redirect(url, 301); + assertTrue(res instanceof Response); + assertSame(url, res.headers.get('Location')); + assertSame(301, res.status); + // reject non-redirect codes + assertThrows(() => Response.redirect(url, 200), RangeError); + // reject invalid url + assertThrows(() => Response.redirect('foobar', 200), TypeError); +})(); diff --git a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java deleted file mode 100644 index 8fb55cbd8f1..00000000000 --- a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/FetchTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.oracle.truffle.js.test.builtins; - -import com.oracle.truffle.js.test.JSTest; -//import com.sun.net.httpserver.HttpServer; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Value; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; - -import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class FetchTest extends JSTest { - //private HttpServer testServer; - - @Before - public void testSetup() throws IOException { - /*HttpServer testServer = HttpServer.create(new InetSocketAddress(8080), 0); - - testServer.createContext("/echo", ctx -> { - byte[] body = ctx.getRequestBody().readAllBytes(); - ctx.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); - ctx.getResponseBody().write(body); - ctx.close(); - }); - - testServer.createContext("/ok", ctx -> { - ctx.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); - ctx.close(); - }); - - testServer.createContext("/redirect/301", ctx -> { - ctx.sendResponseHeaders(HttpURLConnection.HTTP_MOVED_PERM, 0); - ctx.close(); - }); - - testServer.start();*/ - } - - @After - public void testCleanup() { - //testServer.stop(0); - } - - @Test - public void testOk() { - try (Context context = JSTest.newContextBuilder().build()) { - Value result = context.eval(ID, "var x = fetch('http://localhost:8080/ok'); x.status === 200 && x.ok;"); - assertTrue(result.asBoolean()); - } - } - - @Test - public void testAllowGETRequest() { - try (Context context = JSTest.newContextBuilder().build()) { - Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'GET' })"); - } - } - - @Test - public void testAllowPOSTRequest() { - try (Context context = JSTest.newContextBuilder().build()) { - Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'POST' })"); - } - } - - @Test - public void testAllowPUTRequest() { - try (Context context = JSTest.newContextBuilder().build()) { - Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'PUT' })"); - } - } - - @Test - public void testAllowHEADRequest() { - try (Context context = JSTest.newContextBuilder().build()) { - Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'HEAD' })"); - } - } - - @Test - public void testAllowOPTIONSRequest() { - try (Context context = JSTest.newContextBuilder().build()) { - Object result = context.eval(ID, "fetch('http://localhost:8080/echo', { method: 'OPTIONS' })"); - } - } - - @Test - public void testFollowRedirectCodes() { - try (Context context = JSTest.newContextBuilder().build()) { - Object result = context.eval(ID, "fetch('http://localhost:8080/redirect/301')"); - result = context.eval(ID, "fetch('http://localhost:8080/redirect/302')"); - result = context.eval(ID, "fetch('http://localhost:8080/redirect/303')"); - result = context.eval(ID, "fetch('http://localhost:8080/redirect/304')"); - result = context.eval(ID, "fetch('http://localhost:8080/redirect/307')"); - result = context.eval(ID, "fetch('http://localhost:8080/redirect/308')"); - } - } - - -} diff --git a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java new file mode 100644 index 00000000000..9d1cd9dfc63 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java @@ -0,0 +1,935 @@ +/* + * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.test.builtins.fetch; + +import com.oracle.truffle.js.builtins.helper.FetchHttpConnection; +import com.oracle.truffle.js.runtime.JSContextOptions; +import com.oracle.truffle.js.test.JSTest; +import com.oracle.truffle.js.test.interop.AsyncInteropTest.TestOutput; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.Value; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID; +import static org.junit.Assert.*; + +/** + * Tests for the fetch builtin. + */ +public class FetchMethodTest extends JSTest { + private static FetchTestServer localServer; + + @BeforeClass + public static void testSetup() throws IOException { + localServer = new FetchTestServer(8080); + localServer.start(); + } + + @AfterClass + public static void testCleanup() { + localServer.stop(); + } + + @Test + public void testReturnsPromise() { + String out = async( + "const res = fetch('http://localhost:8080/200');" + + "console.log(res instanceof Promise);" + ); + assertEquals("true\n", out); + } + + @Test + public void test200() { + String out = async( + "const res = await fetch('http://localhost:8080/200');" + + "console.log(res.ok, res.status, res.statusText);" + ); + assertEquals("true 200 OK\n", out); + } + + @Test + public void testHandleClientErrorResponse() { + String out = async( + "const res = await fetch('http://localhost:8080/error/400');" + + log("res.headers.get('content-type')") + + log("res.status") + + log("res.statusText") + + "const result = await res.text();" + + log("result") + ); + assertEquals("text/plain\n400\nBad Request\nclient error\n", out); + } + + @Test + public void testHandleServerErrorResponse() { + String out = async( + "const res = await fetch('http://localhost:8080/error/500');" + + log("res.headers.get('content-type')") + + log("res.status") + + log("res.statusText") + + "const result = await res.text();" + + log("result") + ); + assertEquals("text/plain\n500\nInternal Server Error\nserver error\n", out); + } + + @Test + public void testResolvesIntoResponseObject() { + String out = async( + "const res = await fetch('http://localhost:8080/200');" + + log("res instanceof Response") + + log("res.headers instanceof Headers") + + log("res.status") + + log("res.ok") + + log("res.statusText") + ); + assertEquals("true\ntrue\n200\ntrue\nOK\n", out); + } + + @Test + public void testRejectUnsupportedProtocol() { + String out = asyncThrows( + "const res = await fetch('ftp://example.com/');" + ); + assertEquals("fetch cannot load ftp://example.com/. Scheme not supported: ftp\n", out); + } + + @Test(timeout = 5000) + public void testRejectOnNetworkFailure() { + String out = asyncThrows( + "const res = await fetch('http://localhost:50000');" + ); + assertEquals("Connection refused\n", out); + } + + @Test + public void testAcceptPlainTextResponse() { + String out = async( + "const res = await fetch('http://localhost:8080/plain');" + + "const result = await res.text();" + + log("res.headers.get('content-type')") + + log("res.bodyUsed") + + log("result") + ); + assertEquals("text/plain\ntrue\ntext\n", out); + } + + @Test + public void testAcceptHtmlResponse() { + String out = async( + "const res = await fetch('http://localhost:8080/html');" + + "const result = await res.text();" + + log("res.headers.get('content-type')") + + log("res.bodyUsed") + + log("result") + ); + assertEquals("text/html\ntrue\n\n", out); + } + + @Test + public void testAcceptJsonResponse() { + String out = async( + "const res = await fetch('http://localhost:8080/json');" + + "const result = await res.json();" + + log("res.headers.get('content-type')") + + log("res.bodyUsed") + + log("result.name") + ); + assertEquals("application/json\ntrue\nvalue\n", out); + } + + @Test + public void testSendRequestWithCustomHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', {" + + " headers: { 'x-custom-header': 'abc' }" + + "});" + + "const result = await res.json();" + + log("result.headers['x-custom-header']") + ); + assertEquals("abc\n", out); + } + + @Test + public void testCustomHostHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', {" + + " headers: { 'host': 'example.com' }" + + "});" + + "const result = await res.json();" + + log("result.headers.host") + ); + assertEquals("example.com\n", out); + } + + @Test + @Ignore("HttpURLConnection does not support custom methods") + public void testCustomMethod() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', {method: 'foo'});" + + "const result = await res.json();" + + log("result.method === 'foo'") + ); + assertEquals("true\n", out); + } + + @Test + public void testFollowRedirectCode301() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301');" + + log("res.url, res.status") + ); + assertEquals("http://localhost:8080/inspect 200\n", out); + } + + @Test + public void testFollowRedirectCode302() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/302');" + + log("res.url, res.status") + ); + assertEquals("http://localhost:8080/inspect 200\n", out); + } + + @Test + public void testFollowRedirectCode303() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/303');" + + log("res.url, res.status") + ); + assertEquals("http://localhost:8080/inspect 200\n", out); + } + + @Test + public void testFollowRedirectCode307() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/307');" + + log("res.url, res.status") + ); + assertEquals("http://localhost:8080/inspect 200\n", out); + } + + @Test + public void testFollowRedirectCode308() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/308');" + + log("res.url, res.status") + ); + assertEquals("http://localhost:8080/inspect 200\n", out); + } + + @Test + public void testRedirectChain() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/chain');" + + log("res.url, res.status") + ); + assertEquals("http://localhost:8080/inspect 200\n", out); + } + + @Test + public void testFollowPOSTRequestRedirectWithGET() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "method: 'POST'" + + "});" + + "const result = await res.json();" + + log("res.url, res.status") + + log("result.method, result.body === ''") + ); + assertEquals("http://localhost:8080/inspect 200\nGET true\n", out); + } + + @Test + @Ignore("HttpURLConnection does not support PATCH") + public void testFollowPATCHRequestRedirectWithPATCH() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "method: 'PATCH'" + + "});" + + "const result = await res.json();" + + log("res.url, res.status") + + log("result.method, result.body === ''") + ); + assertEquals("http://localhost:8080/inspect 200\nPATCH true\n", out); + } + + @Test + public void testFollow303WithGET() throws IOException { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/303', {" + + "method: 'PUT'" + + "});" + + "const result = await res.json();" + + log("res.url, res.status") + + log("result.method, result.body === ''") + ); + assertEquals("http://localhost:8080/inspect 200\nGET true\n", out); + } + + @Test + public void testMaximumFollowsReached() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/redirect/chain', {" + + "follow: 1" + + "});" + ); + assertEquals("maximum redirect reached at: http://localhost:8080/redirect/chain\n", out); + } + + @Test + public void testNoRedirectsAllowed() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "follow: 0" + + "});" + ); + assertEquals("maximum redirect reached at: http://localhost:8080/redirect/301\n", out); + } + + @Test + public void testRedirectModeManual() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "redirect: 'manual'" + + "});" + + log("res.url") + + log("res.status") + + log("res.headers.get('location')") + ); + assertEquals("http://localhost:8080/redirect/301\n301\n/inspect\n", out); + } + + @Test + public void testRedirectModeManualBrokenLocationHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/bad-location', {" + + "redirect: 'manual'" + + "});" + + log("res.url") + + log("res.status") + + log("res.headers.get('location')") + ); + assertEquals("http://localhost:8080/redirect/bad-location\n301\n<>\n", out); + } + + @Test + public void testRedirectModeManualOtherHost() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/other-host', {" + + "redirect: 'manual'" + + "});" + + log("res.url") + + log("res.status") + + log("res.headers.get('location')") + ); + assertEquals("http://localhost:8080/redirect/other-host\n301\nhttps://github.com/oracle/graaljs\n", out); + } + + @Test + public void testRedirectModeManualNoRedirect() { + String out = async( + "const res = await fetch('http://localhost:8080/200', {" + + "redirect: 'manual'" + + "});" + + log("res.url") + + log("res.status") + + log("res.headers.has('location')") + ); + assertEquals("http://localhost:8080/200\n200\nfalse\n", out); + } + + @Test + public void testRedirectModeError() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "redirect: 'error'" + + "});" + ); + assertEquals("uri requested responds with a redirect, redirect mode is set to error\n", out); + } + + @Test + public void testFollowRedirectAndKeepHeaders() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "headers: {'x-custom-header': 'abc'}" + + "});" + + "const result = await res.json();" + + log("res.url") + + log("result.headers['x-custom-header']") + ); + assertEquals("http://localhost:8080/inspect\nabc\n", out); + } + + @Test(timeout = 5000) + public void testDontForwardSensitiveToDifferentHost() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/other-domain', {" + + "headers: {" + + "'authorization': 'gets=removed'," + + "'cookie': 'gets=removed'," + + "'cookie2': 'gets=removed'," + + "'www-authenticate': 'gets=removed'," + + "'safe-header': 'gets=forwarded'" + + "}" + + "});" + + "const headers = new Headers((await res.json()).headers);" + + log("headers.has('authorization')") + + log("headers.has('cookie')") + + log("headers.has('cookie2')") + + log("headers.has('www-authenticate')") + + log("headers.get('safe-header')") + ); + assertEquals("false\nfalse\nfalse\nfalse\ngets=forwarded\n", out); + } + + @Test + public void testForwardSensitiveHeadersToSameHost() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301', {" + + "headers: {" + + "'authorization': 'gets=forwarded'," + + "'cookie': 'gets=forwarded'," + + "'cookie2': 'gets=forwarded'," + + "'www-authenticate': 'gets=forwarded'," + + "'safe-header': 'gets=forwarded'" + + "}" + + "});" + + "const headers = new Headers((await res.json()).headers);" + + log("headers.get('authorization')") + + log("headers.get('cookie')") + + log("headers.get('cookie2')") + + log("headers.get('www-authenticate')") + + log("headers.get('safe-header')") + ); + assertEquals("gets=forwarded\ngets=forwarded\ngets=forwarded\ngets=forwarded\ngets=forwarded\n", out); + } + + @Test + public void testIsDomainOrSubDomain() throws MalformedURLException { + // forward headers to same (sub)domain + assertTrue(FetchHttpConnection.isDomainOrSubDomain(new URL("http://a.com"), new URL("http://a.com"))); + assertTrue(FetchHttpConnection.isDomainOrSubDomain(new URL("http://a.com"), new URL("http://www.a.com"))); + assertTrue(FetchHttpConnection.isDomainOrSubDomain(new URL("http://a.com"), new URL("http://foo.bar.a.com"))); + // dont forward to parent domain, another sibling or a unrelated domain + assertFalse(FetchHttpConnection.isDomainOrSubDomain(new URL("http://b.com"), new URL("http://a.com"))); + assertFalse(FetchHttpConnection.isDomainOrSubDomain(new URL("http://www.a.com"), new URL("http://a.com"))); + assertFalse(FetchHttpConnection.isDomainOrSubDomain(new URL("http://bob.uk.com"), new URL("http://uk.com"))); + assertFalse(FetchHttpConnection.isDomainOrSubDomain(new URL("http://bob.uk.com"), new URL("http://xyz.uk.com"))); + } + + @Test(timeout = 5000) + public void testDontForwardSensitiveHeadersToDifferentProtocol() { + String out = async( + "const res = await fetch('https://httpbin.org/redirect-to?url=http%3A%2F%2Fhttpbin.org%2Fget&status_code=302', {" + + "headers: {" + + "'authorization': 'gets=removed'," + + "'cookie': 'gets=removed'," + + "'cookie2': 'gets=removed'," + + "'www-authenticate': 'gets=removed'," + + "'safe-header': 'gets=forwarded'" + + "}" + + "});" + + "const headers = new Headers((await res.json()).headers);" + + log("headers.has('authorization')") + + log("headers.has('cookie')") + + log("headers.has('cookie2')") + + log("headers.has('www-authenticate')") + + log("headers.get('safe-header')") + ); + assertEquals("false\nfalse\nfalse\nfalse\ngets=forwarded\n", out); + } + + @Test + public void testIsSameProtocol() throws MalformedURLException { + // forward headers to same protocol + assertTrue(FetchHttpConnection.isSameProtocol(new URL("http://a.com"), new URL("http://a.com"))); + assertTrue(FetchHttpConnection.isSameProtocol(new URL("https://a.com"), new URL("https://a.com"))); + // dont forward to different protocol + assertFalse(FetchHttpConnection.isSameProtocol(new URL("http://b.com"), new URL("https://b.com"))); + assertFalse(FetchHttpConnection.isSameProtocol(new URL("http://a.com"), new URL("https://www.a.com"))); + } + + @Test + public void testBrokenRedirectNormalResponseInFollowMode() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/no-location');" + + log("res.url") + + log("res.status") + + log("res.headers.get('location')") + ); + assertEquals("http://localhost:8080/redirect/no-location\n301\nundefined\n", out); + } + + @Test + public void testBrokenRedirectNormalResponseInManualMode() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/no-location', { redirect: 'manual' });" + + log("res.url") + + log("res.status") + + log("res.headers.get('location')") + ); + assertEquals("http://localhost:8080/redirect/no-location\n301\nundefined\n", out); + } + + @Test + public void testRejectInvalidRedirect() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/redirect/301/invalid-url');" + ); + assertEquals("invalid url in location header\n", out); + } + + @Test + public void testProcessInvalidRedirectInManualMode() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301/invalid-url', { redirect: 'manual' });" + + log("res.url") + + log("res.status") + + log("res.headers.get('location')") + ); + assertEquals("http://localhost:8080/redirect/301/invalid-url\n301\n//super:invalid:url%/\n", out); + } + + @Test + public void testSetRedirectedPropertyWhenRedirected() { + String out = async( + "const res = await fetch('http://localhost:8080/redirect/301/');" + + log("res.url") + + log("res.status") + + log("res.redirected") + ); + assertEquals("http://localhost:8080/inspect\n200\ntrue\n", out); + } + + @Test + public void testDontSetRedirectedPropertyWithoutRedirect() { + String out = async( + "const res = await fetch('http://localhost:8080/200');" + + log("res.url") + + log("res.redirected") + ); + assertEquals("http://localhost:8080/200\nfalse\n", out); + } + + @Test + public void testHandleDNSErrorResponse() { + String out = async( + "const res = await fetch('http://domain.invalid');" + ); + assertEquals("", out); + } + + @Test + public void testRejectInvalidJsonResponse() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/error/json');" + + log("res.headers.get('content-type')") + + "const result = await res.json();" + ); + assertEquals("application/json\nUnexpected token < in JSON at position 0\n", out); + } + + @Test + public void testResponseWithoutStatusText() { + String out = async( + "const res = await fetch('http://localhost:8080/no-status-text');" + + log("res.statusText === ''") + ); + assertEquals("true\n", out); + } + + @Test + public void testNoContentResponse204() { + String out = async( + "const res = await fetch('http://localhost:8080/no-content');" + + log("res.status, res.statusText, res.ok") + + "const result = await res.text();" + + log("result === ''") + ); + assertEquals("204 No Content true\ntrue\n", out); + } + + @Test + public void testNotModifierResponse304() { + String out = async( + "const res = await fetch('http://localhost:8080/not-modified');" + + log("res.status, res.statusText, res.ok") + + "const result = await res.text();" + + log("result === ''") + ); + assertEquals("304 Not Modified false\ntrue\n", out); + } + + @Test + public void testSetDefaultUserAgent() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect');" + + "const result = await res.json();" + + log("result.headers['user-agent']") + ); + assertEquals("graaljs-fetch\n", out); + } + + @Test + public void testSetCustomUserAgent() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', {" + + "headers: { 'user-agent': 'faked' }" + + "});" + + "const result = await res.json();" + + log("result.headers['user-agent']") + ); + assertEquals("faked\n", out); + } + + @Test + public void testSetDefaultAcceptHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect');" + + "const result = await res.json();" + + log("result.headers['accept']") + ); + assertEquals("*/*\n", out); + } + + @Test + public void testSetCustomAcceptHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', {" + + "headers: { 'accept': 'application/json' }" + + "});" + + "const result = await res.json();" + + log("result.headers['accept']") + ); + assertEquals("application/json\n", out); + } + + @Test + public void testPOSTRequest() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'POST' });" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['content-length'] === '0'") + ); + assertEquals("POST\ntrue\n", out); + } + + @Test + public void testPOSTRequestWithStringBody() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'POST', body: 'a=1' });" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['content-length'] === '3'") + + log("result.headers['content-type'] === 'text/plain;charset=UTF-8'") + ); + assertEquals("POST\ntrue\ntrue\n", out); + } + + @Test + public void testPOSTRequestWithObjectBody() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'POST', body: {a: 1} });" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['content-length'] === '15'") + + log("result.headers['content-type'] === 'text/plain;charset=UTF-8'") + ); + assertEquals("POST\ntrue\ntrue\n", out); + } + + @Test + public void testShouldOverriteContentLengthIfAble() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'POST', body: 'a=1', headers: { 'Content-Length': '1000' } });" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['content-length'] === '3'") + + log("result.headers['content-type'] === 'text/plain;charset=UTF-8'") + ); + assertEquals("POST\ntrue\ntrue\n", out); + } + + @Test + public void testShouldAllowPUTRequest() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'PUT', body: 'a=1'});" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['content-length'] === '3'") + + log("result.headers['content-type'] === 'text/plain;charset=UTF-8'") + ); + assertEquals("PUT\ntrue\ntrue\n", out); + } + + @Test + public void testShouldAllowDELETERequest() { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'DELETE' });" + + "const result = await res.json();" + + log("result.method") + ); + assertEquals("DELETE\n", out); + } + + @Test + public void testShouldAllowDELETERequestWithBody() throws InterruptedException { + String out = async( + "const res = await fetch('http://localhost:8080/inspect', { method: 'DELETE', body: 'a=1' });" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['content-length'] === '3'") + + log("result.body") + ); + assertEquals("DELETE\ntrue\na=1\n", out); + } + + @Test + public void testShouldAllowHEADRequestWithContentEncodingHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/error/404', { method: 'HEAD' });" + + log("res.status") + + log("res.headers.get('content-encoding')") + ); + assertEquals("404\ngzip\n", out); + } + + @Test + public void testShouldAllowOPTIONSRequest() { + String out = async( + "const res = await fetch('http://localhost:8080/options', { method: 'OPTIONS' });" + + log("res.status") + + log("res.headers.get('allow')") + ); + assertEquals("200\nGET, HEAD, OPTIONS\n", out); + } + + @Test + public void testShouldRejectConsumingBodyTwice() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/plain');" + + log("res.headers.get('content-type')") + + "await res.text();" + + log("res.bodyUsed") + + "await res.text()" + ); + assertEquals("text/plain\ntrue\nBody already used\n", out); + } + + @Test + public void testRejectCloneAfterBodyConsumed() { + String out = asyncThrows( + "const res = await fetch('http://localhost:8080/plain');" + + log("res.headers.get('content-type')") + + "await res.text();" + + "const res2 = res.clone();" + ); + assertEquals("text/plain\ncannot clone body after it is used\n", out); + } + + @Test + public void testAllowCloningResponseAndReadBothBodies() { + String out = async( + "const res = await fetch('http://localhost:8080/plain');" + + log("res.headers.get('content-type')") + + "const res2 = res.clone();" + + "await res.text();" + + "await res2.text();" + + log("res.bodyUsed, res2.bodyUsed") + ); + assertEquals("text/plain\ntrue true\n", out); + } + + @Test + public void testGetAllResponseValuesOfHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/cookie');" + + log("res.headers.get('set-cookie')") + + log("res.headers.get('Set-Cookie')") + ); + assertEquals("a=1, b=2\na=1, b=2\n", out); + } + + @Test + public void testDeleteHeader() { + String out = async( + "const res = await fetch('http://localhost:8080/cookie');" + + log("res.headers.has('set-cookie')") + + "res.headers.delete('set-cookie');" + + log("res.headers.has('set-cookie')") + ); + assertEquals("true\nfalse\n", out); + } + + @Test + public void testFetchWithRequestInstance() { + String out = async( + "const req = new Request('http://localhost:8080/200');" + + "const res = await fetch(req);" + + log("res.url") + ); + assertEquals("http://localhost:8080/200\n", out); + } + + @Test + public void testFetchOptionsOverwriteRequestInstance() { + String out = async( + "const req = new Request('http://localhost:8080/inspect', { method: 'POST', headers: {a:'1'} });" + + "const res = await fetch(req, { method: 'GET', headers: {a:'2'} } );" + + "const result = await res.json();" + + log("result.method") + + log("result.headers['a'] === '2'") + ); + assertEquals("GET\ntrue\n", out); + } + + @Test + public void testKeepQuestionMarkWithoutParams() { + String out = async( + "const res = await fetch('http://localhost:8080/question?');" + + log("res.url") + ); + assertEquals("http://localhost:8080/question?\n", out); + } + + @Test + public void testKeepUrlParams() { + String out = async( + "const res = await fetch('http://localhost:8080/question?a=1');" + + log("res.url") + ); + assertEquals("http://localhost:8080/question?a=1\n", out); + } + + @Test + public void testKeepHashSymbol() { + String out = async( + "const res = await fetch('http://localhost:8080/question?#');" + + log("res.url") + ); + assertEquals("http://localhost:8080/question?#\n", out); + } + + @Test(timeout = 5000) + public void testSupportsHttps() { + String out = async( + "const res = await fetch('https://github.com/', {method: 'HEAD'});" + + log("res.ok === true") + ); + assertEquals("true\n", out); + } + + @Test + public void testCustomFetchError() { + String out = async( + "const sysErr = new Error('system');" + + "const err = new FetchError('test message','test-type', sysErr);" + + log("err.message") + + log("err.type") + ); + assertEquals("test message\ntest-type\n", out); + } + + @Test + public void testRejectNetworkFailure() { + String out = asyncThrows( + "const res = await fetch('http://localhost:50000');" + ); + assertEquals("Connection refused\n", out); + } + + @Test + public void testExtractErroredSysCall() { + String out = async( + "try {" + + "const res = await fetch('http://localhost:50000');" + + "} catch (err) {" + + log("err.message") + + "}" + ); + assertEquals("Connection refused\n", out); + } + + private String async(String test) { + TestOutput out = new TestOutput(); + try (Context context = JSTest.newContextBuilder().allowHostAccess(HostAccess.ALL).err(new TestOutput()).out(out).option(JSContextOptions.CONSOLE_NAME, "true").option(JSContextOptions.INTEROP_COMPLETE_PROMISES_NAME, "false").build()) { + Value asyncFn = context.eval(ID, "" + + "(async function () {" + + test + + "})"); + asyncFn.executeVoid(); + } + return out.toString(); + } + + private String asyncThrows(String test) { + TestOutput out = new TestOutput(); + try (Context context = JSTest.newContextBuilder().allowHostAccess(HostAccess.ALL).err(new TestOutput()).out(out).option(JSContextOptions.CONSOLE_NAME, "true").option(JSContextOptions.INTEROP_COMPLETE_PROMISES_NAME, "false").build()) { + Value asyncFn = context.eval(ID, "" + + "(async function () {" + + "try {" + + test + + "}" + + "catch (error) {" + + "console.log(error.message)" + + "}" + + "})"); + asyncFn.executeVoid(); + } + return out.toString(); + } + + private String log(String code) { + return "console.log(" + code + ");"; + } +} diff --git a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchTestServer.java b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchTestServer.java new file mode 100644 index 00000000000..15552405159 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchTestServer.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.test.builtins.fetch; + +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.net.HttpURLConnection.*; + +public class FetchTestServer { + private final HttpServer server; + + public FetchTestServer(int port) throws IOException { + server = HttpServer.create(new InetSocketAddress(port), 0); + setupHandlers(); + } + + public void start() { + server.start(); + } + + public void stop() { + server.stop(0); + } + + private void setupHandlers() { + server.createContext("/inspect", ctx -> { + String method = ctx.getRequestMethod(); + String url = ctx.getRequestURI().toString(); + String headers = ctx.getRequestHeaders().entrySet().stream().map(e -> String.format("\"%s\":\"%s\"", e.getKey().toLowerCase(), String.join(", ", e.getValue()))).collect(Collectors.joining(",")); + String reqBody = new String(ctx.getRequestBody().readAllBytes()); + + // build inspect response body + String body = String.format("{\"method\":\"%s\",\"url\":\"%s\",\"headers\":{%s},\"body\": \"%s\"}", method, url, headers, reqBody); + + ctx.getResponseHeaders().set("Content-Type", "application/json"); + ctx.sendResponseHeaders(HTTP_OK, body.getBytes().length); + ctx.getResponseBody().write(body.getBytes()); + ctx.close(); + }); + + server.createContext("/200", ctx -> { + ctx.sendResponseHeaders(HTTP_OK, 0); + ctx.close(); + }); + + server.createContext("/error/400", ctx -> { + byte[] body = "client error".getBytes(); + ctx.getResponseHeaders().set("Content-Type", "text/plain"); + ctx.sendResponseHeaders(HTTP_BAD_REQUEST, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + server.createContext("/error/500", ctx -> { + byte[] body = "server error".getBytes(); + ctx.getResponseHeaders().set("Content-Type", "text/plain"); + ctx.sendResponseHeaders(HTTP_INTERNAL_ERROR, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + server.createContext("/error/json", ctx -> { + byte[] body = "".getBytes(); + ctx.getResponseHeaders().set("Content-Type", "application/json"); + ctx.sendResponseHeaders(HTTP_OK, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + server.createContext("/error/404", ctx -> { + ctx.getResponseHeaders().set("Content-Encoding", "gzip"); + ctx.sendResponseHeaders(HTTP_NOT_FOUND, -1); + ctx.close(); + }); + + server.createContext("/options", ctx -> { + byte[] body = "hello world".getBytes(); + ctx.getResponseHeaders().set("Allow", "GET, HEAD, OPTIONS"); + ctx.sendResponseHeaders(HTTP_OK, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + server.createContext("/plain", ctx -> { + byte[] body = "text".getBytes(); + ctx.getResponseHeaders().set("Content-Type", "text/plain"); + ctx.sendResponseHeaders(HTTP_OK, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + server.createContext("/html", ctx -> { + byte[] body = "".getBytes(); + ctx.getResponseHeaders().set("Content-Type", "text/html"); + ctx.sendResponseHeaders(HTTP_OK, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + server.createContext("/json", ctx -> { + byte[] body = "{\"name\":\"value\"}".getBytes(); + ctx.getResponseHeaders().set("Content-Type", "application/json"); + ctx.sendResponseHeaders(HTTP_OK, body.length); + ctx.getResponseBody().write(body); + ctx.close(); + }); + + Set.of(301, 302, 303, 307, 308).forEach(code -> { + server.createContext("/redirect/" + code, ctx -> { + ctx.getResponseHeaders().add("Location", "/inspect"); + ctx.sendResponseHeaders(code, 0); + ctx.close(); + }); + }); + + server.createContext("/redirect/bad-location", ctx -> { + ctx.getResponseHeaders().add("Location", "<>"); + ctx.sendResponseHeaders(HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + server.createContext("/redirect/other-host", ctx -> { + ctx.getResponseHeaders().add("Location", "https://github.com/oracle/graaljs"); + ctx.sendResponseHeaders(HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + server.createContext("/redirect/chain", ctx -> { + ctx.getResponseHeaders().add("Location", "/redirect/301"); + ctx.sendResponseHeaders(HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + server.createContext("/redirect/other-domain", ctx -> { + ctx.getResponseHeaders().add("Location", "https://httpbin.org/get"); + ctx.sendResponseHeaders(HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + server.createContext("/redirect/no-location", ctx -> { + ctx.sendResponseHeaders(HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + server.createContext("/redirect/301/invalid-url", ctx -> { + ctx.getResponseHeaders().add("Location", "//super:invalid:url%/"); + ctx.sendResponseHeaders(HTTP_MOVED_PERM, 0); + ctx.close(); + }); + + server.createContext("/no-status-text", ctx -> { + ctx.sendResponseHeaders(0, 0); + ctx.close(); + }); + + server.createContext("/no-content", ctx -> { + ctx.sendResponseHeaders(HTTP_NO_CONTENT, -1); + ctx.close(); + }); + + server.createContext("/not-modified", ctx -> { + ctx.sendResponseHeaders(HTTP_NOT_MODIFIED, -1); + ctx.close(); + }); + + server.createContext("/cookie", ctx -> { + ctx.getResponseHeaders().set("Set-Cookie", "a=1, b=2"); + ctx.sendResponseHeaders(HTTP_OK, 0); + ctx.close(); + }); + + server.createContext("/question", ctx -> { + ctx.sendResponseHeaders(HTTP_OK, 0); + ctx.close(); + }); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java index caca859e1bb..c2366018600 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/ConstructorBuiltins.java @@ -74,6 +74,8 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallCollatorNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallDateNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallFetchResponseNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallFetchRequestNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallFetchHeadersNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallDateTimeFormatNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallNumberFormatNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallNumberNodeGen; @@ -82,6 +84,7 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallSymbolNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CallTypedArrayNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructAggregateErrorNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructFetchErrorNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructArrayBufferNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructArrayNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructBigIntNodeGen; @@ -90,6 +93,8 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDataViewNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDateNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructFetchResponseNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructFetchRequestNodeGen; +import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructFetchHeadersNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDateTimeFormatNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructDisplayNamesNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructErrorNodeGen; @@ -131,6 +136,8 @@ import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.ConstructWebAssemblyTableNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.CreateDynamicFunctionNodeGen; import com.oracle.truffle.js.builtins.ConstructorBuiltinsFactory.PromiseConstructorNodeGen; +import com.oracle.truffle.js.builtins.helper.FetchHeaders; +import com.oracle.truffle.js.builtins.helper.FetchRequest; import com.oracle.truffle.js.builtins.helper.FetchResponse; import com.oracle.truffle.js.nodes.CompileRegexNode; import com.oracle.truffle.js.nodes.JSGuards; @@ -219,6 +226,8 @@ import com.oracle.truffle.js.runtime.builtins.JSDateObject; import com.oracle.truffle.js.runtime.builtins.JSError; import com.oracle.truffle.js.runtime.builtins.JSErrorObject; +import com.oracle.truffle.js.runtime.builtins.JSFetchHeaders; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequest; import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.builtins.JSFinalizationRegistry; import com.oracle.truffle.js.runtime.builtins.JSMap; @@ -307,7 +316,7 @@ public enum Constructor implements BuiltinEnum { Error(1), RangeError(1), TypeError(1), - FetchError(1), + FetchError(3), ReferenceError(1), SyntaxError(1), EvalError(1), @@ -355,6 +364,8 @@ public enum Constructor implements BuiltinEnum { // Fetch Response(2), + Request(2), + Headers(1), // Temporal PlainTime(0), @@ -458,9 +469,19 @@ protected Object createNode(JSContext context, JSBuiltin builtin, boolean constr case Response: return construct ? (newTarget - ? ConstructFetchResponseNodeGen.create(context, builtin, true, args().newTarget().varArgs().createArgumentNodes(context)) - : ConstructFetchResponseNodeGen.create(context, builtin, false, args().function().varArgs().createArgumentNodes(context))) + ? ConstructFetchResponseNodeGen.create(context, builtin, true, args().newTarget().fixedArgs(2).createArgumentNodes(context)) + : ConstructFetchResponseNodeGen.create(context, builtin, false, args().function().fixedArgs(2).createArgumentNodes(context))) : CallFetchResponseNodeGen.create(context, builtin, args().createArgumentNodes(context)); + case Request: + return construct ? (newTarget + ? ConstructFetchRequestNodeGen.create(context, builtin, true, args().newTarget().fixedArgs(2).createArgumentNodes(context)) + : ConstructFetchRequestNodeGen.create(context, builtin, false, args().function().fixedArgs(2).createArgumentNodes(context))) + : CallFetchRequestNodeGen.create(context, builtin, args().createArgumentNodes(context)); + case Headers: + return construct ? (newTarget + ? ConstructFetchHeadersNodeGen.create(context, builtin, true, args().newTarget().fixedArgs(1).createArgumentNodes(context)) + : ConstructFetchHeadersNodeGen.create(context, builtin, false, args().function().fixedArgs(1).createArgumentNodes(context))) + : CallFetchHeadersNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); case Collator: return construct ? (newTarget @@ -536,7 +557,6 @@ protected Object createNode(JSContext context, JSBuiltin builtin, boolean constr case Error: case RangeError: case TypeError: - case FetchError: case ReferenceError: case SyntaxError: case EvalError: @@ -553,6 +573,11 @@ protected Object createNode(JSContext context, JSBuiltin builtin, boolean constr return ConstructAggregateErrorNodeGen.create(context, builtin, true, args().newTarget().fixedArgs(3).createArgumentNodes(context)); } return ConstructAggregateErrorNodeGen.create(context, builtin, false, args().function().fixedArgs(3).createArgumentNodes(context)); + case FetchError: + if (newTarget) { + return ConstructFetchErrorNodeGen.create(context, builtin, true, args().newTarget().fixedArgs(4).createArgumentNodes(context)); + } + return ConstructFetchErrorNodeGen.create(context, builtin, false, args().function().fixedArgs(4).createArgumentNodes(context)); case TypedArray: return CallTypedArrayNodeGen.create(context, builtin, args().varArgs().createArgumentNodes(context)); @@ -1114,10 +1139,8 @@ public CallFetchResponseNode(JSContext context, JSBuiltin builtin) { } @Specialization - @TruffleBoundary protected Object callFetchResponse() { - JSRealm realm = getRealm(); - return JSDate.toString(realm.currentTimeMillis(), realm); + throw Errors.createTypeError("Class constructor Response cannot be invoked without 'new'"); } } @@ -1126,14 +1149,106 @@ public ConstructFetchResponseNode(JSContext context, JSBuiltin builtin, boolean super(context, builtin, isNewTargetCase); } + /** + * The fetch response class constructor: https://fetch.spec.whatwg.org/#dom-response. + * @param body A response body + * @param init A optional ResponseInit object https://fetch.spec.whatwg.org/#responseinit + * @return A {@linkplain JSFetchResponse} object + */ @Specialization - protected JSDynamicObject constructFetchResponse(JSDynamicObject newTarget, @SuppressWarnings("unused") Object[] args) { - return swapPrototype(JSFetchResponse.create(getContext(), getRealm(), new FetchResponse()), newTarget); + protected JSDynamicObject constructFetchResponse(JSDynamicObject newTarget, Object body, Object init) { + JSObject parsedOptions; + if (init == Null.instance || init == Undefined.instance) { + parsedOptions = JSOrdinary.create(getContext(), getRealm()); + } else { + parsedOptions = (JSObject) init; + } + return swapPrototype(JSFetchResponse.create(getContext(), getRealm(), new FetchResponse(body, parsedOptions)), newTarget); } @Override protected JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { - return realm.getDatePrototype(); + return realm.getFetchResponsePrototype(); + } + } + + public abstract static class CallFetchRequestNode extends JSBuiltinNode { + public CallFetchRequestNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object callFetchRequest() { + throw Errors.createTypeError("Class constructor Request cannot be invoked without 'new'"); + } + } + + public abstract static class ConstructFetchRequestNode extends ConstructWithNewTargetNode { + public ConstructFetchRequestNode(JSContext context, JSBuiltin builtin, boolean isNewTargetCase) { + super(context, builtin, isNewTargetCase); + } + + /** + * The fetch request class constructor: https://fetch.spec.whatwg.org/#dom-request. + * @param input A string or {@linkplain JSFetchRequest} object + * @param options A optional RequestInit object https://fetch.spec.whatwg.org/#requestinit + * @return A {@linkplain JSFetchRequest} object + */ + @Specialization + protected JSDynamicObject constructFetchRequest(JSDynamicObject newTarget, Object input, Object options, @Cached("create()") JSToStringNode toString) { + JSObject parsedOptions; + if (options == Null.instance || options == Undefined.instance) { + parsedOptions = JSOrdinary.create(getContext(), getRealm()); + } else { + parsedOptions = (JSObject) options; + } + + // Par. 5.4, constructor step 6 + // requests can wrap requests + if (JSFetchRequest.isJSFetchRequest(input) && input != Null.instance && input != Undefined.instance) { + FetchRequest request = JSFetchRequest.getInternalData((JSObject) input); + request.applyRequestInit(parsedOptions); + return swapPrototype(JSFetchRequest.create(getContext(), getRealm(), request), newTarget); + } + + TruffleString url = toString.executeString(input); + return swapPrototype(JSFetchRequest.create(getContext(), getRealm(), new FetchRequest(url, parsedOptions)), newTarget); + } + + @Override + protected JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getFetchRequestPrototype(); + } + } + + public abstract static class CallFetchHeadersNode extends JSBuiltinNode { + public CallFetchHeadersNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object callFetchHeaders() { + throw Errors.createTypeError("Class constructor Headers cannot be invoked without 'new'"); + } + } + + public abstract static class ConstructFetchHeadersNode extends ConstructWithNewTargetNode { + public ConstructFetchHeadersNode(JSContext context, JSBuiltin builtin, boolean isNewTargetCase) { + super(context, builtin, isNewTargetCase); + } + + /** + * The fetch Headers class constructor https://fetch.spec.whatwg.org/#dom-headers. + */ + @Specialization + protected JSDynamicObject constructFetchHeaders(JSDynamicObject newTarget, Object init) { + FetchHeaders headers = new FetchHeaders(init); + return swapPrototype(JSFetchHeaders.create(getContext(), getRealm(), headers), newTarget); + } + + @Override + protected JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getFetchHeadersPrototype(); } } @@ -2456,6 +2571,57 @@ private void installErrorCause(JSObject errorObj, Object options) { } } + public abstract static class ConstructFetchErrorNode extends ConstructWithNewTargetNode { + @Child private ErrorStackTraceLimitNode stackTraceLimitNode; + @Child private InitErrorObjectNode initErrorObjectNode; + @Child private DynamicObjectLibrary setMessage; + + public ConstructFetchErrorNode(JSContext context, JSBuiltin builtin, boolean isNewTargetCase) { + super(context, builtin, isNewTargetCase); + this.stackTraceLimitNode = ErrorStackTraceLimitNode.create(); + this.initErrorObjectNode = InitErrorObjectNode.create(context); + this.setMessage = JSObjectUtil.createDispatched(JSError.ERRORS_TYPE); + } + + @Specialization + protected JSDynamicObject constructError(JSDynamicObject newTarget, TruffleString message, TruffleString type, Object sysErr) { + return constructErrorImpl(newTarget, message, type, sysErr); + } + + @Specialization(guards = "!isString(message)") + protected JSDynamicObject constructError(JSDynamicObject newTarget, Object message, TruffleString type, Object sysErr, + @Cached("create()") JSToStringNode toStringNode) { + return constructErrorImpl(newTarget, message == Undefined.instance ? null : toStringNode.executeString(message), type, sysErr); + } + + private JSDynamicObject constructErrorImpl(JSDynamicObject newTarget, TruffleString messageOpt, TruffleString type, Object sysErr) { + JSRealm realm = getRealm(); + JSErrorObject errorObj = JSError.createErrorObject(getContext(), realm, JSErrorType.FetchError); + swapPrototype(errorObj, newTarget); + + setMessage.putWithFlags(errorObj, JSError.ERRORS_TYPE, type, JSError.MESSAGE_ATTRIBUTES); + + int stackTraceLimit = stackTraceLimitNode.executeInt(); + JSDynamicObject errorFunction = realm.getErrorConstructor(JSErrorType.FetchError); + + // We skip until newTarget (if any) so as to also skip user-defined Error constructors. + JSDynamicObject skipUntil = newTarget == Undefined.instance ? errorFunction : newTarget; + + GraalJSException exception = JSException.createCapture(JSErrorType.FetchError, Strings.toJavaString(messageOpt), errorObj, realm, stackTraceLimit, skipUntil, skipUntil != errorFunction); + return initErrorObjectNode.execute(errorObj, exception, messageOpt, null, sysErr); + } + + @Override + protected JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getErrorPrototype(JSErrorType.FetchError); + } + + @Override + public boolean countsTowardsStackTraceLimit() { + return false; + } + } + @ImportStatic({JSArrayBuffer.class, JSConfig.class}) public abstract static class ConstructDataViewNode extends ConstructWithNewTargetNode { public ConstructDataViewNode(JSContext context, JSBuiltin builtin, boolean isNewTargetCase) { diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchHeadersPrototypeBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchHeadersPrototypeBuiltins.java new file mode 100644 index 00000000000..c0d422ecfe7 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchHeadersPrototypeBuiltins.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.builtins; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.profiles.ConditionProfile; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.nodes.cast.JSToStringNode; +import com.oracle.truffle.js.nodes.function.JSBuiltin; +import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.nodes.function.JSFunctionCallNode; +import com.oracle.truffle.js.nodes.unary.IsCallableNode; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.JSArguments; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; +import com.oracle.truffle.js.runtime.builtins.JSFetchHeaders; +import com.oracle.truffle.js.runtime.builtins.JSFetchHeadersObject; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltinsFactory.JSFetchHeadersAppendNodeGen; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltinsFactory.JSFetchHeadersDeleteNodeGen; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltinsFactory.JSFetchHeadersGetNodeGen; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltinsFactory.JSFetchHeadersHasNodeGen; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltinsFactory.JSFetchHeadersSetNodeGen; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltinsFactory.JSFetchHeadersForEachNodeGen; +import com.oracle.truffle.js.runtime.objects.JSDynamicObject; +import com.oracle.truffle.js.runtime.objects.Undefined; + +import java.util.Map; + +/** + * Contains builtins for {@linkplain JSFetchHeaders}.prototype. + */ +public class FetchHeadersPrototypeBuiltins extends JSBuiltinsContainer.SwitchEnum { + public static final JSBuiltinsContainer BUILTINS = new FetchHeadersPrototypeBuiltins(); + + protected FetchHeadersPrototypeBuiltins() { + super(JSFetchHeaders.PROTOTYPE_NAME, FetchHeadersPrototypeBuiltins.FetchHeadersPrototype.class); + } + + public enum FetchHeadersPrototype implements BuiltinEnum { + append(2), + delete(1), + get(1), + has(1), + set(2), + forEach(1); + + private final int length; + + FetchHeadersPrototype(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + } + + @Override + protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, FetchHeadersPrototypeBuiltins.FetchHeadersPrototype builtinEnum) { + switch (builtinEnum) { + case append: + return JSFetchHeadersAppendNodeGen.create(context, builtin, args().withThis().fixedArgs(2).createArgumentNodes(context)); + case delete: + return JSFetchHeadersDeleteNodeGen.create(context, builtin, args().withThis().fixedArgs(1).createArgumentNodes(context)); + case get: + return JSFetchHeadersGetNodeGen.create(context, builtin, args().withThis().fixedArgs(1).createArgumentNodes(context)); + case has: + return JSFetchHeadersHasNodeGen.create(context, builtin, args().withThis().fixedArgs(1).createArgumentNodes(context)); + case set: + return JSFetchHeadersSetNodeGen.create(context, builtin, args().withThis().fixedArgs(2).createArgumentNodes(context)); + case forEach: + return JSFetchHeadersForEachNodeGen.create(context, builtin, args().withThis().fixedArgs(2).createArgumentNodes(context)); + } + return null; + } + + public abstract static class JSFetchHeadersOperation extends JSBuiltinNode { + private final ConditionProfile isHeaders = ConditionProfile.createBinaryProfile(); + + public JSFetchHeadersOperation(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + protected final JSFetchHeadersObject asFetchHeaders(Object object) { + if (isHeaders.profile(JSFetchHeaders.isJSFetchHeaders(object))) { + return (JSFetchHeadersObject) object; + } else { + throw Errors.createTypeError("Not a Headers object"); + } + } + } + + /** + * https://fetch.spec.whatwg.org/#dom-headers-append. + */ + public abstract static class JSFetchHeadersAppendNode extends FetchHeadersPrototypeBuiltins.JSFetchHeadersOperation { + public JSFetchHeadersAppendNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object append(Object thisHeaders, TruffleString name, TruffleString value) { + asFetchHeaders(thisHeaders).getHeadersMap().append(name.toString(), value.toString()); + return Undefined.instance; + } + } + + /** + * https://fetch.spec.whatwg.org/#dom-headers-delete. + */ + public abstract static class JSFetchHeadersDeleteNode extends FetchHeadersPrototypeBuiltins.JSFetchHeadersOperation { + public JSFetchHeadersDeleteNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object delete(Object thisHeaders, TruffleString name) { + asFetchHeaders(thisHeaders).getHeadersMap().delete(name.toString()); + return Undefined.instance; + } + } + + /** + * https://fetch.spec.whatwg.org/#dom-headers-get. + */ + public abstract static class JSFetchHeadersGetNode extends FetchHeadersPrototypeBuiltins.JSFetchHeadersOperation { + public JSFetchHeadersGetNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object get(Object thisHeaders, Object name, @Cached("create()") JSToStringNode toString) { + String value = asFetchHeaders(thisHeaders).getHeadersMap().get(toString.executeString(name).toString()); + if (value == null) { + return Undefined.instance; + } + return TruffleString.fromJavaStringUncached(value, TruffleString.Encoding.UTF_8); + } + } + + /** + * https://fetch.spec.whatwg.org/#dom-headers-has. + */ + public abstract static class JSFetchHeadersHasNode extends FetchHeadersPrototypeBuiltins.JSFetchHeadersOperation { + public JSFetchHeadersHasNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected boolean has(Object thisHeaders, Object name) { + return asFetchHeaders(thisHeaders).getHeadersMap().has(name.toString()); + } + } + + /** + * https://fetch.spec.whatwg.org/#dom-headers-set. + */ + public abstract static class JSFetchHeadersSetNode extends FetchHeadersPrototypeBuiltins.JSFetchHeadersOperation { + public JSFetchHeadersSetNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object set(Object thisHeaders, Object name, Object value) { + asFetchHeaders(thisHeaders).getHeadersMap().set(name.toString(), value.toString()); + return Undefined.instance; + } + } + + public abstract static class JSFetchHeadersForEachNode extends FetchHeadersPrototypeBuiltins.JSFetchHeadersOperation { + @Child private IsCallableNode isCallableNode; + @Child private JSFunctionCallNode callFunctionNode; + + public JSFetchHeadersForEachNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + private boolean isCallable(Object object) { + if (isCallableNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + isCallableNode = insert(IsCallableNode.create()); + } + return isCallableNode.executeBoolean(object); + } + + private Object call(Object function, Object target, Object... userArguments) { + if (callFunctionNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + callFunctionNode = insert(JSFunctionCallNode.createCall()); + } + return callFunctionNode.executeCall(JSArguments.create(target, function, userArguments)); + } + + @Specialization(guards = {"isCallable.executeBoolean(callback)"}, limit = "1") + protected Object forEachFunction(JSFetchHeadersObject thisHeaders, JSDynamicObject callback, Object thisArg, + @Cached @SuppressWarnings("unused") IsCallableNode isCallable, + @Cached("createCall()") JSFunctionCallNode callNode) { + Map entries = asFetchHeaders(thisHeaders).getHeadersMap().entries(); + + for (Map.Entry e : entries.entrySet()) { + TruffleString key = TruffleString.fromJavaStringUncached(e.getKey(), TruffleString.Encoding.UTF_8); + TruffleString value = TruffleString.fromJavaStringUncached(e.getValue(), TruffleString.Encoding.UTF_8); + callNode.executeCall(JSArguments.create(thisArg, callback, new Object[]{value, key, thisHeaders})); + } + + return Undefined.instance; + } + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchRequestPrototypeBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchRequestPrototypeBuiltins.java new file mode 100644 index 00000000000..7aa0c443147 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchRequestPrototypeBuiltins.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.builtins; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.profiles.BranchProfile; +import com.oracle.truffle.api.profiles.ConditionProfile; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.helper.FetchRequest; +import com.oracle.truffle.js.builtins.helper.TruffleJSONParser; +import com.oracle.truffle.js.nodes.control.TryCatchNode; +import com.oracle.truffle.js.nodes.function.JSBuiltin; +import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.nodes.function.JSFunctionCallNode; +import com.oracle.truffle.js.nodes.promise.NewPromiseCapabilityNode; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.JSArguments; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; +import com.oracle.truffle.js.runtime.builtins.JSArrayBuffer; +import com.oracle.truffle.js.runtime.builtins.JSArrayBufferObject; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequest; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequestObject; +import com.oracle.truffle.js.runtime.objects.JSDynamicObject; +import com.oracle.truffle.js.runtime.objects.PromiseCapabilityRecord; +import com.oracle.truffle.js.runtime.objects.Undefined; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltinsFactory.JSFetchRequestCloneNodeGen; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltinsFactory.JSFetchRequestBodyArrayBufferNodeGen; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltinsFactory.JSFetchRequestBodyBlobNodeGen; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltinsFactory.JSFetchRequestBodyFormDataNodeGen; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltinsFactory.JSFetchRequestBodyJsonNodeGen; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltinsFactory.JSFetchRequestBodyTextNodeGen; + +/** + * Contains builtins for {@linkplain JSFetchRequest}.prototype. + */ +public final class FetchRequestPrototypeBuiltins extends JSBuiltinsContainer.SwitchEnum { + public static final JSBuiltinsContainer BUILTINS = new FetchRequestPrototypeBuiltins(); + + protected FetchRequestPrototypeBuiltins() { + super(JSFetchRequest.PROTOTYPE_NAME, FetchRequestPrototypeBuiltins.FetchRequestPrototype.class); + } + + public enum FetchRequestPrototype implements BuiltinEnum { + clone(0), + // body + arrayBuffer(0), + blob(0), + formData(0), + json(0), + text(0); + + private final int length; + + FetchRequestPrototype(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + } + + @Override + protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, FetchRequestPrototypeBuiltins.FetchRequestPrototype builtinEnum) { + switch (builtinEnum) { + case clone: + return JSFetchRequestCloneNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case arrayBuffer: + return JSFetchRequestBodyArrayBufferNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case blob: + return JSFetchRequestBodyBlobNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case formData: + return JSFetchRequestBodyFormDataNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case json: + return JSFetchRequestBodyJsonNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case text: + return JSFetchRequestBodyTextNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + } + return null; + } + + public abstract static class JSFetchRequestOperation extends JSBuiltinNode { + @Child NewPromiseCapabilityNode newPromiseCapability; + @Child JSFunctionCallNode promiseResolutionCallNode; + @Child TryCatchNode.GetErrorObjectNode getErrorObjectNode; + private final BranchProfile errorBranch = BranchProfile.create(); + private final ConditionProfile isRequest = ConditionProfile.createBinaryProfile(); + + public JSFetchRequestOperation(JSContext context, JSBuiltin builtin) { + super(context, builtin); + this.newPromiseCapability = NewPromiseCapabilityNode.create(context); + this.promiseResolutionCallNode = JSFunctionCallNode.createCall(); + } + + protected JSDynamicObject toPromise(Object resolution) { + PromiseCapabilityRecord promiseCapability = newPromiseCapability.execute(getRealm().getPromiseConstructor()); + try { + promiseResolutionCallNode.executeCall(JSArguments.createOneArg(Undefined.instance, promiseCapability.getResolve(), resolution)); + } catch (AbstractTruffleException ex) { + errorBranch.enter(); + if (getErrorObjectNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + getErrorObjectNode = insert(TryCatchNode.GetErrorObjectNode.create(getContext())); + } + Object error = getErrorObjectNode.execute(ex); + promiseResolutionCallNode.executeCall(JSArguments.createOneArg(Undefined.instance, promiseCapability.getReject(), error)); + } + return promiseCapability.getPromise(); + } + + protected final JSFetchRequestObject asFetchRequest(Object object) { + if (isRequest.profile(JSFetchRequest.isJSFetchRequest(object))) { + return (JSFetchRequestObject) object; + } else { + throw Errors.createTypeError("Not a Request"); + } + } + } + + public abstract static class JSFetchRequestCloneNode extends FetchRequestPrototypeBuiltins.JSFetchRequestOperation { + public JSFetchRequestCloneNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSFetchRequestObject doOperation(Object thisRequest) { + FetchRequest request = asFetchRequest(thisRequest).getRequestMap(); + FetchRequest cloned = request.copy(); + return JSFetchRequest.create(getContext(), getRealm(), cloned); + } + } + + public abstract static class JSFetchRequestBodyArrayBufferNode extends FetchRequestPrototypeBuiltins.JSFetchRequestOperation { + public JSFetchRequestBodyArrayBufferNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSDynamicObject doOperation(Object thisRequest) { + FetchRequest request = asFetchRequest(thisRequest).getRequestMap(); + String body = request.consumeBody(); + JSArrayBufferObject arrayBuffer = JSArrayBuffer.createArrayBuffer(getContext(), getRealm(), body.getBytes()); + return toPromise(arrayBuffer); + } + } + + public abstract static class JSFetchRequestBodyBlobNode extends FetchRequestPrototypeBuiltins.JSFetchRequestOperation { + public JSFetchRequestBodyBlobNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSDynamicObject doOperation(@SuppressWarnings("unused") Object thisRequest) { + throw Errors.notImplemented("JSFetchRequestBodyBlobNode"); + } + } + + public abstract static class JSFetchRequestBodyFormDataNode extends FetchRequestPrototypeBuiltins.JSFetchRequestOperation { + public JSFetchRequestBodyFormDataNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSDynamicObject doOperation(@SuppressWarnings("unused") Object thisRequest) { + throw Errors.notImplemented("JSFetchRequestBodyFormDataNode"); + } + } + + public abstract static class JSFetchRequestBodyJsonNode extends FetchRequestPrototypeBuiltins.JSFetchRequestOperation { + public JSFetchRequestBodyJsonNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSDynamicObject doOperation(Object thisRequest) { + FetchRequest request = asFetchRequest(thisRequest).getRequestMap(); + TruffleJSONParser parser = new TruffleJSONParser(getContext()); + return toPromise(parser.parse(TruffleString.fromJavaStringUncached(request.consumeBody(), TruffleString.Encoding.UTF_8), getRealm())); + } + } + + public abstract static class JSFetchRequestBodyTextNode extends FetchRequestPrototypeBuiltins.JSFetchRequestOperation { + public JSFetchRequestBodyTextNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSDynamicObject doOperation(Object thisRequest) { + FetchRequest request = asFetchRequest(thisRequest).getRequestMap(); + String body = request.consumeBody(); + return toPromise(TruffleString.fromJavaStringUncached(body == null ? "" : body, TruffleString.Encoding.UTF_8)); + } + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java index ec24c31e02a..9b6fbec054d 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponseFunctionBuiltins.java @@ -1,14 +1,63 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.oracle.truffle.js.builtins; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.helper.FetchHttpConnection; +import com.oracle.truffle.js.builtins.helper.FetchResponse; import com.oracle.truffle.js.nodes.function.JSBuiltin; import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.runtime.Errors; import com.oracle.truffle.js.runtime.JSContext; import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltinsFactory.FetchErrorNodeGen; import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltinsFactory.FetchJsonNodeGen; import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltinsFactory.FetchRedirectNodeGen; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponseObject; +import com.oracle.truffle.js.runtime.objects.JSObject; + +import java.net.MalformedURLException; +import java.net.URL; public class FetchResponseFunctionBuiltins extends JSBuiltinsContainer.SwitchEnum { public static final JSBuiltinsContainer BUILTINS = new FetchResponseFunctionBuiltins(); @@ -18,9 +67,9 @@ protected FetchResponseFunctionBuiltins() { } public enum FetchResponseFunction implements BuiltinEnum { - error(1), - json(1), - redirect(1); + error(0), + json(2), + redirect(2); private final int length; @@ -38,46 +87,75 @@ public int getLength() { protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, FetchResponseFunction builtinEnum) { switch (builtinEnum) { case error: - return FetchErrorNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); + return FetchErrorNodeGen.create(context, builtin, args().fixedArgs(0).createArgumentNodes(context)); case json: - return FetchJsonNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); + return FetchJsonNodeGen.create(context, builtin, args().fixedArgs(2).createArgumentNodes(context)); case redirect: - return FetchRedirectNodeGen.create(context, builtin, args().fixedArgs(1).createArgumentNodes(context)); + return FetchRedirectNodeGen.create(context, builtin, args().fixedArgs(2).createArgumentNodes(context)); } return null; } + /** + * https://fetch.spec.whatwg.org/#dom-response-error. + */ public abstract static class FetchErrorNode extends JSBuiltinNode { public FetchErrorNode(JSContext context, JSBuiltin builtin) { super(context, builtin); } @Specialization - protected int error() { - return 0; + protected JSFetchResponseObject error() { + FetchResponse response = new FetchResponse(); + response.setStatus(0); + response.setStatusText(""); + response.setType(FetchResponse.FetchResponseType.error); + return JSFetchResponse.create(getContext(), getRealm(), response); } - } + /** + * https://fetch.spec.whatwg.org/#dom-response-json. + */ public abstract static class FetchJsonNode extends JSBuiltinNode { public FetchJsonNode(JSContext context, JSBuiltin builtin) { super(context, builtin); } @Specialization - protected int json(Object a, Object b) { - return 0; + protected JSFetchResponseObject json(JSObject data, JSObject init) { + FetchResponse response = new FetchResponse(data, init); + response.setContentType("application/json"); + return JSFetchResponse.create(getContext(), getRealm(), response); } } + /** + * https://fetch.spec.whatwg.org/#dom-response-redirect. + */ public abstract static class FetchRedirectNode extends JSBuiltinNode { public FetchRedirectNode(JSContext context, JSBuiltin builtin) { super(context, builtin); } @Specialization - protected int redirect(Object a, Object b) { - return 0; + protected JSFetchResponseObject redirect(TruffleString url, int status) { + FetchResponse response = new FetchResponse(); + + URL parsedURL; + try { + parsedURL = new URL(url.toString()); + } catch (MalformedURLException exp) { + throw Errors.createTypeError(exp.getMessage()); + } + + if (!FetchHttpConnection.REDIRECT_STATUS.contains(status)) { + throw Errors.createRangeError("Failed to execute redirect on response: Invalid status code"); + } + + response.setStatus(status); + response.headers.set("Location", parsedURL.toString()); + return JSFetchResponse.create(getContext(), getRealm(), response); } } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java index 4da5f52eaee..70818e04c2f 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/FetchResponsePrototypeBuiltins.java @@ -40,13 +40,36 @@ */ package com.oracle.truffle.js.builtins; +import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.dsl.Specialization; -import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseValueOfNodeGen; +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.profiles.BranchProfile; +import com.oracle.truffle.api.profiles.ConditionProfile; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseCloneNodeGen; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseBodyBlobNodeGen; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseBodyFormDataNodeGen; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseBodyJsonNodeGen; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseBodyTextNodeGen; +import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltinsFactory.JSFetchResponseBodyArrayBufferNodeGen; +import com.oracle.truffle.js.builtins.helper.FetchResponse; +import com.oracle.truffle.js.builtins.helper.TruffleJSONParser; +import com.oracle.truffle.js.nodes.control.TryCatchNode; import com.oracle.truffle.js.nodes.function.JSBuiltin; import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.nodes.function.JSFunctionCallNode; +import com.oracle.truffle.js.nodes.promise.NewPromiseCapabilityNode; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.JSArguments; import com.oracle.truffle.js.runtime.JSContext; import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; +import com.oracle.truffle.js.runtime.builtins.JSArrayBuffer; +import com.oracle.truffle.js.runtime.builtins.JSArrayBufferObject; import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponseObject; +import com.oracle.truffle.js.runtime.objects.JSDynamicObject; +import com.oracle.truffle.js.runtime.objects.PromiseCapabilityRecord; +import com.oracle.truffle.js.runtime.objects.Undefined; /** * Contains builtins for {@linkplain JSFetchResponse}.prototype. @@ -60,7 +83,13 @@ protected FetchResponsePrototypeBuiltins() { } public enum FetchResponsePrototype implements BuiltinEnum { - valueOf(0); + clone(0), + // body + arrayBuffer(0), + blob(0), + formData(0), + json(0), + text(0); private final int length; @@ -77,27 +106,132 @@ public int getLength() { @Override protected Object createNode(JSContext context, JSBuiltin builtin, boolean construct, boolean newTarget, FetchResponsePrototype builtinEnum) { switch (builtinEnum) { - case valueOf: - return JSFetchResponseValueOfNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case clone: + return JSFetchResponseCloneNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case arrayBuffer: + return JSFetchResponseBodyArrayBufferNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case blob: + return JSFetchResponseBodyBlobNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case formData: + return JSFetchResponseBodyFormDataNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case json: + return JSFetchResponseBodyJsonNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); + case text: + return JSFetchResponseBodyTextNodeGen.create(context, builtin, args().withThis().createArgumentNodes(context)); } return null; } public abstract static class JSFetchResponseOperation extends JSBuiltinNode { + @Child NewPromiseCapabilityNode newPromiseCapability; + @Child JSFunctionCallNode promiseResolutionCallNode; + @Child TryCatchNode.GetErrorObjectNode getErrorObjectNode; + private final BranchProfile errorBranch = BranchProfile.create(); + private final ConditionProfile isResponse = ConditionProfile.createBinaryProfile(); + public JSFetchResponseOperation(JSContext context, JSBuiltin builtin) { super(context, builtin); + this.newPromiseCapability = NewPromiseCapabilityNode.create(context); + this.promiseResolutionCallNode = JSFunctionCallNode.createCall(); + } + + protected JSDynamicObject toPromise(Object resolution) { + PromiseCapabilityRecord promiseCapability = newPromiseCapability.execute(getRealm().getPromiseConstructor()); + try { + promiseResolutionCallNode.executeCall(JSArguments.createOneArg(Undefined.instance, promiseCapability.getResolve(), resolution)); + } catch (AbstractTruffleException ex) { + errorBranch.enter(); + if (getErrorObjectNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + getErrorObjectNode = insert(TryCatchNode.GetErrorObjectNode.create(getContext())); + } + Object error = getErrorObjectNode.execute(ex); + promiseResolutionCallNode.executeCall(JSArguments.createOneArg(Undefined.instance, promiseCapability.getReject(), error)); + } + return promiseCapability.getPromise(); + } + + protected final JSFetchResponseObject asFetchResponse(Object object) { + if (isResponse.profile(JSFetchResponse.isJSFetchResponse(object))) { + return (JSFetchResponseObject) object; + } else { + throw Errors.createTypeError("Not a Response"); + } + } + } + + public abstract static class JSFetchResponseCloneNode extends JSFetchResponseOperation { + public JSFetchResponseCloneNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSFetchResponseObject doOperation(Object thisResponse) { + FetchResponse response = asFetchResponse(thisResponse).getResponseMap(); + FetchResponse cloned = response.copy(); + return JSFetchResponse.create(getContext(), getRealm(), cloned); + } + } + + public abstract static class JSFetchResponseBodyArrayBufferNode extends JSFetchResponseOperation { + public JSFetchResponseBodyArrayBufferNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object doOperation(Object thisResponse) { + FetchResponse response = asFetchResponse(thisResponse).getResponseMap(); + String body = response.consumeBody(); + JSArrayBufferObject arrayBuffer = JSArrayBuffer.createArrayBuffer(getContext(), getRealm(), body.getBytes()); + return toPromise(arrayBuffer); } } - public abstract static class JSFetchResponseValueOfNode extends JSFetchResponseOperation { + public abstract static class JSFetchResponseBodyBlobNode extends JSFetchResponseOperation { + public JSFetchResponseBodyBlobNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object doOperation(@SuppressWarnings("unused") Object thisResponse) { + throw Errors.notImplemented("JSFetchResponseBodyBlobNode"); + } + } + + public abstract static class JSFetchResponseBodyFormDataNode extends JSFetchResponseOperation { + public JSFetchResponseBodyFormDataNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected Object doOperation(@SuppressWarnings("unused") Object thisResponse) { + throw Errors.notImplemented("JSFetchResponseBodyFormDataNode"); + } + } + + public abstract static class JSFetchResponseBodyJsonNode extends JSFetchResponseOperation { + public JSFetchResponseBodyJsonNode(JSContext context, JSBuiltin builtin) { + super(context, builtin); + } + + @Specialization + protected JSDynamicObject doOperation(Object thisResponse) { + FetchResponse response = asFetchResponse(thisResponse).getResponseMap(); + TruffleJSONParser parser = new TruffleJSONParser(getContext()); + return toPromise(parser.parse(TruffleString.fromJavaStringUncached(response.consumeBody(), TruffleString.Encoding.UTF_8), getRealm())); + } + } - public JSFetchResponseValueOfNode(JSContext context, JSBuiltin builtin) { + public abstract static class JSFetchResponseBodyTextNode extends JSFetchResponseOperation { + public JSFetchResponseBodyTextNode(JSContext context, JSBuiltin builtin) { super(context, builtin); } @Specialization - protected double doOperation(Object thisResponse) { - return 1.0; + protected JSDynamicObject doOperation(Object thisResponse) { + FetchResponse response = asFetchResponse(thisResponse).getResponseMap(); + String body = response.consumeBody(); + return toPromise(TruffleString.fromJavaStringUncached(body == null ? "" : body, TruffleString.Encoding.UTF_8)); } } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java index 0ea4b4de8b6..955dca0dc33 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/GlobalBuiltins.java @@ -63,6 +63,7 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.InvalidArrayIndexException; @@ -114,14 +115,18 @@ import com.oracle.truffle.js.nodes.cast.JSToNumberNode; import com.oracle.truffle.js.nodes.cast.JSToStringNode; import com.oracle.truffle.js.nodes.cast.JSTrimWhitespaceNode; +import com.oracle.truffle.js.nodes.control.TryCatchNode; import com.oracle.truffle.js.nodes.function.EvalNode; import com.oracle.truffle.js.nodes.function.JSBuiltin; import com.oracle.truffle.js.nodes.function.JSBuiltinNode; +import com.oracle.truffle.js.nodes.function.JSFunctionCallNode; import com.oracle.truffle.js.nodes.function.JSLoadNode; import com.oracle.truffle.js.nodes.interop.ImportValueNode; +import com.oracle.truffle.js.nodes.promise.NewPromiseCapabilityNode; import com.oracle.truffle.js.runtime.BigInt; import com.oracle.truffle.js.runtime.Errors; import com.oracle.truffle.js.runtime.Evaluator; +import com.oracle.truffle.js.runtime.JSArguments; import com.oracle.truffle.js.runtime.JSConfig; import com.oracle.truffle.js.runtime.JSConsoleUtil; import com.oracle.truffle.js.runtime.JSContext; @@ -135,6 +140,7 @@ import com.oracle.truffle.js.runtime.builtins.BuiltinEnum; import com.oracle.truffle.js.runtime.builtins.JSArray; import com.oracle.truffle.js.runtime.builtins.JSArrayBuffer; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequest; import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.builtins.JSFunction; import com.oracle.truffle.js.runtime.builtins.JSOrdinary; @@ -146,7 +152,7 @@ import com.oracle.truffle.js.runtime.objects.JSObject; import com.oracle.truffle.js.runtime.objects.JSObjectUtil; import com.oracle.truffle.js.runtime.objects.Null; -import com.oracle.truffle.js.runtime.objects.Nullish; +import com.oracle.truffle.js.runtime.objects.PromiseCapabilityRecord; import com.oracle.truffle.js.runtime.objects.PropertyProxy; import com.oracle.truffle.js.runtime.objects.Undefined; @@ -1329,45 +1335,71 @@ protected TruffleString escape(Object value) { } } + /** + * Implementation of the Fetch API. + * Reference: https://fetch.spec.whatwg.org/commit-snapshots/9bb2ded94073377ec5d9b5e3cda391df6c769a0a/ + */ public abstract static class JSGlobalFetchNode extends JSBuiltinNode { + @Child NewPromiseCapabilityNode newPromiseCapability; + @Child JSFunctionCallNode promiseResolutionCallNode; + @Child TryCatchNode.GetErrorObjectNode getErrorObjectNode; + private final BranchProfile errorBranch = BranchProfile.create(); protected JSGlobalFetchNode(JSContext context, JSBuiltin builtin) { super(context, builtin); + this.newPromiseCapability = NewPromiseCapabilityNode.create(context); + this.promiseResolutionCallNode = JSFunctionCallNode.createCall(); } - private JSObject buildResponseObject(FetchResponse response) { - JSObject obj = JSOrdinary.create(getContext(), getRealm()); - - JSObject.set(obj, "url", tstr(response.getUrl().toExternalForm())); - JSObject.set(obj, "redirected", response.getCounter() > 0); - JSObject.set(obj, "status", response.getStatus()); - JSObject.set(obj, "ok", response.getStatus() == 200); - JSObject.set(obj, "statusText", tstr(response.getStatusText())); - - return obj; - } - - private TruffleString tstr(String s) { - return TruffleString.fromJavaStringUncached(s, TruffleString.Encoding.UTF_8); + protected JSDynamicObject toPromise(Object resolution) { + JSRealm realm = getRealm(); + PromiseCapabilityRecord promiseCapability = newPromiseCapability.execute(realm.getPromiseConstructor()); + try { + promiseResolutionCallNode.executeCall(JSArguments.createOneArg(Undefined.instance, promiseCapability.getResolve(), resolution)); + } catch (AbstractTruffleException ex) { + errorBranch.enter(); + if (getErrorObjectNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + getErrorObjectNode = insert(TryCatchNode.GetErrorObjectNode.create(getContext())); + } + Object error = getErrorObjectNode.execute(ex); + promiseResolutionCallNode.executeCall(JSArguments.createOneArg(Undefined.instance, promiseCapability.getReject(), error)); + } + return promiseCapability.getPromise(); } + /** + * The fetch method https://fetch.spec.whatwg.org/#fetch-method. + * @param input A string or {@linkplain JSFetchRequest} object + * @param options A optional ReuestInit object https://fetch.spec.whatwg.org/#requestinit + * @return returns a {@linkplain JSFetchResponse} object + */ @Specialization - protected JSObject fetch(TruffleString urlString, Object options) { + protected JSDynamicObject fetch(Object input, Object options, @Cached("create()") JSToStringNode toString) { + JSObject parsedOptions; + if (options == Null.instance || options == Undefined.instance) { + parsedOptions = JSOrdinary.create(getContext(), getRealm()); + } else { + parsedOptions = (JSObject) options; + } + + FetchRequest request; + if (JSFetchRequest.isJSFetchRequest(input)) { + request = JSFetchRequest.getInternalData((JSObject) input); + request.applyRequestInit(parsedOptions); + } else { + request = new FetchRequest(toString.executeString(input), parsedOptions); + } + + FetchResponse response = null; try { - JSObject parsedOptions; - if (options == Null.instance || options == Undefined.instance) { - parsedOptions = JSOrdinary.create(getContext(), getRealm()); - } else { - parsedOptions = (JSObject) options; - } - FetchRequest request = new FetchRequest(urlString, parsedOptions); - FetchResponse response = FetchHttpConnection.open(request); - return buildResponseObject(response); - } catch (Exception e) { - e.printStackTrace(); + FetchHttpConnection.node = this; + response = FetchHttpConnection.connect(request); + } catch (IOException e) { + throw Errors.createFetchError(e.getMessage(), "system", this); } - return null; + return toPromise(JSFetchResponse.create(getContext(), getRealm(), response)); } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchBody.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchBody.java new file mode 100644 index 00000000000..efbc1489c6b --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchBody.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.builtins.helper; + +import com.oracle.truffle.js.nodes.cast.JSToStringNode; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.objects.Null; +import com.oracle.truffle.js.runtime.objects.Undefined; + +public class FetchBody { + protected String body; + private boolean bodyUsed; + private String contentType = "text/plain;charset=UTF-8"; + + public void setBody(Object body) { + if (body == Undefined.instance || body == Null.instance) { + this.body = null; + } else { + this.body = JSToStringNode.create().executeString(body).toString(); + } + } + + public void setBody(String body) { + this.body = body; + } + + public String getBody() { + return body; + } + + public String consumeBody() { + if (!bodyUsed) { + bodyUsed = true; + return body; + } else { + throw Errors.createTypeError("Body already used"); + } + } + + public int getBodyBytes() { + if (body == null) { + return 0; + } + return body.getBytes().length; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public boolean isBodyUsed() { + return bodyUsed; + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java index 196f542dfb7..7eb63135673 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHeaders.java @@ -1,40 +1,179 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.oracle.truffle.js.builtins.helper; +import com.oracle.truffle.js.nodes.cast.JSToStringNode; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.builtins.JSFetchHeaders; +import com.oracle.truffle.js.runtime.objects.JSObject; +import com.oracle.truffle.js.runtime.objects.Null; +import com.oracle.truffle.js.runtime.objects.Undefined; + import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +/** + * Internal data structure for {@linkplain JSFetchHeaders}. + */ public class FetchHeaders { - private final Map> headers; + // sorted by header name, https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set + private final SortedMap> headers; - public FetchHeaders (Map> init) { - headers = init; + public FetchHeaders() { + headers = new TreeMap<>(); } - public FetchHeaders() { - headers = new HashMap<>(); + public FetchHeaders(Map> init) { + headers = new TreeMap<>(); + if (!init.isEmpty()) { + init.keySet().stream().filter(Objects::nonNull).forEach(k -> headers.put(k.toLowerCase(), init.get(k))); + } + } + + public FetchHeaders(Object init) { + headers = new TreeMap<>(); + + if (init == Null.instance || init == Undefined.instance) { + return; + } + + if (JSFetchHeaders.isJSFetchHeaders(init)) { + FetchHeaders initHeaders = JSFetchHeaders.getInternalData(init); + initHeaders.keys().forEach(k -> append(k, initHeaders.get(k))); + return; + } + + if (JSObject.isJSObject(init)) { + JSObject obj = (JSObject) init; + Object[] keys = JSObject.getKeyArray(obj); + + Arrays.stream(keys).forEach(k -> { + JSToStringNode toString = JSToStringNode.create(); + String name = toString.executeString(k).toString(); + String value = toString.executeString(JSObject.get(obj, k)).toString(); + append(name, value); + }); + } else { + throw Errors.createTypeError("Failed to construct 'Headers': The provided value has incorrect type"); + } } public void append(String name, String value) { - headers.computeIfAbsent(name, v -> new ArrayList<>()).add(value); + name = name.toLowerCase(); + String normalizedValue = value.trim(); + validateHeaderName(name); + validateHeaderValue(name, normalizedValue); + headers.computeIfAbsent(name, v -> new ArrayList<>()).add(normalizedValue); } public void delete(String name) { + name = name.toLowerCase(); + validateHeaderName(name); headers.remove(name); } public String get(String name) { - return String.join(",", headers.get(name)); + name = name.toLowerCase(); + validateHeaderName(name); + List match = headers.get(name); + + if (match == null) { + return null; + } + + return String.join(", ", match); } public boolean has(String name) { + name = name.toLowerCase(); + validateHeaderName(name); return headers.containsKey(name); } public void set(String name, String value) { + name = name.toLowerCase(); + String normalizedValue = value.trim(); + validateHeaderName(name); + validateHeaderValue(name, normalizedValue); headers.computeIfAbsent(name, v -> new ArrayList<>()).clear(); - headers.get(name).add(value); + headers.get(name).add(normalizedValue); + } + + public Map entries() { + return headers.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> get(e.getKey()))); + } + + public Set keys() { + return headers.keySet(); + } + + /** + * validated by field-name token production as in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2. + */ + private void validateHeaderName(String name) { + if (name.isEmpty() + || name.isBlank() + //token regex: https://github.com/nodejs/node/blob/4d8674b50f6050d5dad649dbd32ce60cbd24f362/lib/_http_common.js#L204 + || !name.matches("^[\\^_`\\w\\-!#$%&'*+.|~]+$")) { + throw Errors.createTypeError("Header name must be a valid HTTP token: [ " + name + "]"); + } + } + + /** + * validated by field-name token production as in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2. + */ + private void validateHeaderValue(String name, String value) { + // field-vchar regex: https://github.com/nodejs/node/blob/4d8674b50f6050d5dad649dbd32ce60cbd24f362/lib/_http_common.js#L214 + long matches = Pattern.compile("[^\\x09\\x20-\\x7e\\x80-\\xff]").matcher(value).results().count(); + if (matches > 0) { + throw Errors.createTypeError("Invalid character in header field [ " + name + ": " + value + "]"); + } } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java index eedce94d8bd..1a6c6aa1114 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java @@ -1,52 +1,219 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.oracle.truffle.js.builtins.helper; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.js.runtime.Errors; + import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; -import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -import static java.net.HttpURLConnection.HTTP_MOVED_PERM; -import static java.net.HttpURLConnection.HTTP_MOVED_TEMP; -import static java.net.HttpURLConnection.HTTP_OK; -import static java.net.HttpURLConnection.HTTP_SEE_OTHER; +import static java.net.HttpURLConnection.*; +/** + * Implementation of HTTP Fetch as of https://fetch.spec.whatwg.org/commit-snapshots/9bb2ded94073377ec5d9b5e3cda391df6c769a0a/. + */ public class FetchHttpConnection { + public static final Set SUPPORTED_SCHEMA = Set.of("data", "http", "https"); // https://fetch.spec.whatwg.org/#redirect-status - private static final List REDIRECT_STATUS = Arrays.asList(HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER, 307, 308); + public static final Set REDIRECT_STATUS = Set.of(HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER, 307, 308); + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + public static final Set VALID_POLICY = Set.of("", + "no-referrer", + "no-referrer-when-downgrade", + "same-origin", + "origin", + "strict-origin", + "origin-when-cross-origin", + "strict-origin-when-cross-origin", + "unsafe-url"); + public static final String DEFAULT_REFERRER_POLICY = "strict-origin-when-cross-origin"; + public static Node node = null; + + public static FetchResponse connect(FetchRequest request) throws IOException { + // to overwrite Host header (https://stackoverflow.com/a/8172736/10708558) + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + + if (node == null) { + throw new IllegalStateException("originating node is null"); + } + + if (!SUPPORTED_SCHEMA.contains(request.getUrl().getProtocol())) { + throw Errors.createTypeError("fetch cannot load " + + request.getUrl().toString() + + ". Scheme not supported: " + + request.getUrl().getProtocol() + ); + } + + // Setup Connection + HttpURLConnection connection = (HttpURLConnection) request.getUrl().openConnection(); + connection.setInstanceFollowRedirects(false); // don't follow automatically + connection.setRequestMethod(request.getMethod()); + connection.setDoOutput(true); + + // Par. 4.1, Main fetch step 7 + if (request.getReferrerPolicy().isEmpty()) { + request.setReferrerPolicy(DEFAULT_REFERRER_POLICY); + } + + // Setup Headers + setRequestHeaders(connection, request); + + // Setup Requests Body + if (request.body != null) { + OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); + out.write(request.body); + out.flush(); + out.close(); + } + + // Establish connection to the resource + // connection can now be treated as `actualResponse` as in Par. 4.3 https://fetch.spec.whatwg.org/#http-fetch + connection.connect(); + + int status = connection.getResponseCode(); + + // Par. 4.4, HTTP-redirect fetch steps https://fetch.spec.whatwg.org/#http-redirect-fetch + if (isRedirect(status)) { + URL locationURL = null; + String location = connection.getHeaderField("Location"); + + try { + // HTTP-redirect fetch step 3 + if (location != null) { + locationURL = URI.create(request.getUrl().toString()).resolve(location).toURL(); + } + } catch (IllegalArgumentException exception) { + if (!request.getRedirectMode().equals("manual")) { + throw Errors.createFetchError("invalid url in location header", "unsupported-redirect", node); + } + } - public static FetchResponse open(FetchRequest request) throws Exception { - HttpURLConnection connection; - FetchHeaders headers = new FetchHeaders(); - URL s = null; - do { - connection = (HttpURLConnection) request.getUrl().openConnection(); - connection.setInstanceFollowRedirects(false); - connection.setRequestMethod(request.getMethod().toString()); + // Par. 4.3 Http fetch step 8 + switch (request.getRedirectMode()) { + case "manual": + // return response as is + break; + case "error": + throw Errors.createFetchError("uri requested responds with a redirect, redirect mode is set to error", "no-redirect", node); + case "follow": + // HTTP-redirect fetch step 4 + if (locationURL == null) { + break; + } - int status = connection.getResponseCode(); + // HTTP-redirect fetch step 7 + if (request.getRedirectCount() >= request.getFollow()) { + throw Errors.createFetchError("maximum redirect reached at: " + request.getUrl(), "max-redirect", node); + } - if (isRedirect(status)) { - String location = connection.getHeaderField("Location"); - URL locationURL = new URL(location); + // HTTP-redirect fetch step 8 + request.incrementRedirectCount(); - request.incrementRedirectCount(); - request.setUrl(locationURL); + // remove sensitive headers if redirecting to a new domain or protocol changes + if (!isDomainOrSubDomain(locationURL, request.getUrl()) || !isSameProtocol(locationURL, request.getUrl())) { + Set.of("authorization", "www-authenticate", "cookie", "cookie2").forEach(k -> { + request.headers.delete(k); + }); + } + // HTTP-redirect fetch step 11 + if (status != 303 && request.body != null) { + throw Errors.createFetchError("Cannot follow redirect with body", "unsupported-redirect", node); + } + + // HTTP-redirect fetch step 12 + if (status == HTTP_SEE_OTHER || ((status == HTTP_MOVED_PERM || status == HTTP_MOVED_TEMP) && request.getMethod().equals("POST"))) { + request.setMethod("GET"); + request.setBody(null); + request.headers.delete("content-length"); + request.headers.delete("content-type"); + request.headers.delete("content-location"); + request.headers.delete("content-language"); + request.headers.delete("content-encoding"); + } + + // HTTP-redirect fetch step 17 + request.setUrl(locationURL); + + // HTTP-redirect fetch step 18 + // https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect + List policyTokens = connection.getHeaderFields().get("referrer-policy"); + if (policyTokens != null) { + policyTokens.stream().filter(VALID_POLICY::contains).reduce((a, b) -> b).ifPresent(request::setReferrerPolicy); + } + + // HTTP-redirect fetch step 19 invoke fetch, following the redirect + return connect(request); + default: + throw Errors.createTypeError("Redirect option " + request.getRedirectMode() + " is not a valid value of RequestRedirect"); } - } while(connection.getResponseCode() != HTTP_OK); + } - BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); - StringBuilder body = new StringBuilder(); - String line; - while ((line = in.readLine()) != null) { - body.append(line); - body.append('\n'); + // Prepare Response + InputStream inputStream = null; + try { + inputStream = connection.getInputStream(); + } catch (IOException exception) { + inputStream = connection.getErrorStream(); } - in.close(); + + BufferedReader br = null; + if (inputStream != null) { + br = new BufferedReader(new InputStreamReader(inputStream)); + } + String responseBody = br != null ? br.lines().collect(Collectors.joining()) : ""; FetchResponse response = new FetchResponse(); + response.setBody(responseBody); response.setUrl(request.getUrl()); response.setCounter(request.getRedirectCount()); response.setStatusText(connection.getResponseMessage()); @@ -56,7 +223,39 @@ public static FetchResponse open(FetchRequest request) throws Exception { return response; } - private static boolean isRedirect(int status) { + public static boolean isRedirect(int status) { return REDIRECT_STATUS.contains(status); } + + public static boolean isDomainOrSubDomain(URL destination, URL origin) { + String d = destination.getHost(); + String o = origin.getHost(); + return d.equals(o) || o.endsWith("." + d); + } + + public static boolean isSameProtocol(URL destination, URL origin) { + return destination.getProtocol().equals(origin.getProtocol()); + } + + private static void setRequestHeaders(HttpURLConnection connection, FetchRequest req) { + connection.setRequestProperty("Accept", "*/*"); + connection.setRequestProperty("Accept-Encoding", "gzip,deflate,br"); + if (req.body != null) { + int length = req.getBodyBytes(); + connection.setFixedLengthStreamingMode(length); + } else if (Set.of("POST", "PUT").contains(req.getMethod())) { + connection.setFixedLengthStreamingMode(0); + } + connection.setRequestProperty("User-Agent", "graaljs-fetch"); + + // Par. 4.6. HTTP-network-or-cache fetch step 11 + if (req.isReferrerUrl()) { + connection.setRequestProperty("Referer", req.getReferrer()); + } + + // user specified headers + req.headers.keys().forEach(key -> { + connection.setRequestProperty(key, req.headers.get(key)); + }); + } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java index f899758cd51..b93831b4a4d 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchRequest.java @@ -1,20 +1,81 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.oracle.truffle.js.builtins.helper; import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.nodes.cast.JSToInt32Node; +import com.oracle.truffle.js.nodes.cast.JSToStringNode; import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.Strings; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequest; import com.oracle.truffle.js.runtime.objects.JSObject; -import com.oracle.truffle.js.runtime.objects.Null; import java.net.MalformedURLException; import java.net.URL; +import java.util.Set; -public class FetchRequest { - private final int maxFollow = 20; +/** + * The internal data structure for {@linkplain JSFetchRequest}. + */ +public class FetchRequest extends FetchBody { + // RequestInit fields https://fetch.spec.whatwg.org/#requestinit + private static final TruffleString METHOD = Strings.constant("method"); + private static final TruffleString REFERRER = Strings.constant("referrer"); + private static final TruffleString REFERRER_POLICY = Strings.constant("referrerPolicy"); + private static final TruffleString REDIRECT = Strings.constant("redirect"); + private static final TruffleString BODY = Strings.constant("body"); + private static final TruffleString HEADERS = Strings.constant("headers"); + // Non-standard init fields + private static final TruffleString FOLLOW = Strings.constant("follow"); private URL url; - private TruffleString method; + private String method; + private String redirectMode; private int redirectCount; - private FetchHeaders headers; + private int follow; + public FetchHeaders headers; + + private String referrer; + private boolean isReferrerUrl; + private String referrerPolicy; public URL getUrl() { return url; @@ -24,34 +85,176 @@ public void setUrl(URL url) { this.url = url; } - public TruffleString getMethod() { + public String getMethod() { return method; } + public void setMethod(String method) { + this.method = method; + } + public int getRedirectCount() { return redirectCount; } - public void incrementRedirectCount() throws Exception { - if (redirectCount < maxFollow) { - redirectCount++; - } else { - throw new Exception(); + public String getRedirectMode() { + return redirectMode; + } + + public void incrementRedirectCount() { + redirectCount++; + } + + public int getFollow() { + return follow; + } + + public FetchHeaders getHeaders() { + return headers; + } + + public String getReferrer() { + return referrer; + } + + public boolean isReferrerUrl() { + return isReferrerUrl; + } + + public String getReferrerPolicy() { + return referrerPolicy; + } + + public void setReferrerPolicy(String referrerPolicy) { + if (!FetchHttpConnection.VALID_POLICY.contains(referrerPolicy)) { + throw Errors.createTypeError("Invalid referrerPolicy: " + referrerPolicy); } + this.referrerPolicy = referrerPolicy; } - public FetchRequest(TruffleString input, JSObject init) throws MalformedURLException { - this.url = new URL(input.toString()); + public FetchRequest() { } + + /** + * Par. 5.4, Request constructor step 5. + * + * @param input A string + * @param init A RequestInit object, https://fetch.spec.whatwg.org/#requestinit + */ + public FetchRequest(TruffleString input, JSObject init) { + try { + url = new URL(input.toString()); + } catch (MalformedURLException e) { + throw Errors.createTypeError("Invalid URL: " + e.getMessage()); + } + // Par. 5.4, Request constructor step 5.3 if (url.getUserInfo() != null) { - throw Errors.createFetchError(url + " includes embedded credentials\""); + throw Errors.createTypeError(url + " is an url with embedded credentials"); } - TruffleString k = TruffleString.fromJavaStringUncached("method", TruffleString.Encoding.UTF_8); - if (JSObject.hasProperty(init, k)) { - method = (TruffleString) JSObject.get(init, k); - } else { - method = TruffleString.fromJavaStringUncached("GET", TruffleString.Encoding.UTF_8); + setDefault(); + applyRequestInit(init); + } + + public void applyRequestInit(JSObject init) { + // Par. 5.4, Request constructor step 14 + if (JSObject.hasProperty(init, REFERRER)) { + if (JSObject.get(init, REFERRER).equals("")) { + // Par. 5.4, Request constructor step 14.2 + referrer = "no-referrer"; + } else { + String initReferrer = JSToStringNode.create().executeString(JSObject.get(init, REFERRER)).toString(); + try { + // Par. 5.4, Request constructor step 14.3.1 + URL parsedReferrer = new URL(initReferrer); + // Par. 5.4, Request constructor step 14.3.3 + if (parsedReferrer.toString().matches("^about:(\\/\\/)?client$")) { + referrer = "client"; + } else { + referrer = initReferrer; + isReferrerUrl = true; + } + } catch (MalformedURLException e) { + throw Errors.createTypeError("Invalid URL" + initReferrer); + } + } } + + // Par. 5.4, Request constructor step 15 + if (JSObject.hasProperty(init, REFERRER_POLICY)) { + String initReferrerPolicy = JSToStringNode.create().executeString(JSObject.get(init, REFERRER_POLICY)).toString(); + setReferrerPolicy(initReferrerPolicy); + } + + // Par. 5.4, Request constructor step 22 + if (JSObject.hasProperty(init, REDIRECT)) { + redirectMode = JSToStringNode.create().executeString(JSObject.get(init, REDIRECT)).toString(); + } + + // Par. 5.4, Request constructor step 25 + if (JSObject.hasProperty(init, METHOD)) { + String initMethod = JSToStringNode.create().executeString(JSObject.get(init, METHOD)).toString(); + // Par. 5.4, Request constructor step 25.2 + if (Set.of("CONNECT", "TRACE", "TRACK").contains(initMethod.toUpperCase())) { + throw Errors.createTypeError("Forbidden method name"); + } + // Par. 5.4, Request constructor step 25.3, 25.4 + if (Set.of("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT").contains(initMethod.toUpperCase())) { + method = initMethod.toUpperCase(); + } else { + method = initMethod; + } + } + + // Par. 5.4, Request constructor step 32 + if (JSObject.hasProperty(init, HEADERS)) { + headers = new FetchHeaders(JSObject.get(init, HEADERS)); + } + + // Par. 5.4, Request constructor step 34 + if (JSObject.hasProperty(init, BODY) && (method.equals("GET") || method.equals("HEAD"))) { + throw Errors.createTypeError("Request with GET/HEAD method cannot have body"); + } + + // Par. 5.4, Request constructor step 36 + if (JSObject.hasProperty(init, BODY)) { + Object initBody = JSObject.get(init, BODY); + setBody(initBody); + // Par. 5.4, Request constructor step 36.4 + headers.set("Content-Type", getContentType()); + } + + if (JSObject.hasProperty(init, FOLLOW)) { + follow = JSToInt32Node.create().executeInt(JSObject.get(init, BODY)); + } + } + + private void setDefault() { + method = "GET"; + headers = new FetchHeaders(); + body = null; + referrer = "client"; + referrerPolicy = ""; + redirectMode = "follow"; + follow = 20; + } + + public FetchRequest copy() { + if (isBodyUsed()) { + throw Errors.createError("cannot clone body after it is used"); + } + + FetchRequest clone = new FetchRequest(); + clone.url = this.url; + clone.body = this.body; + clone.method = this.method; + clone.referrer = this.referrer; + clone.referrerPolicy = this.referrerPolicy; + clone.redirectMode = this.redirectMode; + clone.follow = this.follow; + clone.redirectCount = this.redirectCount; + clone.headers = new FetchHeaders(); + this.headers.keys().forEach(k -> clone.headers.append(k, this.headers.get(k))); + return clone; } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java index f1bfaab5458..027ed68d7b2 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchResponse.java @@ -1,13 +1,123 @@ +/* + * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.oracle.truffle.js.builtins.helper; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.nodes.cast.JSToInt32Node; +import com.oracle.truffle.js.nodes.cast.JSToStringNode; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.Strings; +import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; +import com.oracle.truffle.js.runtime.objects.JSObject; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; -public class FetchResponse { +/** + * The internal data structure for {@linkplain JSFetchResponse}. + */ +public class FetchResponse extends FetchBody { + // ResponseInit fields https://fetch.spec.whatwg.org/#responseinit + private static final TruffleString STATUS = Strings.constant("status"); + private static final TruffleString STATUS_TEXT = Strings.constant("statusText"); + private static final TruffleString HEADERS = Strings.constant("headers"); + //non-standard fields + private static final TruffleString URL = Strings.constant("url"); + private URL url; private int status; private String statusText; private int counter; - private FetchHeaders headers; + private FetchResponseType type; + public FetchHeaders headers; + + public enum FetchResponseType { + basic, cors, default_, error, opaque, opaqueredirect + } + + public FetchResponse() { + setDefault(); + } + + public FetchResponse(Object body, JSObject init) { + setDefault(); + setBody(body); + + headers = new FetchHeaders(init); + + if (init.hasProperty(HEADERS)) { + headers = new FetchHeaders(JSObject.get(init, HEADERS)); + } + + if (body != null && !headers.has("Content-Type")) { + headers.append("Content-Type", getContentType()); + } + + if (init.hasProperty(STATUS)) { + status = JSToInt32Node.create().executeInt(JSObject.get(init, STATUS)); + } + + if (init.hasProperty(STATUS_TEXT)) { + statusText = JSToStringNode.create().executeString(JSObject.get(init, STATUS_TEXT)).toString(); + } + + if (init.hasProperty(URL)) { + String initUrl = JSToStringNode.create().executeString(JSObject.get(init, URL)).toString(); + try { + url = new URL(initUrl); + } catch (MalformedURLException e) { + throw Errors.createTypeError("Invalid url: " + initUrl); + } + } + } + + private void setDefault() { + status = 200; + statusText = ""; + headers = new FetchHeaders(); + type = FetchResponseType.default_; + counter = 0; + body = null; + } public URL getUrl() { return url; @@ -17,10 +127,26 @@ public void setUrl(URL url) { this.url = url; } + public FetchResponseType getType() { + return type; + } + + public void setType(FetchResponseType type) { + this.type = type; + } + + public void setUrl(TruffleString str) throws MalformedURLException { + this.url = new URL(str.toString()); + } + public int getStatus() { return status; } + public boolean getOk() { + return status >= HttpURLConnection.HTTP_OK && status < 300; + } + public void setStatus(int status) { this.status = status; } @@ -30,17 +156,17 @@ public String getStatusText() { } public void setStatusText(String statusText) { - this.statusText = statusText; - } - - public int getCounter() { - return counter; + this.statusText = statusText == null ? "" : statusText; } public void setCounter(int counter) { this.counter = counter; } + public boolean getRedirected() { + return counter > 0; + } + public FetchHeaders getHeaders() { return headers; } @@ -48,4 +174,21 @@ public FetchHeaders getHeaders() { public void setHeaders(FetchHeaders headers) { this.headers = headers; } + + public FetchResponse copy() { + if (isBodyUsed()) { + throw Errors.createError("cannot clone body after it is used"); + } + + FetchResponse clone = new FetchResponse(); + clone.url = this.url; + clone.status = this.status; + clone.statusText = this.statusText; + clone.type = this.type; + clone.counter = this.counter; + clone.body = this.body; + clone.headers = new FetchHeaders(); + this.headers.keys().forEach(k -> clone.headers.append(k, this.headers.get(k))); + return clone; + } } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java index 78596962231..f3111eae3af 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/Errors.java @@ -58,6 +58,8 @@ import com.oracle.truffle.js.runtime.objects.JSObjectUtil; import com.oracle.truffle.js.runtime.objects.Null; +import java.io.IOException; + /** * Utility class to to create all kinds of ECMAScript-defined Error Objects. */ @@ -120,9 +122,17 @@ public static JSException createURIError(String message) { return JSException.create(JSErrorType.URIError, message); } + @TruffleBoundary - public static JSException createFetchError(String message) { - return JSException.create(JSErrorType.FetchError, message); + public static JSException createFetchError(String message, String type, Node originatingNode) { + JSContext context = JavaScriptLanguage.get(originatingNode).getJSContext(); + JSRealm realm = JSRealm.get(originatingNode); + JSErrorObject errorObj = JSError.createErrorObject(context, realm, JSErrorType.FetchError); + JSError.setMessage(errorObj, TruffleString.fromJavaStringUncached(message, TruffleString.Encoding.UTF_8)); + JSObjectUtil.putDataProperty(context, errorObj, JSError.ERRORS_TYPE, TruffleString.fromJavaStringUncached(type, TruffleString.Encoding.UTF_8), JSError.ERRORS_ATTRIBUTES); + JSException exception = JSException.create(JSErrorType.FetchError, message, errorObj, realm); + JSError.setException(realm, errorObj, exception, false); + return exception; } @TruffleBoundary diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java index 09a30ae5fce..61db5f68f0f 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSContext.java @@ -93,6 +93,8 @@ import com.oracle.truffle.js.runtime.builtins.JSDate; import com.oracle.truffle.js.runtime.builtins.JSDictionary; import com.oracle.truffle.js.runtime.builtins.JSError; +import com.oracle.truffle.js.runtime.builtins.JSFetchHeaders; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequest; import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.builtins.JSFinalizationRegistry; import com.oracle.truffle.js.runtime.builtins.JSFinalizationRegistryObject; @@ -318,6 +320,24 @@ public enum BuiltinFunctionKey { RegExp$7, RegExp$8, RegExp$9, + FetchResponseGetType, + FetchResponseGetUrl, + FetchResponseGetRedirected, + FetchResponseGetStatus, + FetchResponseGetStatusText, + FetchResponseGetOk, + FetchResponseGetHeaders, + FetchResponseGetBody, + FetchResponseGetBodyUsed, + FetchRequestGetMethod, + FetchRequestGetUrl, + FetchRequestGetReferrer, + FetchRequestGetReferrerPolicy, + FetchRequestGetRedirect, + FetchRequestGetHeaders, + FetchRequestGetSignal, + FetchRequestGetBody, + FetchRequestGetBodyUsed, SymbolGetDescription, MapGetSize, SetGetSize, @@ -447,6 +467,8 @@ public enum BuiltinFunctionKey { private final JSObjectFactory regExpFactory; private final JSObjectFactory dateFactory; private final JSObjectFactory fetchResponseFactory; + private final JSObjectFactory fetchRequestFactory; + private final JSObjectFactory fetchHeadersFactory; private final JSObjectFactory nonStrictArgumentsFactory; private final JSObjectFactory strictArgumentsFactory; private final JSObjectFactory callSiteFactory; @@ -622,6 +644,8 @@ protected JSContext(Evaluator evaluator, JSContextOptions contextOptions, JavaSc this.regExpFactory = builder.create(JSRegExp.INSTANCE); this.dateFactory = builder.create(JSDate.INSTANCE); this.fetchResponseFactory = builder.create(JSFetchResponse.INSTANCE); + this.fetchRequestFactory = builder.create(JSFetchRequest.INSTANCE); + this.fetchHeadersFactory = builder.create(JSFetchHeaders.INSTANCE); this.symbolFactory = builder.create(JSSymbol.INSTANCE); this.mapFactory = builder.create(JSMap.INSTANCE); @@ -982,6 +1006,14 @@ public final JSObjectFactory getFetchResponseFactory() { return fetchResponseFactory; } + public final JSObjectFactory getFetchRequestFactory() { + return fetchRequestFactory; + } + + public final JSObjectFactory getFetchHeadersFactory() { + return fetchHeadersFactory; + } + public final JSObjectFactory getEnumerateIteratorFactory() { return enumerateIteratorFactory; } diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java index d4133b1a0f1..47f6fc15b18 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSErrorType.java @@ -83,7 +83,6 @@ public enum JSErrorType implements PrototypeSupplier { // Fetch FetchError, - AbortError, // WebAssembly CompileError, diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java index 12b2203e828..200c57c0633 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/JSRealm.java @@ -57,6 +57,8 @@ import java.util.SplittableRandom; import java.util.WeakHashMap; +import com.oracle.truffle.js.runtime.builtins.JSFetchHeaders; +import com.oracle.truffle.js.runtime.builtins.JSFetchRequest; import com.oracle.truffle.js.runtime.builtins.JSFetchResponse; import com.oracle.truffle.js.runtime.objects.Null; import org.graalvm.collections.Pair; @@ -273,6 +275,10 @@ public class JSRealm { private final JSDynamicObject datePrototype; private final JSFunctionObject fetchResponseConstructor; private final JSDynamicObject fetchResponsePrototype; + private final JSFunctionObject fetchRequestConstructor; + private final JSDynamicObject fetchRequestPrototype; + private final JSFunctionObject fetchHeadersConstructor; + private final JSDynamicObject fetchHeadersPrototype; @CompilationFinal(dimensions = 1) private final JSDynamicObject[] errorConstructors; @CompilationFinal(dimensions = 1) private final JSDynamicObject[] errorPrototypes; private final JSFunctionObject callSiteConstructor; @@ -615,6 +621,12 @@ protected JSRealm(JSContext context, TruffleLanguage.Env env, JSRealm parentReal ctor = JSFetchResponse.createConstructor(this); this.fetchResponseConstructor = ctor.getFunctionObject(); this.fetchResponsePrototype = ctor.getPrototype(); + ctor = JSFetchRequest.createConstructor(this); + this.fetchRequestConstructor = ctor.getFunctionObject(); + this.fetchRequestPrototype = ctor.getPrototype(); + ctor = JSFetchHeaders.createConstructor(this); + this.fetchHeadersConstructor = ctor.getFunctionObject(); + this.fetchHeadersPrototype = ctor.getPrototype(); ctor = JSArray.createConstructor(this); this.arrayConstructor = ctor.getFunctionObject(); this.arrayPrototype = (JSArrayObject) ctor.getPrototype(); @@ -1254,6 +1266,30 @@ public final JSDynamicObject getDatePrototype() { return datePrototype; } + public final JSFunctionObject getFetchResponseConstructor() { + return fetchResponseConstructor; + } + + public final JSDynamicObject getFetchResponsePrototype() { + return fetchResponsePrototype; + } + + public final JSFunctionObject getFetchRequestConstructor() { + return fetchRequestConstructor; + } + + public final JSDynamicObject getFetchRequestPrototype() { + return fetchRequestPrototype; + } + + public final JSFunctionObject getFetchHeadersConstructor() { + return fetchHeadersConstructor; + } + + public final JSDynamicObject getFetchHeadersPrototype() { + return fetchHeadersPrototype; + } + public final JSFunctionObject getSegmenterConstructor() { return segmenterConstructor; } @@ -1699,6 +1735,9 @@ public void setupGlobals() { putGlobalProperty(JSArray.CLASS_NAME, getArrayConstructor()); putGlobalProperty(JSString.CLASS_NAME, getStringConstructor()); putGlobalProperty(JSDate.CLASS_NAME, getDateConstructor()); + putGlobalProperty(JSFetchResponse.CLASS_NAME, getFetchResponseConstructor()); + putGlobalProperty(JSFetchRequest.CLASS_NAME, getFetchRequestConstructor()); + putGlobalProperty(JSFetchHeaders.CLASS_NAME, getFetchHeadersConstructor()); putGlobalProperty(JSNumber.CLASS_NAME, getNumberConstructor()); putGlobalProperty(JSBoolean.CLASS_NAME, getBooleanConstructor()); putGlobalProperty(JSRegExp.CLASS_NAME, getRegExpConstructor()); diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSError.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSError.java index 91b2c5aae6a..894b39e21fa 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSError.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSError.java @@ -84,6 +84,8 @@ public final class JSError extends JSNonProxy { public static final TruffleString STACK_NAME = Strings.constant("stack"); public static final HiddenKey FORMATTED_STACK_NAME = new HiddenKey("FormattedStack"); public static final TruffleString ERRORS_NAME = Strings.constant("errors"); + public static final TruffleString ERRORS_TYPE = Strings.constant("type"); + public static final TruffleString ERRORS_SYSERR = Strings.constant("erroredSysCall"); public static final int ERRORS_ATTRIBUTES = JSAttributes.getDefaultNotEnumerable(); public static final TruffleString PREPARE_STACK_TRACE_NAME = Strings.constant("prepareStackTrace"); public static final TruffleString LINE_NUMBER_PROPERTY_NAME = Strings.constant("lineNumber"); diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeaders.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeaders.java new file mode 100644 index 00000000000..a1d5aee245c --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeaders.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.runtime.builtins; + +import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.FetchHeadersPrototypeBuiltins; +import com.oracle.truffle.js.builtins.helper.FetchHeaders; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.JSRealm; +import com.oracle.truffle.js.runtime.Strings; +import com.oracle.truffle.js.runtime.objects.JSDynamicObject; +import com.oracle.truffle.js.runtime.objects.JSObject; +import com.oracle.truffle.js.runtime.objects.JSObjectUtil; + +/** + * https://fetch.spec.whatwg.org/#headers-class. + */ +public final class JSFetchHeaders extends JSNonProxy implements JSConstructorFactory.Default.Default, PrototypeSupplier { + public static final JSFetchHeaders INSTANCE = new JSFetchHeaders(); + public static final TruffleString CLASS_NAME = Strings.constant("Headers"); + public static final TruffleString PROTOTYPE_NAME = Strings.constant("Headers.prototype"); + + private JSFetchHeaders() { } + + public static boolean isJSFetchHeaders(Object obj) { + return obj instanceof JSFetchHeadersObject; + } + + @Override + public TruffleString getClassName() { + return CLASS_NAME; + } + + @Override + public TruffleString getClassName(JSDynamicObject object) { + return getClassName(); + } + + @Override + public TruffleString getBuiltinToStringTag(JSDynamicObject object) { + return getClassName(object); + } + + public static FetchHeaders getInternalData(Object obj) { + assert isJSFetchHeaders(obj); + return ((JSFetchHeadersObject) obj).getHeadersMap(); + } + + @Override + public JSDynamicObject createPrototype(JSRealm realm, JSFunctionObject ctor) { + JSContext ctx = realm.getContext(); + + JSObject prototype = JSObjectUtil.createOrdinaryPrototypeObject(realm); + JSObjectUtil.putConstructorProperty(ctx, prototype, ctor); + JSObjectUtil.putFunctionsFromContainer(realm, prototype, FetchHeadersPrototypeBuiltins.BUILTINS); + + return prototype; + } + + @Override + public Shape makeInitialShape(JSContext ctx, JSDynamicObject prototype) { + return JSObjectUtil.getProtoChildShape(prototype, INSTANCE, ctx); + } + + @Override + public JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getFetchHeadersPrototype(); + } + + public static JSConstructor createConstructor(JSRealm realm) { + return INSTANCE.createConstructorAndPrototype(realm); + } + + public static JSFetchHeadersObject create(JSContext context, JSRealm realm, FetchHeaders headers) { + JSObjectFactory factory = context.getFetchHeadersFactory(); + JSFetchHeadersObject obj = JSFetchHeadersObject.create(factory.getShape(realm), headers); + factory.initProto(obj, realm); + return context.trackAllocation(obj); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeadersObject.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeadersObject.java new file mode 100644 index 00000000000..bc5d13eb058 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchHeadersObject.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.runtime.builtins; + +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.helper.FetchHeaders; +import com.oracle.truffle.js.runtime.objects.JSNonProxyObject; + +@ExportLibrary(InteropLibrary.class) +public final class JSFetchHeadersObject extends JSNonProxyObject { + private FetchHeaders value; + + protected JSFetchHeadersObject(Shape shape, FetchHeaders value) { + super(shape); + this.value = value; + } + + public FetchHeaders getHeadersMap() { + return value; + } + + public void setHeadersMap(FetchHeaders value) { + this.value = value; + } + + public static JSFetchHeadersObject create(Shape shape, FetchHeaders value) { + return new JSFetchHeadersObject(shape, value); + } + + @Override + public TruffleString getClassName() { + return JSFetchHeaders.CLASS_NAME; + } + + @Override + public TruffleString getBuiltinToStringTag() { + return getClassName(); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequest.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequest.java new file mode 100644 index 00000000000..2b988689299 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequest.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.runtime.builtins; + +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.profiles.BranchProfile; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.FetchRequestPrototypeBuiltins; +import com.oracle.truffle.js.builtins.helper.FetchHeaders; +import com.oracle.truffle.js.builtins.helper.FetchRequest; +import com.oracle.truffle.js.runtime.Errors; +import com.oracle.truffle.js.runtime.JSContext; +import com.oracle.truffle.js.runtime.JSRealm; +import com.oracle.truffle.js.runtime.JavaScriptRootNode; +import com.oracle.truffle.js.runtime.Strings; +import com.oracle.truffle.js.runtime.objects.JSDynamicObject; +import com.oracle.truffle.js.runtime.objects.JSObject; +import com.oracle.truffle.js.runtime.objects.JSObjectUtil; +import com.oracle.truffle.js.runtime.objects.Null; +import com.oracle.truffle.js.runtime.objects.Undefined; + +/** + * https://fetch.spec.whatwg.org/#request-class. + */ +public final class JSFetchRequest extends JSNonProxy implements JSConstructorFactory.Default.Default, PrototypeSupplier { + public static final JSFetchRequest INSTANCE = new JSFetchRequest(); + public static final TruffleString CLASS_NAME = Strings.constant("Request"); + public static final TruffleString PROTOTYPE_NAME = Strings.constant("Request.prototype"); + + // getter names + private static final TruffleString METHOD = Strings.constant("method"); + private static final TruffleString URL = Strings.constant("url"); + private static final TruffleString REFERRER = Strings.constant("referrer"); + private static final TruffleString REFERRER_POLICY = Strings.constant("referrerPolicy"); + private static final TruffleString REDIRECT = Strings.constant("redirect"); + private static final TruffleString HEADERS = Strings.constant("headers"); + // body getter names + private static final TruffleString BODY = Strings.constant("body"); + private static final TruffleString BODY_USED = Strings.constant("bodyUsed"); + + private JSFetchRequest() { } + + public static boolean isJSFetchRequest(Object obj) { + return obj instanceof JSFetchRequestObject; + } + + @Override + public TruffleString getClassName() { + return CLASS_NAME; + } + + @Override + public TruffleString getClassName(JSDynamicObject object) { + return getClassName(); + } + + @Override + public TruffleString getBuiltinToStringTag(JSDynamicObject object) { + return getClassName(object); + } + + public static FetchRequest getInternalData(JSDynamicObject obj) { + assert isJSFetchRequest(obj); + return ((JSFetchRequestObject) obj).getRequestMap(); + } + + private static TruffleString getMethod(JSDynamicObject obj) { + String method = getInternalData(obj).getMethod(); + return TruffleString.fromJavaStringUncached(method, TruffleString.Encoding.UTF_8); + } + + private static TruffleString getUrl(JSDynamicObject obj) { + String url = getInternalData(obj).getUrl().toString(); + return TruffleString.fromJavaStringUncached(url, TruffleString.Encoding.UTF_8); + } + + // https://fetch.spec.whatwg.org/#dom-request-referrer + private static Object getReferrer(JSDynamicObject obj) { + String referrer = getInternalData(obj).getReferrer(); + + if (referrer == null) { + return Undefined.instance; + } + if (referrer.equals("no-referrer")) { + return TruffleString.fromJavaStringUncached("", TruffleString.Encoding.UTF_8); + } + if (referrer.equals("client")) { + return TruffleString.fromJavaStringUncached("about:client", TruffleString.Encoding.UTF_8); + } + + return TruffleString.fromJavaStringUncached(referrer, TruffleString.Encoding.UTF_8); + } + + private static TruffleString getReferrerPolicy(JSDynamicObject obj) { + String referrerPolicy = getInternalData(obj).getReferrerPolicy(); + return TruffleString.fromJavaStringUncached(referrerPolicy, TruffleString.Encoding.UTF_8); + } + + private static TruffleString getRedirectMode(JSDynamicObject obj) { + String redirect = getInternalData(obj).getRedirectMode(); + return TruffleString.fromJavaStringUncached(redirect, TruffleString.Encoding.UTF_8); + } + + private static FetchHeaders getHeaders(JSDynamicObject obj) { + return getInternalData(obj).getHeaders(); + } + + private static Object getBody(JSDynamicObject obj) { + String body = getInternalData(obj).getBody(); + if (body == null) { + return Null.instance; + } + return TruffleString.fromJavaStringUncached(body, TruffleString.Encoding.UTF_8); + } + + private static boolean getBodyUsed(JSDynamicObject obj) { + return getInternalData(obj).isBodyUsed(); + } + + private static JSDynamicObject createGetterFunction(JSRealm realm, TruffleString name, JSContext.BuiltinFunctionKey key) { + JSFunctionData getterData = realm.getContext().getOrCreateBuiltinFunctionData(key, (c) -> { + CallTarget callTarget = new JavaScriptRootNode(c.getLanguage(), null, null) { + private final BranchProfile errorBranch = BranchProfile.create(); + + @Override + public Object execute(VirtualFrame frame) { + Object obj = frame.getArguments()[0]; + if (JSFetchRequest.isJSFetchRequest(obj)) { + switch (key) { + case FetchRequestGetUrl: + return JSFetchRequest.getUrl((JSFetchRequestObject) obj); + case FetchRequestGetMethod: + return JSFetchRequest.getMethod((JSFetchRequestObject) obj); + case FetchRequestGetReferrer: + return JSFetchRequest.getReferrer((JSFetchRequestObject) obj); + case FetchRequestGetReferrerPolicy: + return JSFetchRequest.getReferrerPolicy((JSFetchRequestObject) obj); + case FetchRequestGetRedirect: + return JSFetchRequest.getRedirectMode((JSFetchRequestObject) obj); + case FetchRequestGetHeaders: + FetchHeaders headers = getHeaders((JSFetchRequestObject) obj); + return JSFetchHeaders.create(realm.getContext(), realm, headers); + case FetchRequestGetBody: + return JSFetchRequest.getBody((JSFetchRequestObject) obj); + case FetchRequestGetBodyUsed: + return JSFetchRequest.getBodyUsed((JSFetchRequestObject) obj); + default: + throw new IllegalArgumentException("FetchRequest getter function key expected"); + } + } else { + errorBranch.enter(); + throw Errors.createTypeError("Request expected"); + } + } + }.getCallTarget(); + return JSFunctionData.createCallOnly(c, callTarget, 0, Strings.concat(Strings.GET_SPC, name)); + }); + return JSFunction.create(realm, getterData); + } + + @Override + public JSDynamicObject createPrototype(JSRealm realm, JSFunctionObject ctor) { + JSContext ctx = realm.getContext(); + + JSObject prototype = JSObjectUtil.createOrdinaryPrototypeObject(realm); + JSObjectUtil.putConstructorProperty(ctx, prototype, ctor); + + JSObjectUtil.putBuiltinAccessorProperty(prototype, URL, createGetterFunction(realm, URL, JSContext.BuiltinFunctionKey.FetchRequestGetUrl), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, METHOD, createGetterFunction(realm, METHOD, JSContext.BuiltinFunctionKey.FetchRequestGetMethod), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, REDIRECT, createGetterFunction(realm, REDIRECT, JSContext.BuiltinFunctionKey.FetchRequestGetRedirect), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, REFERRER, createGetterFunction(realm, REFERRER, JSContext.BuiltinFunctionKey.FetchRequestGetReferrer), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, REFERRER_POLICY, createGetterFunction(realm, REFERRER_POLICY, JSContext.BuiltinFunctionKey.FetchRequestGetReferrerPolicy), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, HEADERS, createGetterFunction(realm, HEADERS, JSContext.BuiltinFunctionKey.FetchRequestGetHeaders), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, BODY, createGetterFunction(realm, BODY, JSContext.BuiltinFunctionKey.FetchRequestGetBody), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, BODY_USED, createGetterFunction(realm, BODY_USED, JSContext.BuiltinFunctionKey.FetchRequestGetBodyUsed), Undefined.instance); + + JSObjectUtil.putFunctionsFromContainer(realm, prototype, FetchRequestPrototypeBuiltins.BUILTINS); + + return prototype; + } + + @Override + public Shape makeInitialShape(JSContext ctx, JSDynamicObject prototype) { + return JSObjectUtil.getProtoChildShape(prototype, INSTANCE, ctx); + } + + @Override + public JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getFetchRequestPrototype(); + } + + public static JSConstructor createConstructor(JSRealm realm) { + return INSTANCE.createConstructorAndPrototype(realm); + } + + public static JSFetchRequestObject create(JSContext context, JSRealm realm, FetchRequest request) { + JSObjectFactory factory = context.getFetchRequestFactory(); + JSFetchRequestObject obj = JSFetchRequestObject.create(factory.getShape(realm), request); + factory.initProto(obj, realm); + return context.trackAllocation(obj); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequestObject.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequestObject.java new file mode 100644 index 00000000000..f42170581b6 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchRequestObject.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.js.runtime.builtins; + +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.strings.TruffleString; +import com.oracle.truffle.js.builtins.helper.FetchRequest; +import com.oracle.truffle.js.runtime.objects.JSNonProxyObject; + +@ExportLibrary(InteropLibrary.class) +public final class JSFetchRequestObject extends JSNonProxyObject { + private FetchRequest value; + + protected JSFetchRequestObject(Shape shape, FetchRequest value) { + super(shape); + this.value = value; + } + + public FetchRequest getRequestMap() { + return value; + } + + public void setResponseMap(FetchRequest value) { + this.value = value; + } + + public static JSFetchRequestObject create(Shape shape, FetchRequest value) { + return new JSFetchRequestObject(shape, value); + } + + @Override + public TruffleString getClassName() { + return JSFetchResponse.CLASS_NAME; + } + + @Override + public TruffleString getBuiltinToStringTag() { + return getClassName(); + } +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java index 50064d55f86..e5b06ef9bca 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSFetchResponse.java @@ -1,25 +1,89 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package com.oracle.truffle.js.runtime.builtins; +import com.oracle.truffle.api.CallTarget; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.object.Shape; +import com.oracle.truffle.api.profiles.BranchProfile; import com.oracle.truffle.api.strings.TruffleString; import com.oracle.truffle.js.builtins.FetchResponseFunctionBuiltins; import com.oracle.truffle.js.builtins.FetchResponsePrototypeBuiltins; +import com.oracle.truffle.js.builtins.helper.FetchHeaders; import com.oracle.truffle.js.builtins.helper.FetchResponse; +import com.oracle.truffle.js.runtime.Errors; import com.oracle.truffle.js.runtime.JSContext; import com.oracle.truffle.js.runtime.JSRealm; +import com.oracle.truffle.js.runtime.JavaScriptRootNode; import com.oracle.truffle.js.runtime.Strings; import com.oracle.truffle.js.runtime.objects.JSDynamicObject; import com.oracle.truffle.js.runtime.objects.JSObject; import com.oracle.truffle.js.runtime.objects.JSObjectUtil; -import com.oracle.truffle.js.runtime.objects.JSShape; +import com.oracle.truffle.js.runtime.objects.Null; +import com.oracle.truffle.js.runtime.objects.Undefined; +/** + * https://fetch.spec.whatwg.org/#response-class. + */ public final class JSFetchResponse extends JSNonProxy implements JSConstructorFactory.Default.WithFunctions, PrototypeSupplier { + public static final JSFetchResponse INSTANCE = new JSFetchResponse(); public static final TruffleString CLASS_NAME = Strings.constant("Response"); public static final TruffleString PROTOTYPE_NAME = Strings.constant("Response.prototype"); - public static final JSFetchResponse INSTANCE = new JSFetchResponse(); + // getter names + private static final TruffleString TYPE = Strings.constant("type"); + private static final TruffleString URL = Strings.constant("url"); + private static final TruffleString REDIRECTED = Strings.constant("redirected"); + private static final TruffleString STATUS = Strings.constant("status"); + private static final TruffleString STATUS_TEXT = Strings.constant("statusText"); + private static final TruffleString OK = Strings.constant("ok"); + private static final TruffleString HEADERS = Strings.constant("headers"); + // body getter names + private static final TruffleString BODY = Strings.constant("body"); + private static final TruffleString BODY_USED = Strings.constant("bodyUsed"); + + private JSFetchResponse() { } - private JSFetchResponse() { + public static boolean isJSFetchResponse(Object obj) { + return obj instanceof JSFetchResponseObject; } @Override @@ -37,27 +101,132 @@ public TruffleString getBuiltinToStringTag(JSDynamicObject object) { return getClassName(object); } - public static JSConstructor createConstructor(JSRealm realm) { - return INSTANCE.createConstructorAndPrototype(realm, FetchResponseFunctionBuiltins.BUILTINS); + private static FetchResponse getInternalData(JSDynamicObject obj) { + assert isJSFetchResponse(obj); + return ((JSFetchResponseObject) obj).getResponseMap(); + } + + private static TruffleString getType(JSDynamicObject obj) { + String type = getInternalData(obj).getType().toString(); + type = type.replace("_", ""); + return TruffleString.fromJavaStringUncached(type, TruffleString.Encoding.UTF_8); + } + + private static TruffleString getUrl(JSDynamicObject obj) { + String url = getInternalData(obj).getUrl() == null ? "" : getInternalData(obj).getUrl().toString(); + return TruffleString.fromJavaStringUncached(url, TruffleString.Encoding.UTF_8); + } + + private static boolean getRedirected(JSDynamicObject obj) { + return getInternalData(obj).getRedirected(); + } + + private static int getStatus(JSDynamicObject obj) { // su + return getInternalData(obj).getStatus(); + } + + private static TruffleString getStatusText(JSDynamicObject obj) { + String statusText = getInternalData(obj).getStatusText(); + return TruffleString.fromJavaStringUncached(statusText, TruffleString.Encoding.UTF_8); + } + + private static boolean getOk(JSDynamicObject obj) { + return getInternalData(obj).getOk(); + } + + private static FetchHeaders getHeaders(JSDynamicObject obj) { + return getInternalData(obj).getHeaders(); + } + + private static Object getBody(JSDynamicObject obj) { + String body = getInternalData(obj).getBody(); + if (body == null) { + return Null.instance; + } + return TruffleString.fromJavaStringUncached(body, TruffleString.Encoding.UTF_8); + } + + private static boolean getBodyUsed(JSDynamicObject obj) { + return getInternalData(obj).isBodyUsed(); + } + + private static JSDynamicObject createGetterFunction(JSRealm realm, TruffleString name, JSContext.BuiltinFunctionKey key) { + JSFunctionData getterData = realm.getContext().getOrCreateBuiltinFunctionData(key, (c) -> { + CallTarget callTarget = new JavaScriptRootNode(c.getLanguage(), null, null) { + private final BranchProfile errorBranch = BranchProfile.create(); + + @Override + public Object execute(VirtualFrame frame) { + Object obj = frame.getArguments()[0]; + if (JSFetchResponse.isJSFetchResponse(obj)) { + switch (key) { + case FetchResponseGetUrl: + return JSFetchResponse.getUrl((JSFetchResponseObject) obj); + case FetchResponseGetRedirected: + return JSFetchResponse.getRedirected((JSFetchResponseObject) obj); + case FetchResponseGetType: + return JSFetchResponse.getType((JSFetchResponseObject) obj); + case FetchResponseGetStatus: + return JSFetchResponse.getStatus((JSFetchResponseObject) obj); + case FetchResponseGetStatusText: + return JSFetchResponse.getStatusText((JSFetchResponseObject) obj); + case FetchResponseGetOk: + return JSFetchResponse.getOk((JSFetchResponseObject) obj); + case FetchResponseGetHeaders: + FetchHeaders headers = getHeaders((JSFetchResponseObject) obj); + return JSFetchHeaders.create(realm.getContext(), realm, headers); + case FetchResponseGetBody: + return JSFetchResponse.getBody((JSFetchResponseObject) obj); + case FetchResponseGetBodyUsed: + return JSFetchResponse.getBodyUsed((JSFetchResponseObject) obj); + default: + throw new IllegalArgumentException("FetchResponse getter function key expected"); + } + } else { + errorBranch.enter(); + throw Errors.createTypeError("Response expected"); + } + } + }.getCallTarget(); + return JSFunctionData.createCallOnly(c, callTarget, 0, Strings.concat(Strings.GET_SPC, name)); + }); + return JSFunction.create(realm, getterData); } @Override public JSDynamicObject createPrototype(JSRealm realm, JSFunctionObject ctor) { JSContext ctx = realm.getContext(); - JSObject responsePrototype; - if (ctx.getEcmaScriptVersion() < 6) { - Shape protoShape = JSShape.createPrototypeShape(realm.getContext(), INSTANCE, realm.getObjectPrototype()); - responsePrototype = JSFetchResponseObject.create(protoShape, new FetchResponse()); - JSObjectUtil.setOrVerifyPrototype(ctx, responsePrototype, realm.getObjectPrototype()); - } else { - responsePrototype = JSObjectUtil.createOrdinaryPrototypeObject(realm); - } + JSObject prototype = JSObjectUtil.createOrdinaryPrototypeObject(realm); + JSObjectUtil.putConstructorProperty(ctx, prototype, ctor); - JSObjectUtil.putConstructorProperty(ctx, responsePrototype, ctor); - JSObjectUtil.putFunctionsFromContainer(realm, responsePrototype, FetchResponsePrototypeBuiltins.BUILTINS); + JSObjectUtil.putBuiltinAccessorProperty(prototype, TYPE, createGetterFunction(realm, TYPE, JSContext.BuiltinFunctionKey.FetchResponseGetType), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, URL, createGetterFunction(realm, URL, JSContext.BuiltinFunctionKey.FetchResponseGetUrl), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, REDIRECTED, createGetterFunction(realm, REDIRECTED, JSContext.BuiltinFunctionKey.FetchResponseGetRedirected), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, STATUS, createGetterFunction(realm, STATUS, JSContext.BuiltinFunctionKey.FetchResponseGetStatus), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, STATUS_TEXT, createGetterFunction(realm, STATUS_TEXT, JSContext.BuiltinFunctionKey.FetchResponseGetStatusText), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, OK, createGetterFunction(realm, OK, JSContext.BuiltinFunctionKey.FetchResponseGetOk), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, HEADERS, createGetterFunction(realm, HEADERS, JSContext.BuiltinFunctionKey.FetchResponseGetHeaders), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, BODY, createGetterFunction(realm, BODY, JSContext.BuiltinFunctionKey.FetchResponseGetBody), Undefined.instance); + JSObjectUtil.putBuiltinAccessorProperty(prototype, BODY_USED, createGetterFunction(realm, BODY_USED, JSContext.BuiltinFunctionKey.FetchResponseGetBodyUsed), Undefined.instance); - return responsePrototype; + JSObjectUtil.putFunctionsFromContainer(realm, prototype, FetchResponsePrototypeBuiltins.BUILTINS); + + return prototype; + } + + @Override + public Shape makeInitialShape(JSContext ctx, JSDynamicObject prototype) { + return JSObjectUtil.getProtoChildShape(prototype, INSTANCE, ctx); + } + + @Override + public JSDynamicObject getIntrinsicDefaultProto(JSRealm realm) { + return realm.getFetchResponsePrototype(); + } + + public static JSConstructor createConstructor(JSRealm realm) { + return INSTANCE.createConstructorAndPrototype(realm, FetchResponseFunctionBuiltins.BUILTINS); } public static JSFetchResponseObject create(JSContext context, JSRealm realm, FetchResponse response) { From 0a376064ea725dfca92d723d82c94d5fe8af2296 Mon Sep 17 00:00:00 2001 From: Julian Kaindl Date: Tue, 10 Jan 2023 09:55:27 +0100 Subject: [PATCH 3/3] Add support for data urls --- .../test/builtins/fetch/FetchMethodTest.java | 20 +++++ .../builtins/helper/FetchHttpConnection.java | 84 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java index 9d1cd9dfc63..118153e48fb 100644 --- a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java +++ b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/fetch/FetchMethodTest.java @@ -41,6 +41,7 @@ package com.oracle.truffle.js.test.builtins.fetch; import com.oracle.truffle.js.builtins.helper.FetchHttpConnection; +import com.oracle.truffle.js.builtins.helper.FetchResponse; import com.oracle.truffle.js.runtime.JSContextOptions; import com.oracle.truffle.js.test.JSTest; import com.oracle.truffle.js.test.interop.AsyncInteropTest.TestOutput; @@ -53,7 +54,10 @@ import org.junit.Test; import java.io.IOException; +import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID; @@ -141,6 +145,22 @@ public void testRejectUnsupportedProtocol() { assertEquals("fetch cannot load ftp://example.com/. Scheme not supported: ftp\n", out); } + @Test + public void testDataUrlProcessor() throws MalformedURLException, URISyntaxException { + FetchResponse res = FetchHttpConnection.processDataUrl(new URI("data:,helloworld")); + assertEquals(res.getBody(), "helloworld"); + res = FetchHttpConnection.processDataUrl(new URI("data:text/plain,helloworld")); + assertEquals(res.getBody(), "helloworld"); + } + + @Test + public void testBase64DataUrlEncoding() throws MalformedURLException, URISyntaxException { + FetchResponse res = FetchHttpConnection.processDataUrl(new URI("data:text/plain;base64,aGVsbG93b3JsZA")); + assertEquals(res.getBody(), "helloworld"); + res = FetchHttpConnection.processDataUrl(new URI("data:;base64,aGVsbG93b3JsZA")); + assertEquals(res.getBody(), "helloworld"); + } + @Test(timeout = 5000) public void testRejectOnNetworkFailure() { String out = asyncThrows( diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java index 1a6c6aa1114..be7bd25f236 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/builtins/helper/FetchHttpConnection.java @@ -50,9 +50,15 @@ import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static java.net.HttpURLConnection.*; @@ -93,6 +99,14 @@ public static FetchResponse connect(FetchRequest request) throws IOException { ); } + if (request.getUrl().getProtocol().equals("data")) { + try { + processDataUrl(request.getUrl().toURI()); + } catch (URISyntaxException e) { + throw Errors.createFetchError("Failed to fetch data url", "", node); + } + } + // Setup Connection HttpURLConnection connection = (HttpURLConnection) request.getUrl().openConnection(); connection.setInstanceFollowRedirects(false); // don't follow automatically @@ -223,6 +237,76 @@ public static FetchResponse connect(FetchRequest request) throws IOException { return response; } + // https://fetch.spec.whatwg.org/#data-url-processor + public static FetchResponse processDataUrl(URI dataUri) throws URISyntaxException { + // 1. Assert: dataUrl scheme is "data" + if (!dataUri.getScheme().equals("data")) { + return null; + } + + // 2. Let input be the result of running the URL serializer on dataURL with exclude fragment set to true. + if (dataUri.getFragment() != null) { + dataUri = new URI(dataUri.getScheme(), dataUri.getSchemeSpecificPart(), null); + } + + String input = dataUri.toString(); + + // 3. Remove the leading "data:" from input. + input = input.substring(5); + + // 5. Let mimeType be the result of collecting a sequence of code points + // that are not equal to U+002C (,), given position. + String mimeType = input.codePoints() + .takeWhile(c -> c != ',') + .collect(StringBuilder::new, + StringBuilder::appendCodePoint, + StringBuilder::append + ) + .toString(); + + // 6. Strip leading and trailing ASCII whitespace from mimeType. + mimeType = mimeType.strip(); + + // 8. Advance position by 1. + // 9. Let encodedBody be the remainder of input. + String encodedBody = input.substring(mimeType.length() + 1); + String body = encodedBody; + + // 11. If mimeType ends with U+003B (;), + // followed by zero or more U+0020 SPACE, + // followed by an ASCII case-insensitive match for "base64", + // then: + Pattern p = Pattern.compile(";(\\u0020)*base64$", Pattern.CASE_INSENSITIVE); + if (p.matcher(mimeType).find()) { + // 11.2. Set body to the forgiving-base64 decode of stringBody. + byte[] decoded = Base64.getDecoder().decode(encodedBody.trim()); + body = new String(decoded, StandardCharsets.UTF_8); + // 11.4 Remove the last 6 code points from mimeType. + mimeType = mimeType.substring(0, mimeType.length() - 6); + // 11.5 Remove trailing U+0020 SPACE code points from mimeType, if any. + mimeType = mimeType.trim(); + // 11.6 Remove the last U+003B (;) from mimeType. + mimeType = mimeType.substring(0, mimeType.length() - 1); + } + + // 12. If mimeType starts with ";", then prepend "text/plain" to mimeType. + if (mimeType.startsWith(";")) { + mimeType = "text/plain" + mimeType; + } + + // 14. If mimeTypeRecord is failure, then set mimeTypeRecord to text/plain;charset=US-ASCII. + if (mimeType.isEmpty()) { + mimeType = "text/plain;charset=US-ASCII"; + } + + FetchResponse response = new FetchResponse(); + response.body = body; + response.headers = new FetchHeaders(Map.of("Content-Type", List.of(mimeType))); + response.setStatusText("OK"); + + return response; + } + public static boolean isRedirect(int status) { return REDIRECT_STATUS.contains(status); }