1/*
2 * Copyright (C) 2016 Canon Inc.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted, provided that the following conditions
6 * are required to be met:
7 *
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Canon Inc. nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR
21 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include "config.h"
30#include "FetchRequest.h"
31
32#include "Document.h"
33#include "HTTPParsers.h"
34#include "JSAbortSignal.h"
35#include "Logging.h"
36#include "Quirks.h"
37#include "ScriptExecutionContext.h"
38#include "SecurityOrigin.h"
39#include "Settings.h"
40
41namespace WebCore {
42
43static Optional<Exception> setMethod(ResourceRequest& request, const String& initMethod)
44{
45 if (!isValidHTTPToken(initMethod))
46 return Exception { TypeError, "Method is not a valid HTTP token."_s };
47 if (isForbiddenMethod(initMethod))
48 return Exception { TypeError, "Method is forbidden."_s };
49 request.setHTTPMethod(normalizeHTTPMethod(initMethod));
50 return WTF::nullopt;
51}
52
53static ExceptionOr<String> computeReferrer(ScriptExecutionContext& context, const String& referrer)
54{
55 if (referrer.isEmpty())
56 return "no-referrer"_str;
57
58 // FIXME: Tighten the URL parsing algorithm according https://url.spec.whatwg.org/#concept-url-parser.
59 URL referrerURL = context.completeURL(referrer);
60 if (!referrerURL.isValid())
61 return Exception { TypeError, "Referrer is not a valid URL."_s };
62
63 if (referrerURL.protocolIs("about") && referrerURL.path() == "client")
64 return "client"_str;
65
66 if (!(context.securityOrigin() && context.securityOrigin()->canRequest(referrerURL)))
67 return "client"_str;
68
69 return String { referrerURL.string() };
70}
71
72static Optional<Exception> buildOptions(FetchOptions& options, ResourceRequest& request, String& referrer, ScriptExecutionContext& context, const FetchRequest::Init& init)
73{
74 if (!init.window.isUndefinedOrNull() && !init.window.isEmpty())
75 return Exception { TypeError, "Window can only be null."_s };
76
77 if (init.hasMembers()) {
78 if (options.mode == FetchOptions::Mode::Navigate)
79 options.mode = FetchOptions::Mode::SameOrigin;
80 referrer = "client"_s;
81 options.referrerPolicy = { };
82 }
83
84 if (!init.referrer.isNull()) {
85 auto result = computeReferrer(context, init.referrer);
86 if (result.hasException())
87 return result.releaseException();
88 referrer = result.releaseReturnValue();
89 }
90
91 if (init.referrerPolicy)
92 options.referrerPolicy = init.referrerPolicy.value();
93
94 if (init.mode) {
95 options.mode = init.mode.value();
96 if (options.mode == FetchOptions::Mode::Navigate)
97 return Exception { TypeError, "Request constructor does not accept navigate fetch mode."_s };
98 }
99
100 if (init.credentials)
101 options.credentials = init.credentials.value();
102
103 if (init.cache)
104 options.cache = init.cache.value();
105 if (options.cache == FetchOptions::Cache::OnlyIfCached && options.mode != FetchOptions::Mode::SameOrigin)
106 return Exception { TypeError, "only-if-cached cache option requires fetch mode to be same-origin."_s };
107
108 if (init.redirect)
109 options.redirect = init.redirect.value();
110
111 if (!init.integrity.isNull())
112 options.integrity = init.integrity;
113
114 if (init.keepalive && init.keepalive.value())
115 options.keepAlive = true;
116
117 if (!init.method.isNull()) {
118 if (auto exception = setMethod(request, init.method))
119 return exception;
120 }
121
122 return WTF::nullopt;
123}
124
125static bool methodCanHaveBody(const ResourceRequest& request)
126{
127 return request.httpMethod() != "GET" && request.httpMethod() != "HEAD";
128}
129
130ExceptionOr<void> FetchRequest::initializeOptions(const Init& init)
131{
132 ASSERT(scriptExecutionContext());
133
134 auto exception = buildOptions(m_options, m_request, m_referrer, *scriptExecutionContext(), init);
135 if (exception)
136 return WTFMove(exception.value());
137
138 if (m_options.mode == FetchOptions::Mode::NoCors) {
139 const String& method = m_request.httpMethod();
140 if (method != "GET" && method != "POST" && method != "HEAD")
141 return Exception { TypeError, "Method must be GET, POST or HEAD in no-cors mode."_s };
142 m_headers->setGuard(FetchHeaders::Guard::RequestNoCors);
143 }
144
145 return { };
146}
147
148static inline Optional<Exception> processInvalidSignal(ScriptExecutionContext& context)
149{
150 ASCIILiteral message { "FetchRequestInit.signal should be undefined, null or an AbortSignal object."_s };
151 context.addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message);
152
153 if (is<Document>(context) && downcast<Document>(context).quirks().shouldIgnoreInvalidSignal())
154 return { };
155
156 RELEASE_LOG_ERROR(ResourceLoading, "FetchRequestInit.signal should be undefined, null or an AbortSignal object.");
157 return Exception { TypeError, message };
158}
159
160ExceptionOr<void> FetchRequest::initializeWith(const String& url, Init&& init)
161{
162 ASSERT(scriptExecutionContext());
163 // FIXME: Tighten the URL parsing algorithm according https://url.spec.whatwg.org/#concept-url-parser.
164 URL requestURL = scriptExecutionContext()->completeURL(url);
165 if (!requestURL.isValid() || !requestURL.user().isEmpty() || !requestURL.pass().isEmpty())
166 return Exception { TypeError, "URL is not valid or contains user credentials."_s };
167
168 m_options.mode = Mode::Cors;
169 m_options.credentials = Credentials::SameOrigin;
170 m_referrer = "client"_s;
171 m_request.setURL(requestURL);
172 m_request.setRequester(ResourceRequest::Requester::Fetch);
173 m_request.setInitiatorIdentifier(scriptExecutionContext()->resourceRequestIdentifier());
174
175 auto optionsResult = initializeOptions(init);
176 if (optionsResult.hasException())
177 return optionsResult.releaseException();
178
179 if (init.signal) {
180 if (auto* signal = JSAbortSignal::toWrapped(scriptExecutionContext()->vm(), init.signal))
181 m_signal->follow(*signal);
182 else if (!init.signal.isUndefinedOrNull()) {
183 if (auto exception = processInvalidSignal(*scriptExecutionContext()))
184 return WTFMove(*exception);
185 }
186 }
187
188 if (init.headers) {
189 auto fillResult = m_headers->fill(*init.headers);
190 if (fillResult.hasException())
191 return fillResult.releaseException();
192 }
193
194 if (init.body) {
195 auto setBodyResult = setBody(WTFMove(*init.body));
196 if (setBodyResult.hasException())
197 return setBodyResult.releaseException();
198 }
199
200 updateContentType();
201 return { };
202}
203
204ExceptionOr<void> FetchRequest::initializeWith(FetchRequest& input, Init&& init)
205{
206 m_request = input.m_request;
207 m_options = input.m_options;
208 m_referrer = input.m_referrer;
209
210 auto optionsResult = initializeOptions(init);
211 if (optionsResult.hasException())
212 return optionsResult.releaseException();
213
214 if (init.signal && !init.signal.isUndefined()) {
215 if (auto* signal = JSAbortSignal::toWrapped(scriptExecutionContext()->vm(), init.signal))
216 m_signal->follow(*signal);
217 else if (!init.signal.isNull()) {
218 if (auto exception = processInvalidSignal(*scriptExecutionContext()))
219 return WTFMove(*exception);
220 }
221
222 } else
223 m_signal->follow(input.m_signal.get());
224
225 if (init.headers) {
226 auto fillResult = m_headers->fill(*init.headers);
227 if (fillResult.hasException())
228 return fillResult.releaseException();
229 } else {
230 auto fillResult = m_headers->fill(input.headers());
231 if (fillResult.hasException())
232 return fillResult.releaseException();
233 }
234
235 if (init.body) {
236 auto setBodyResult = setBody(WTFMove(*init.body));
237 if (setBodyResult.hasException())
238 return setBodyResult.releaseException();
239 } else {
240 if (input.isDisturbedOrLocked())
241 return Exception { TypeError, "Request input is disturbed or locked."_s };
242
243 auto setBodyResult = setBody(input);
244 if (setBodyResult.hasException())
245 return setBodyResult.releaseException();
246 }
247
248 updateContentType();
249 return { };
250}
251
252ExceptionOr<void> FetchRequest::setBody(FetchBody::Init&& body)
253{
254 if (!methodCanHaveBody(m_request))
255 return Exception { TypeError, makeString("Request has method '", m_request.httpMethod(), "' and cannot have a body") };
256
257 ASSERT(scriptExecutionContext());
258 extractBody(*scriptExecutionContext(), WTFMove(body));
259
260 if (m_options.keepAlive && hasReadableStreamBody())
261 return Exception { TypeError, "Request cannot have a ReadableStream body and keepalive set to true"_s };
262 return { };
263}
264
265ExceptionOr<void> FetchRequest::setBody(FetchRequest& request)
266{
267 if (!request.isBodyNull()) {
268 if (!methodCanHaveBody(m_request))
269 return Exception { TypeError, makeString("Request has method '", m_request.httpMethod(), "' and cannot have a body") };
270 // FIXME: If body has a readable stream, we should pipe it to this new body stream.
271 m_body = WTFMove(*request.m_body);
272 request.setDisturbed();
273 }
274
275 if (m_options.keepAlive && hasReadableStreamBody())
276 return Exception { TypeError, "Request cannot have a ReadableStream body and keepalive set to true"_s };
277 return { };
278}
279
280ExceptionOr<Ref<FetchRequest>> FetchRequest::create(ScriptExecutionContext& context, Info&& input, Init&& init)
281{
282 auto request = adoptRef(*new FetchRequest(context, WTF::nullopt, FetchHeaders::create(FetchHeaders::Guard::Request), { }, { }, { }));
283
284 if (WTF::holds_alternative<String>(input)) {
285 auto result = request->initializeWith(WTF::get<String>(input), WTFMove(init));
286 if (result.hasException())
287 return result.releaseException();
288 } else {
289 auto result = request->initializeWith(*WTF::get<RefPtr<FetchRequest>>(input), WTFMove(init));
290 if (result.hasException())
291 return result.releaseException();
292 }
293
294 return request;
295}
296
297String FetchRequest::referrer() const
298{
299 if (m_referrer == "no-referrer")
300 return String();
301 if (m_referrer == "client")
302 return "about:client"_s;
303 return m_referrer;
304}
305
306const String& FetchRequest::urlString() const
307{
308 if (m_requestURL.isNull())
309 m_requestURL = m_request.url();
310 return m_requestURL;
311}
312
313ResourceRequest FetchRequest::resourceRequest() const
314{
315 ASSERT(scriptExecutionContext());
316
317 ResourceRequest request = m_request;
318 request.setHTTPHeaderFields(m_headers->internalHeaders());
319
320 if (!isBodyNull())
321 request.setHTTPBody(body().bodyAsFormData(*scriptExecutionContext()));
322
323 return request;
324}
325
326ExceptionOr<Ref<FetchRequest>> FetchRequest::clone(ScriptExecutionContext& context)
327{
328 if (isDisturbedOrLocked())
329 return Exception { TypeError, "Body is disturbed or locked"_s };
330
331 auto clone = adoptRef(*new FetchRequest(context, WTF::nullopt, FetchHeaders::create(m_headers.get()), ResourceRequest { m_request }, FetchOptions { m_options}, String { m_referrer }));
332 clone->cloneBody(*this);
333 clone->m_signal->follow(m_signal);
334 return clone;
335}
336
337const char* FetchRequest::activeDOMObjectName() const
338{
339 return "Request";
340}
341
342bool FetchRequest::canSuspendForDocumentSuspension() const
343{
344 // FIXME: We can probably do the same strategy as XHR.
345 return !isActive();
346}
347
348} // namespace WebCore
349
350