Use "= default" to denote default constructor or destructor
[WebKit-https.git] / Source / WebCore / Modules / websockets / WebSocketHandshake.cpp
1 /*
2  * Copyright (C) 2011 Google Inc.  All rights reserved.
3  * Copyright (C) Research In Motion Limited 2011. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include "config.h"
33 #include "WebSocketHandshake.h"
34
35 #include "Cookie.h"
36 #include "CookieJar.h"
37 #include "Document.h"
38 #include "HTTPHeaderMap.h"
39 #include "HTTPHeaderNames.h"
40 #include "HTTPParsers.h"
41 #include "Logging.h"
42 #include "ResourceRequest.h"
43 #include "ScriptExecutionContext.h"
44 #include "SecurityOrigin.h"
45 #include "URL.h"
46 #include "WebSocket.h"
47 #include <wtf/ASCIICType.h>
48 #include <wtf/CryptographicallyRandomNumber.h>
49 #include <wtf/MD5.h>
50 #include <wtf/SHA1.h>
51 #include <wtf/StdLibExtras.h>
52 #include <wtf/StringExtras.h>
53 #include <wtf/Vector.h>
54 #include <wtf/text/Base64.h>
55 #include <wtf/text/CString.h>
56 #include <wtf/text/StringBuilder.h>
57 #include <wtf/text/StringView.h>
58 #include <wtf/text/WTFString.h>
59 #include <wtf/unicode/CharacterNames.h>
60
61 namespace WebCore {
62
63 static String resourceName(const URL& url)
64 {
65     StringBuilder name;
66     name.append(url.path());
67     if (name.isEmpty())
68         name.append('/');
69     if (!url.query().isNull()) {
70         name.append('?');
71         name.append(url.query());
72     }
73     String result = name.toString();
74     ASSERT(!result.isEmpty());
75     ASSERT(!result.contains(' '));
76     return result;
77 }
78
79 static String hostName(const URL& url, bool secure)
80 {
81     ASSERT(url.protocolIs("wss") == secure);
82     StringBuilder builder;
83     builder.append(url.host().convertToASCIILowercase());
84     if (url.port() && ((!secure && url.port().value() != 80) || (secure && url.port().value() != 443))) {
85         builder.append(':');
86         builder.appendNumber(url.port().value());
87     }
88     return builder.toString();
89 }
90
91 static const size_t maxInputSampleSize = 128;
92 static String trimInputSample(const char* p, size_t len)
93 {
94     String s = String(p, std::min<size_t>(len, maxInputSampleSize));
95     if (len > maxInputSampleSize)
96         s.append(horizontalEllipsis);
97     return s;
98 }
99
100 static String generateSecWebSocketKey()
101 {
102     static const size_t nonceSize = 16;
103     unsigned char key[nonceSize];
104     cryptographicallyRandomValues(key, nonceSize);
105     return base64Encode(key, nonceSize);
106 }
107
108 String WebSocketHandshake::getExpectedWebSocketAccept(const String& secWebSocketKey)
109 {
110     static const char* const webSocketKeyGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
111     SHA1 sha1;
112     CString keyData = secWebSocketKey.ascii();
113     sha1.addBytes(reinterpret_cast<const uint8_t*>(keyData.data()), keyData.length());
114     sha1.addBytes(reinterpret_cast<const uint8_t*>(webSocketKeyGUID), strlen(webSocketKeyGUID));
115     SHA1::Digest hash;
116     sha1.computeHash(hash);
117     return base64Encode(hash.data(), SHA1::hashSize);
118 }
119
120 WebSocketHandshake::WebSocketHandshake(const URL& url, const String& protocol, Document* document, bool allowCookies)
121     : m_url(url)
122     , m_clientProtocol(protocol)
123     , m_secure(m_url.protocolIs("wss"))
124     , m_document(document)
125     , m_mode(Incomplete)
126     , m_allowCookies(allowCookies)
127 {
128     m_secWebSocketKey = generateSecWebSocketKey();
129     m_expectedAccept = getExpectedWebSocketAccept(m_secWebSocketKey);
130 }
131
132 WebSocketHandshake::~WebSocketHandshake() = default;
133
134 const URL& WebSocketHandshake::url() const
135 {
136     return m_url;
137 }
138
139 void WebSocketHandshake::setURL(const URL& url)
140 {
141     m_url = url.isolatedCopy();
142 }
143
144 // FIXME: Return type should just be String, not const String.
145 const String WebSocketHandshake::host() const
146 {
147     return m_url.host().convertToASCIILowercase();
148 }
149
150 const String& WebSocketHandshake::clientProtocol() const
151 {
152     return m_clientProtocol;
153 }
154
155 void WebSocketHandshake::setClientProtocol(const String& protocol)
156 {
157     m_clientProtocol = protocol;
158 }
159
160 bool WebSocketHandshake::secure() const
161 {
162     return m_secure;
163 }
164
165 String WebSocketHandshake::clientOrigin() const
166 {
167     return m_document->securityOrigin().toString();
168 }
169
170 String WebSocketHandshake::clientLocation() const
171 {
172     StringBuilder builder;
173     builder.append(m_secure ? "wss" : "ws");
174     builder.appendLiteral("://");
175     builder.append(hostName(m_url, m_secure));
176     builder.append(resourceName(m_url));
177     return builder.toString();
178 }
179
180 CString WebSocketHandshake::clientHandshakeMessage()
181 {
182     // Keep the following consistent with clientHandshakeRequest().
183     StringBuilder builder;
184
185     builder.appendLiteral("GET ");
186     builder.append(resourceName(m_url));
187     builder.appendLiteral(" HTTP/1.1\r\n");
188
189     Vector<String> fields;
190     fields.append("Upgrade: websocket");
191     fields.append("Connection: Upgrade");
192     fields.append("Host: " + hostName(m_url, m_secure));
193     fields.append("Origin: " + clientOrigin());
194     if (!m_clientProtocol.isEmpty())
195         fields.append("Sec-WebSocket-Protocol: " + m_clientProtocol);
196
197     URL url = httpURLForAuthenticationAndCookies();
198     if (m_allowCookies && m_document) {
199         String cookie = cookieRequestHeaderFieldValue(*m_document, url);
200         if (!cookie.isEmpty())
201             fields.append("Cookie: " + cookie);
202     }
203
204     // Add no-cache headers to avoid compatibility issue.
205     // There are some proxies that rewrite "Connection: upgrade"
206     // to "Connection: close" in the response if a request doesn't contain
207     // these headers.
208     fields.append("Pragma: no-cache");
209     fields.append("Cache-Control: no-cache");
210
211     fields.append("Sec-WebSocket-Key: " + m_secWebSocketKey);
212     fields.append("Sec-WebSocket-Version: 13");
213     const String extensionValue = m_extensionDispatcher.createHeaderValue();
214     if (extensionValue.length())
215         fields.append("Sec-WebSocket-Extensions: " + extensionValue);
216
217     // Add a User-Agent header.
218     fields.append("User-Agent: " + m_document->userAgent(m_document->url()));
219
220     // Fields in the handshake are sent by the client in a random order; the
221     // order is not meaningful.  Thus, it's ok to send the order we constructed
222     // the fields.
223
224     for (auto& field : fields) {
225         builder.append(field);
226         builder.appendLiteral("\r\n");
227     }
228
229     builder.appendLiteral("\r\n");
230
231     return builder.toString().utf8();
232 }
233
234 ResourceRequest WebSocketHandshake::clientHandshakeRequest()
235 {
236     // Keep the following consistent with clientHandshakeMessage().
237     ResourceRequest request(m_url);
238     request.setHTTPMethod("GET");
239
240     request.setHTTPHeaderField(HTTPHeaderName::Connection, "Upgrade");
241     request.setHTTPHeaderField(HTTPHeaderName::Host, hostName(m_url, m_secure));
242     request.setHTTPHeaderField(HTTPHeaderName::Origin, clientOrigin());
243     if (!m_clientProtocol.isEmpty())
244         request.setHTTPHeaderField(HTTPHeaderName::SecWebSocketProtocol, m_clientProtocol);
245
246     URL url = httpURLForAuthenticationAndCookies();
247     if (m_allowCookies && m_document) {
248         String cookie = cookieRequestHeaderFieldValue(*m_document, url);
249         if (!cookie.isEmpty())
250             request.setHTTPHeaderField(HTTPHeaderName::Cookie, cookie);
251     }
252
253     request.setHTTPHeaderField(HTTPHeaderName::Pragma, "no-cache");
254     request.setHTTPHeaderField(HTTPHeaderName::CacheControl, "no-cache");
255
256     request.setHTTPHeaderField(HTTPHeaderName::SecWebSocketKey, m_secWebSocketKey);
257     request.setHTTPHeaderField(HTTPHeaderName::SecWebSocketVersion, "13");
258     const String extensionValue = m_extensionDispatcher.createHeaderValue();
259     if (extensionValue.length())
260         request.setHTTPHeaderField(HTTPHeaderName::SecWebSocketExtensions, extensionValue);
261
262     // Add a User-Agent header.
263     request.setHTTPHeaderField(HTTPHeaderName::UserAgent, m_document->userAgent(m_document->url()));
264
265     return request;
266 }
267
268 void WebSocketHandshake::reset()
269 {
270     m_mode = Incomplete;
271     m_extensionDispatcher.reset();
272 }
273
274 void WebSocketHandshake::clearDocument()
275 {
276     m_document = nullptr;
277 }
278
279 int WebSocketHandshake::readServerHandshake(const char* header, size_t len)
280 {
281     m_mode = Incomplete;
282     int statusCode;
283     String statusText;
284     int lineLength = readStatusLine(header, len, statusCode, statusText);
285     if (lineLength == -1)
286         return -1;
287     if (statusCode == -1) {
288         m_mode = Failed; // m_failureReason is set inside readStatusLine().
289         return len;
290     }
291     LOG(Network, "WebSocketHandshake %p readServerHandshake() Status code is %d", this, statusCode);
292
293     m_serverHandshakeResponse = ResourceResponse();
294     m_serverHandshakeResponse.setHTTPStatusCode(statusCode);
295     m_serverHandshakeResponse.setHTTPStatusText(statusText);
296
297     if (statusCode != 101) {
298         m_mode = Failed;
299         m_failureReason = makeString("Unexpected response code: ", String::number(statusCode));
300         return len;
301     }
302     m_mode = Normal;
303     if (!strnstr(header, "\r\n\r\n", len)) {
304         // Just hasn't been received fully yet.
305         m_mode = Incomplete;
306         return -1;
307     }
308     const char* p = readHTTPHeaders(header + lineLength, header + len);
309     if (!p) {
310         LOG(Network, "WebSocketHandshake %p readServerHandshake() readHTTPHeaders() failed", this);
311         m_mode = Failed; // m_failureReason is set inside readHTTPHeaders().
312         return len;
313     }
314     if (!checkResponseHeaders()) {
315         LOG(Network, "WebSocketHandshake %p readServerHandshake() checkResponseHeaders() failed", this);
316         m_mode = Failed;
317         return p - header;
318     }
319
320     m_mode = Connected;
321     return p - header;
322 }
323
324 WebSocketHandshake::Mode WebSocketHandshake::mode() const
325 {
326     return m_mode;
327 }
328
329 String WebSocketHandshake::failureReason() const
330 {
331     return m_failureReason;
332 }
333
334 String WebSocketHandshake::serverWebSocketProtocol() const
335 {
336     return m_serverHandshakeResponse.httpHeaderFields().get(HTTPHeaderName::SecWebSocketProtocol);
337 }
338
339 String WebSocketHandshake::serverSetCookie() const
340 {
341     return m_serverHandshakeResponse.httpHeaderFields().get(HTTPHeaderName::SetCookie);
342 }
343
344 String WebSocketHandshake::serverUpgrade() const
345 {
346     return m_serverHandshakeResponse.httpHeaderFields().get(HTTPHeaderName::Upgrade);
347 }
348
349 String WebSocketHandshake::serverConnection() const
350 {
351     return m_serverHandshakeResponse.httpHeaderFields().get(HTTPHeaderName::Connection);
352 }
353
354 String WebSocketHandshake::serverWebSocketAccept() const
355 {
356     return m_serverHandshakeResponse.httpHeaderFields().get(HTTPHeaderName::SecWebSocketAccept);
357 }
358
359 String WebSocketHandshake::acceptedExtensions() const
360 {
361     return m_extensionDispatcher.acceptedExtensions();
362 }
363
364 const ResourceResponse& WebSocketHandshake::serverHandshakeResponse() const
365 {
366     return m_serverHandshakeResponse;
367 }
368
369 void WebSocketHandshake::addExtensionProcessor(std::unique_ptr<WebSocketExtensionProcessor> processor)
370 {
371     m_extensionDispatcher.addProcessor(WTFMove(processor));
372 }
373
374 URL WebSocketHandshake::httpURLForAuthenticationAndCookies() const
375 {
376     URL url = m_url.isolatedCopy();
377     bool couldSetProtocol = url.setProtocol(m_secure ? "https" : "http");
378     ASSERT_UNUSED(couldSetProtocol, couldSetProtocol);
379     return url;
380 }
381
382 // https://tools.ietf.org/html/rfc6455#section-4.1
383 // "The HTTP version MUST be at least 1.1."
384 static inline bool headerHasValidHTTPVersion(StringView httpStatusLine)
385 {
386     const char* httpVersionStaticPreambleLiteral = "HTTP/";
387     StringView httpVersionStaticPreamble(reinterpret_cast<const LChar*>(httpVersionStaticPreambleLiteral), strlen(httpVersionStaticPreambleLiteral));
388     if (!httpStatusLine.startsWith(httpVersionStaticPreamble))
389         return false;
390
391     // Check that there is a version number which should be at least three characters after "HTTP/"
392     unsigned preambleLength = httpVersionStaticPreamble.length();
393     if (httpStatusLine.length() < preambleLength + 3)
394         return false;
395
396     auto dotPosition = httpStatusLine.find('.', preambleLength);
397     if (dotPosition == notFound)
398         return false;
399
400     StringView majorVersionView = httpStatusLine.substring(preambleLength, dotPosition - preambleLength);
401     bool isValid;
402     int majorVersion = majorVersionView.toIntStrict(isValid);
403     if (!isValid)
404         return false;
405
406     unsigned minorVersionLength;
407     unsigned charactersLeftAfterDotPosition = httpStatusLine.length() - dotPosition;
408     for (minorVersionLength = 1; minorVersionLength < charactersLeftAfterDotPosition; minorVersionLength++) {
409         if (!isASCIIDigit(httpStatusLine[dotPosition + minorVersionLength]))
410             break;
411     }
412     int minorVersion = (httpStatusLine.substring(dotPosition + 1, minorVersionLength)).toIntStrict(isValid);
413     if (!isValid)
414         return false;
415
416     return (majorVersion >= 1 && minorVersion >= 1) || majorVersion >= 2;
417 }
418
419 // Returns the header length (including "\r\n"), or -1 if we have not received enough data yet.
420 // If the line is malformed or the status code is not a 3-digit number,
421 // statusCode and statusText will be set to -1 and a null string, respectively.
422 int WebSocketHandshake::readStatusLine(const char* header, size_t headerLength, int& statusCode, String& statusText)
423 {
424     // Arbitrary size limit to prevent the server from sending an unbounded
425     // amount of data with no newlines and forcing us to buffer it all.
426     static const int maximumLength = 1024;
427
428     statusCode = -1;
429     statusText = String();
430
431     const char* space1 = nullptr;
432     const char* space2 = nullptr;
433     const char* p;
434     size_t consumedLength;
435
436     for (p = header, consumedLength = 0; consumedLength < headerLength; p++, consumedLength++) {
437         if (*p == ' ') {
438             if (!space1)
439                 space1 = p;
440             else if (!space2)
441                 space2 = p;
442         } else if (*p == '\0') {
443             // The caller isn't prepared to deal with null bytes in status
444             // line. WebSockets specification doesn't prohibit this, but HTTP
445             // does, so we'll just treat this as an error.
446             m_failureReason = ASCIILiteral("Status line contains embedded null");
447             return p + 1 - header;
448         } else if (!isASCII(*p)) {
449             m_failureReason = ASCIILiteral("Status line contains non-ASCII character");
450             return p + 1 - header;
451         } else if (*p == '\n')
452             break;
453     }
454     if (consumedLength == headerLength)
455         return -1; // We have not received '\n' yet.
456
457     const char* end = p + 1;
458     int lineLength = end - header;
459     if (lineLength > maximumLength) {
460         m_failureReason = ASCIILiteral("Status line is too long");
461         return maximumLength;
462     }
463
464     // The line must end with "\r\n".
465     if (lineLength < 2 || *(end - 2) != '\r') {
466         m_failureReason = ASCIILiteral("Status line does not end with CRLF");
467         return lineLength;
468     }
469
470     if (!space1 || !space2) {
471         m_failureReason = makeString("No response code found: ", trimInputSample(header, lineLength - 2));
472         return lineLength;
473     }
474
475     StringView httpStatusLine(reinterpret_cast<const LChar*>(header), space1 - header);
476     if (!headerHasValidHTTPVersion(httpStatusLine)) {
477         m_failureReason = makeString("Invalid HTTP version string: ", httpStatusLine);
478         return lineLength;
479     }
480
481     StringView statusCodeString(reinterpret_cast<const LChar*>(space1 + 1), space2 - space1 - 1);
482     if (statusCodeString.length() != 3) // Status code must consist of three digits.
483         return lineLength;
484     for (int i = 0; i < 3; ++i)
485         if (!isASCIIDigit(statusCodeString[i])) {
486             m_failureReason = makeString("Invalid status code: ", statusCodeString);
487             return lineLength;
488         }
489
490     bool ok = false;
491     statusCode = statusCodeString.toIntStrict(ok);
492     ASSERT(ok);
493
494     statusText = String(space2 + 1, end - space2 - 3); // Exclude "\r\n".
495     return lineLength;
496 }
497
498 const char* WebSocketHandshake::readHTTPHeaders(const char* start, const char* end)
499 {
500     StringView name;
501     String value;
502     bool sawSecWebSocketExtensionsHeaderField = false;
503     bool sawSecWebSocketAcceptHeaderField = false;
504     bool sawSecWebSocketProtocolHeaderField = false;
505     const char* p = start;
506     for (; p < end; p++) {
507         size_t consumedLength = parseHTTPHeader(p, end - p, m_failureReason, name, value);
508         if (!consumedLength)
509             return nullptr;
510         p += consumedLength;
511
512         // Stop once we consumed an empty line.
513         if (name.isEmpty())
514             break;
515
516         HTTPHeaderName headerName;
517         if (!findHTTPHeaderName(name, headerName)) {
518             // Evidence in the wild shows that services make use of custom headers in the handshake
519             m_serverHandshakeResponse.addHTTPHeaderField(name.toString(), value);
520             continue;
521         }
522
523         // https://tools.ietf.org/html/rfc7230#section-3.2.4
524         // "Newly defined header fields SHOULD limit their field values to US-ASCII octets."
525         if ((headerName == HTTPHeaderName::SecWebSocketExtensions
526             || headerName == HTTPHeaderName::SecWebSocketAccept
527             || headerName == HTTPHeaderName::SecWebSocketProtocol)
528             && !value.containsOnlyASCII()) {
529             m_failureReason = makeString(name, " header value should only contain ASCII characters");
530             return nullptr;
531         }
532         
533         if (headerName == HTTPHeaderName::SecWebSocketExtensions) {
534             if (sawSecWebSocketExtensionsHeaderField) {
535                 m_failureReason = ASCIILiteral("The Sec-WebSocket-Extensions header must not appear more than once in an HTTP response");
536                 return nullptr;
537             }
538             if (!m_extensionDispatcher.processHeaderValue(value)) {
539                 m_failureReason = m_extensionDispatcher.failureReason();
540                 return nullptr;
541             }
542             sawSecWebSocketExtensionsHeaderField = true;
543         } else {
544             if (headerName == HTTPHeaderName::SecWebSocketAccept) {
545                 if (sawSecWebSocketAcceptHeaderField) {
546                     m_failureReason = ASCIILiteral("The Sec-WebSocket-Accept header must not appear more than once in an HTTP response");
547                     return nullptr;
548                 }
549                 sawSecWebSocketAcceptHeaderField = true;
550             } else if (headerName == HTTPHeaderName::SecWebSocketProtocol) {
551                 if (sawSecWebSocketProtocolHeaderField) {
552                     m_failureReason = ASCIILiteral("The Sec-WebSocket-Protocol header must not appear more than once in an HTTP response");
553                     return nullptr;
554                 }
555                 sawSecWebSocketProtocolHeaderField = true;
556             }
557
558             m_serverHandshakeResponse.addHTTPHeaderField(headerName, value);
559         }
560     }
561     return p;
562 }
563
564 bool WebSocketHandshake::checkResponseHeaders()
565 {
566     const String& serverWebSocketProtocol = this->serverWebSocketProtocol();
567     const String& serverUpgrade = this->serverUpgrade();
568     const String& serverConnection = this->serverConnection();
569     const String& serverWebSocketAccept = this->serverWebSocketAccept();
570
571     if (serverUpgrade.isNull()) {
572         m_failureReason = ASCIILiteral("Error during WebSocket handshake: 'Upgrade' header is missing");
573         return false;
574     }
575     if (serverConnection.isNull()) {
576         m_failureReason = ASCIILiteral("Error during WebSocket handshake: 'Connection' header is missing");
577         return false;
578     }
579     if (serverWebSocketAccept.isNull()) {
580         m_failureReason = ASCIILiteral("Error during WebSocket handshake: 'Sec-WebSocket-Accept' header is missing");
581         return false;
582     }
583
584     if (!equalLettersIgnoringASCIICase(serverUpgrade, "websocket")) {
585         m_failureReason = ASCIILiteral("Error during WebSocket handshake: 'Upgrade' header value is not 'WebSocket'");
586         return false;
587     }
588     if (!equalLettersIgnoringASCIICase(serverConnection, "upgrade")) {
589         m_failureReason = ASCIILiteral("Error during WebSocket handshake: 'Connection' header value is not 'Upgrade'");
590         return false;
591     }
592
593     if (serverWebSocketAccept != m_expectedAccept) {
594         m_failureReason = ASCIILiteral("Error during WebSocket handshake: Sec-WebSocket-Accept mismatch");
595         return false;
596     }
597     if (!serverWebSocketProtocol.isNull()) {
598         if (m_clientProtocol.isEmpty()) {
599             m_failureReason = ASCIILiteral("Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch");
600             return false;
601         }
602         Vector<String> result;
603         m_clientProtocol.split(WebSocket::subprotocolSeparator(), result);
604         if (!result.contains(serverWebSocketProtocol)) {
605             m_failureReason = ASCIILiteral("Error during WebSocket handshake: Sec-WebSocket-Protocol mismatch");
606             return false;
607         }
608     }
609     return true;
610 }
611
612 } // namespace WebCore