OLD | NEW |
| (Empty) |
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 | |
34 #include "modules/websockets/WebSocketHandshake.h" | |
35 | |
36 #include "core/dom/Document.h" | |
37 #include "core/inspector/ScriptCallStack.h" | |
38 #include "core/loader/CookieJar.h" | |
39 #include "modules/websockets/DOMWebSocket.h" | |
40 #include "platform/Cookie.h" | |
41 #include "platform/Crypto.h" | |
42 #include "platform/Logging.h" | |
43 #include "platform/network/HTTPHeaderMap.h" | |
44 #include "platform/network/HTTPParsers.h" | |
45 #include "platform/weborigin/SecurityOrigin.h" | |
46 #include "public/platform/Platform.h" | |
47 #include "wtf/CryptographicallyRandomNumber.h" | |
48 #include "wtf/StdLibExtras.h" | |
49 #include "wtf/StringExtras.h" | |
50 #include "wtf/Vector.h" | |
51 #include "wtf/text/Base64.h" | |
52 #include "wtf/text/CString.h" | |
53 #include "wtf/text/StringBuilder.h" | |
54 #include "wtf/unicode/CharacterNames.h" | |
55 | |
56 namespace blink { | |
57 | |
58 String formatHandshakeFailureReason(const String& detail) | |
59 { | |
60 return "Error during WebSocket handshake: " + detail; | |
61 } | |
62 | |
63 static String resourceName(const KURL& 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 KURL& url, bool secure) | |
80 { | |
81 ASSERT(url.protocolIs("wss") == secure); | |
82 StringBuilder builder; | |
83 builder.append(url.host().lower()); | |
84 if (url.port() && ((!secure && url.port() != 80) || (secure && url.port() !=
443))) { | |
85 builder.append(':'); | |
86 builder.appendNumber(url.port()); | |
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 if (len > maxInputSampleSize) | |
95 return String(p, maxInputSampleSize) + horizontalEllipsis; | |
96 return String(p, len); | |
97 } | |
98 | |
99 static String generateSecWebSocketKey() | |
100 { | |
101 static const size_t nonceSize = 16; | |
102 unsigned char key[nonceSize]; | |
103 cryptographicallyRandomValues(key, nonceSize); | |
104 return base64Encode(reinterpret_cast<char*>(key), nonceSize); | |
105 } | |
106 | |
107 String WebSocketHandshake::getExpectedWebSocketAccept(const String& secWebSocket
Key) | |
108 { | |
109 static const char webSocketKeyGUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11
"; | |
110 CString keyData = secWebSocketKey.ascii(); | |
111 | |
112 StringBuilder digestable; | |
113 digestable.append(secWebSocketKey); | |
114 digestable.append(webSocketKeyGUID, strlen(webSocketKeyGUID)); | |
115 CString digestableCString = digestable.toString().utf8(); | |
116 DigestValue digest; | |
117 bool digestSuccess = computeDigest(HashAlgorithmSha1, digestableCString.data
(), digestableCString.length(), digest); | |
118 RELEASE_ASSERT(digestSuccess); | |
119 | |
120 return base64Encode(reinterpret_cast<const char*>(digest.data()), sha1HashSi
ze); | |
121 } | |
122 | |
123 WebSocketHandshake::WebSocketHandshake(const KURL& url, const String& protocol,
Document* document) | |
124 : m_url(url) | |
125 , m_clientProtocol(protocol) | |
126 , m_secure(m_url.protocolIs("wss")) | |
127 , m_document(document) | |
128 , m_mode(Incomplete) | |
129 { | |
130 m_secWebSocketKey = generateSecWebSocketKey(); | |
131 m_expectedAccept = getExpectedWebSocketAccept(m_secWebSocketKey); | |
132 } | |
133 | |
134 WebSocketHandshake::~WebSocketHandshake() | |
135 { | |
136 Platform::current()->histogramEnumeration("WebCore.WebSocket.HandshakeResult
", m_mode, WebSocketHandshake::ModeMax); | |
137 } | |
138 | |
139 const KURL& WebSocketHandshake::url() const | |
140 { | |
141 return m_url; | |
142 } | |
143 | |
144 void WebSocketHandshake::setURL(const KURL& url) | |
145 { | |
146 m_url = url.copy(); | |
147 } | |
148 | |
149 const String WebSocketHandshake::host() const | |
150 { | |
151 return m_url.host().lower(); | |
152 } | |
153 | |
154 const String& WebSocketHandshake::clientProtocol() const | |
155 { | |
156 return m_clientProtocol; | |
157 } | |
158 | |
159 void WebSocketHandshake::setClientProtocol(const String& protocol) | |
160 { | |
161 m_clientProtocol = protocol; | |
162 } | |
163 | |
164 bool WebSocketHandshake::secure() const | |
165 { | |
166 return m_secure; | |
167 } | |
168 | |
169 String WebSocketHandshake::clientOrigin() const | |
170 { | |
171 return m_document->securityOrigin()->toString(); | |
172 } | |
173 | |
174 String WebSocketHandshake::clientLocation() const | |
175 { | |
176 StringBuilder builder; | |
177 if (m_secure) | |
178 builder.appendLiteral("wss"); | |
179 else | |
180 builder.appendLiteral("ws"); | |
181 builder.appendLiteral("://"); | |
182 builder.append(hostName(m_url, m_secure)); | |
183 builder.append(resourceName(m_url)); | |
184 return builder.toString(); | |
185 } | |
186 | |
187 CString WebSocketHandshake::clientHandshakeMessage() const | |
188 { | |
189 ASSERT(m_document); | |
190 | |
191 // Keep the following consistent with clientHandshakeRequest(). | |
192 StringBuilder builder; | |
193 | |
194 builder.appendLiteral("GET "); | |
195 builder.append(resourceName(m_url)); | |
196 builder.appendLiteral(" HTTP/1.1\r\n"); | |
197 | |
198 Vector<String> fields; | |
199 fields.append("Upgrade: websocket"); | |
200 fields.append("Connection: Upgrade"); | |
201 fields.append("Host: " + hostName(m_url, m_secure)); | |
202 fields.append("Origin: " + clientOrigin()); | |
203 if (!m_clientProtocol.isEmpty()) | |
204 fields.append("Sec-WebSocket-Protocol: " + m_clientProtocol); | |
205 | |
206 // Add no-cache headers to avoid compatibility issue. | |
207 // There are some proxies that rewrite "Connection: upgrade" | |
208 // to "Connection: close" in the response if a request doesn't contain | |
209 // these headers. | |
210 fields.append("Pragma: no-cache"); | |
211 fields.append("Cache-Control: no-cache"); | |
212 | |
213 fields.append("Sec-WebSocket-Key: " + m_secWebSocketKey); | |
214 fields.append("Sec-WebSocket-Version: 13"); | |
215 const String extensionValue = m_extensionDispatcher.createHeaderValue(); | |
216 if (extensionValue.length()) | |
217 fields.append("Sec-WebSocket-Extensions: " + extensionValue); | |
218 | |
219 fields.append("User-Agent: " + m_document->userAgent(m_document->url())); | |
220 | |
221 // Fields in the handshake are sent by the client in a random order; the | |
222 // order is not meaningful. Thus, it's ok to send the order we constructed | |
223 // the fields. | |
224 | |
225 for (size_t i = 0; i < fields.size(); i++) { | |
226 builder.append(fields[i]); | |
227 builder.appendLiteral("\r\n"); | |
228 } | |
229 | |
230 builder.appendLiteral("\r\n"); | |
231 | |
232 return builder.toString().utf8(); | |
233 } | |
234 | |
235 PassRefPtr<WebSocketHandshakeRequest> WebSocketHandshake::clientHandshakeRequest
() const | |
236 { | |
237 ASSERT(m_document); | |
238 | |
239 // Keep the following consistent with clientHandshakeMessage(). | |
240 // FIXME: do we need to store m_secWebSocketKey1, m_secWebSocketKey2 and | |
241 // m_key3 in WebSocketHandshakeRequest? | |
242 RefPtr<WebSocketHandshakeRequest> request = WebSocketHandshakeRequest::creat
e(m_url); | |
243 request->addHeaderField("Upgrade", "websocket"); | |
244 request->addHeaderField("Connection", "Upgrade"); | |
245 request->addHeaderField("Host", AtomicString(hostName(m_url, m_secure))); | |
246 request->addHeaderField("Origin", AtomicString(clientOrigin())); | |
247 if (!m_clientProtocol.isEmpty()) | |
248 request->addHeaderField("Sec-WebSocket-Protocol", AtomicString(m_clientP
rotocol)); | |
249 | |
250 KURL url = httpURLForAuthenticationAndCookies(); | |
251 | |
252 String cookie = cookieRequestHeaderFieldValue(m_document, url); | |
253 if (!cookie.isEmpty()) | |
254 request->addHeaderField("Cookie", AtomicString(cookie)); | |
255 // Set "Cookie2: <cookie>" if cookies 2 exists for url? | |
256 | |
257 request->addHeaderField("Pragma", "no-cache"); | |
258 request->addHeaderField("Cache-Control", "no-cache"); | |
259 | |
260 request->addHeaderField("Sec-WebSocket-Key", AtomicString(m_secWebSocketKey)
); | |
261 request->addHeaderField("Sec-WebSocket-Version", "13"); | |
262 const String extensionValue = m_extensionDispatcher.createHeaderValue(); | |
263 if (extensionValue.length()) | |
264 request->addHeaderField("Sec-WebSocket-Extensions", AtomicString(extensi
onValue)); | |
265 | |
266 request->addHeaderField("User-Agent", AtomicString(m_document->userAgent(m_d
ocument->url()))); | |
267 | |
268 return request.release(); | |
269 } | |
270 | |
271 void WebSocketHandshake::reset() | |
272 { | |
273 m_mode = Incomplete; | |
274 m_extensionDispatcher.reset(); | |
275 } | |
276 | |
277 void WebSocketHandshake::clearDocument() | |
278 { | |
279 m_document = nullptr; | |
280 } | |
281 | |
282 int WebSocketHandshake::readServerHandshake(const char* header, size_t len) | |
283 { | |
284 m_mode = Incomplete; | |
285 int statusCode; | |
286 String statusText; | |
287 int lineLength = readStatusLine(header, len, statusCode, statusText); | |
288 if (lineLength == -1) | |
289 return -1; | |
290 if (statusCode == -1) { | |
291 m_mode = Failed; // m_failureReason is set inside readStatusLine(). | |
292 return len; | |
293 } | |
294 WTF_LOG(Network, "WebSocketHandshake %p readServerHandshake() Status code is
%d", this, statusCode); | |
295 m_response.setStatusCode(statusCode); | |
296 m_response.setStatusText(statusText); | |
297 if (statusCode != 101) { | |
298 m_mode = Failed; | |
299 m_failureReason = formatHandshakeFailureReason("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 WTF_LOG(Network, "WebSocketHandshake %p readServerHandshake() readHTTPHe
aders() failed", this); | |
311 m_mode = Failed; // m_failureReason is set inside readHTTPHeaders(). | |
312 return len; | |
313 } | |
314 if (!checkResponseHeaders()) { | |
315 WTF_LOG(Network, "WebSocketHandshake %p readServerHandshake() checkRespo
nseHeaders() 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 const AtomicString& WebSocketHandshake::serverWebSocketProtocol() const | |
335 { | |
336 return m_response.headerFields().get("sec-websocket-protocol"); | |
337 } | |
338 | |
339 const AtomicString& WebSocketHandshake::serverUpgrade() const | |
340 { | |
341 return m_response.headerFields().get("upgrade"); | |
342 } | |
343 | |
344 const AtomicString& WebSocketHandshake::serverConnection() const | |
345 { | |
346 return m_response.headerFields().get("connection"); | |
347 } | |
348 | |
349 const AtomicString& WebSocketHandshake::serverWebSocketAccept() const | |
350 { | |
351 return m_response.headerFields().get("sec-websocket-accept"); | |
352 } | |
353 | |
354 String WebSocketHandshake::acceptedExtensions() const | |
355 { | |
356 return m_extensionDispatcher.acceptedExtensions(); | |
357 } | |
358 | |
359 const WebSocketHandshakeResponse& WebSocketHandshake::serverHandshakeResponse()
const | |
360 { | |
361 return m_response; | |
362 } | |
363 | |
364 void WebSocketHandshake::addExtensionProcessor(PassOwnPtr<WebSocketExtensionProc
essor> processor) | |
365 { | |
366 m_extensionDispatcher.addProcessor(processor); | |
367 } | |
368 | |
369 KURL WebSocketHandshake::httpURLForAuthenticationAndCookies() const | |
370 { | |
371 KURL url = m_url.copy(); | |
372 bool couldSetProtocol = url.setProtocol(m_secure ? "https" : "http"); | |
373 ASSERT_UNUSED(couldSetProtocol, couldSetProtocol); | |
374 return url; | |
375 } | |
376 | |
377 // Returns the header length (including "\r\n"), or -1 if we have not received e
nough data yet. | |
378 // If the line is malformed or the status code is not a 3-digit number, | |
379 // statusCode and statusText will be set to -1 and a null string, respectively. | |
380 int WebSocketHandshake::readStatusLine(const char* header, size_t headerLength,
int& statusCode, String& statusText) | |
381 { | |
382 // Arbitrary size limit to prevent the server from sending an unbounded | |
383 // amount of data with no newlines and forcing us to buffer it all. | |
384 static const int maximumLength = 1024; | |
385 | |
386 statusCode = -1; | |
387 statusText = String(); | |
388 | |
389 const char* space1 = 0; | |
390 const char* space2 = 0; | |
391 const char* p; | |
392 size_t consumedLength; | |
393 | |
394 for (p = header, consumedLength = 0; consumedLength < headerLength; p++, con
sumedLength++) { | |
395 if (*p == ' ') { | |
396 if (!space1) | |
397 space1 = p; | |
398 else if (!space2) | |
399 space2 = p; | |
400 } else if (*p == '\0') { | |
401 // The caller isn't prepared to deal with null bytes in status | |
402 // line. WebSockets specification doesn't prohibit this, but HTTP | |
403 // does, so we'll just treat this as an error. | |
404 m_failureReason = formatHandshakeFailureReason("Status line contains
embedded null"); | |
405 return p + 1 - header; | |
406 } else if (*p == '\n') { | |
407 break; | |
408 } | |
409 } | |
410 if (consumedLength == headerLength) | |
411 return -1; // We have not received '\n' yet. | |
412 | |
413 const char* end = p + 1; | |
414 int lineLength = end - header; | |
415 if (lineLength > maximumLength) { | |
416 m_failureReason = formatHandshakeFailureReason("Status line is too long"
); | |
417 return maximumLength; | |
418 } | |
419 | |
420 // The line must end with "\r\n". | |
421 if (lineLength < 2 || *(end - 2) != '\r') { | |
422 m_failureReason = formatHandshakeFailureReason("Status line does not end
with CRLF"); | |
423 return lineLength; | |
424 } | |
425 | |
426 if (!space1 || !space2) { | |
427 m_failureReason = formatHandshakeFailureReason("No response code found i
n status line: " + trimInputSample(header, lineLength - 2)); | |
428 return lineLength; | |
429 } | |
430 | |
431 String statusCodeString(space1 + 1, space2 - space1 - 1); | |
432 if (statusCodeString.length() != 3) // Status code must consist of three dig
its. | |
433 return lineLength; | |
434 for (int i = 0; i < 3; ++i) { | |
435 if (statusCodeString[i] < '0' || statusCodeString[i] > '9') { | |
436 m_failureReason = formatHandshakeFailureReason("Invalid status code:
" + statusCodeString); | |
437 return lineLength; | |
438 } | |
439 } | |
440 | |
441 bool ok = false; | |
442 statusCode = statusCodeString.toInt(&ok); | |
443 ASSERT(ok); | |
444 | |
445 statusText = String(space2 + 1, end - space2 - 3); // Exclude "\r\n". | |
446 return lineLength; | |
447 } | |
448 | |
449 const char* WebSocketHandshake::readHTTPHeaders(const char* start, const char* e
nd) | |
450 { | |
451 m_response.clearHeaderFields(); | |
452 | |
453 AtomicString name; | |
454 AtomicString value; | |
455 bool sawSecWebSocketAcceptHeaderField = false; | |
456 bool sawSecWebSocketProtocolHeaderField = false; | |
457 const char* p = start; | |
458 while (p < end) { | |
459 size_t consumedLength = parseHTTPHeader(p, end - p, m_failureReason, nam
e, value); | |
460 if (!consumedLength) | |
461 return 0; | |
462 p += consumedLength; | |
463 | |
464 // Stop once we consumed an empty line. | |
465 if (name.isEmpty()) | |
466 break; | |
467 | |
468 // Sec-WebSocket-Extensions may be split. We parse and check the | |
469 // header value every time the header appears. | |
470 if (equalIgnoringCase("Sec-WebSocket-Extensions", name)) { | |
471 if (!m_extensionDispatcher.processHeaderValue(value)) { | |
472 m_failureReason = formatHandshakeFailureReason(m_extensionDispat
cher.failureReason()); | |
473 return 0; | |
474 } | |
475 } else if (equalIgnoringCase("Sec-WebSocket-Accept", name)) { | |
476 if (sawSecWebSocketAcceptHeaderField) { | |
477 m_failureReason = formatHandshakeFailureReason("'Sec-WebSocket-A
ccept' header must not appear more than once in a response"); | |
478 return 0; | |
479 } | |
480 m_response.addHeaderField(name, value); | |
481 sawSecWebSocketAcceptHeaderField = true; | |
482 } else if (equalIgnoringCase("Sec-WebSocket-Protocol", name)) { | |
483 if (sawSecWebSocketProtocolHeaderField) { | |
484 m_failureReason = formatHandshakeFailureReason("'Sec-WebSocket-P
rotocol' header must not appear more than once in a response"); | |
485 return 0; | |
486 } | |
487 m_response.addHeaderField(name, value); | |
488 sawSecWebSocketProtocolHeaderField = true; | |
489 } else { | |
490 m_response.addHeaderField(name, value); | |
491 } | |
492 } | |
493 | |
494 String extensions = m_extensionDispatcher.acceptedExtensions(); | |
495 if (!extensions.isEmpty()) | |
496 m_response.addHeaderField("Sec-WebSocket-Extensions", AtomicString(exten
sions)); | |
497 return p; | |
498 } | |
499 | |
500 bool WebSocketHandshake::checkResponseHeaders() | |
501 { | |
502 const AtomicString& serverWebSocketProtocol = this->serverWebSocketProtocol(
); | |
503 const AtomicString& serverUpgrade = this->serverUpgrade(); | |
504 const AtomicString& serverConnection = this->serverConnection(); | |
505 const AtomicString& serverWebSocketAccept = this->serverWebSocketAccept(); | |
506 | |
507 if (serverUpgrade.isNull()) { | |
508 m_failureReason = formatHandshakeFailureReason("'Upgrade' header is miss
ing"); | |
509 return false; | |
510 } | |
511 if (serverConnection.isNull()) { | |
512 m_failureReason = formatHandshakeFailureReason("'Connection' header is m
issing"); | |
513 return false; | |
514 } | |
515 if (serverWebSocketAccept.isNull()) { | |
516 m_failureReason = formatHandshakeFailureReason("'Sec-WebSocket-Accept' h
eader is missing"); | |
517 return false; | |
518 } | |
519 | |
520 if (!equalIgnoringCase(serverUpgrade, "websocket")) { | |
521 m_failureReason = formatHandshakeFailureReason("'Upgrade' header value i
s not 'WebSocket': " + serverUpgrade); | |
522 return false; | |
523 } | |
524 if (!equalIgnoringCase(serverConnection, "upgrade")) { | |
525 m_failureReason = formatHandshakeFailureReason("'Connection' header valu
e is not 'Upgrade': " + serverConnection); | |
526 return false; | |
527 } | |
528 | |
529 if (serverWebSocketAccept != m_expectedAccept) { | |
530 m_failureReason = formatHandshakeFailureReason("Incorrect 'Sec-WebSocket
-Accept' header value"); | |
531 return false; | |
532 } | |
533 if (!serverWebSocketProtocol.isNull()) { | |
534 if (m_clientProtocol.isEmpty()) { | |
535 m_failureReason = formatHandshakeFailureReason("Response must not in
clude 'Sec-WebSocket-Protocol' header if not present in request: " + serverWebSo
cketProtocol); | |
536 return false; | |
537 } | |
538 Vector<String> result; | |
539 m_clientProtocol.split(String(DOMWebSocket::subprotocolSeperator()), res
ult); | |
540 if (!result.contains(serverWebSocketProtocol)) { | |
541 m_failureReason = formatHandshakeFailureReason("'Sec-WebSocket-Proto
col' header value '" + serverWebSocketProtocol + "' in response does not match a
ny of sent values"); | |
542 return false; | |
543 } | |
544 } else if (!m_clientProtocol.isEmpty()) { | |
545 m_failureReason = formatHandshakeFailureReason("Sent non-empty 'Sec-WebS
ocket-Protocol' header but no response was received"); | |
546 return false; | |
547 } | |
548 return true; | |
549 } | |
550 | |
551 void WebSocketHandshake::trace(Visitor* visitor) | |
552 { | |
553 visitor->trace(m_document); | |
554 } | |
555 | |
556 } // namespace blink | |
OLD | NEW |