Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(83)

Side by Side Diff: net/websockets/websocket_channel.cc

Issue 12764006: WebSocketChannel implementation (Closed) Base URL: http://git.chromium.org/chromium/src.git@web_socket_dispatcher
Patch Set: Make ConnectDelegate constructor explicit Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/websockets/websocket_channel.h"
6
7 #include <algorithm>
8
9 #include "base/basictypes.h" // for size_t
10 #include "base/bind.h"
11 #include "base/safe_numerics.h"
12 #include "base/strings/string_util.h"
13 #include "net/base/big_endian.h"
14 #include "net/base/io_buffer.h"
15 #include "net/http/http_request_info.h"
16 #include "net/http/http_stream_factory.h"
17 #include "net/ssl/ssl_config_service.h"
18 #include "net/websockets/websocket_errors.h"
19 #include "net/websockets/websocket_event_interface.h"
20 #include "net/websockets/websocket_frame.h"
21 #include "net/websockets/websocket_mux.h"
22 #include "net/websockets/websocket_stream.h"
23
24 namespace net {
25
26 namespace {
27
28 const int kDefaultSendQuotaLowWaterMark = 1 << 16;
29 const int kDefaultSendQuotaHighWaterMark = 1 << 17;
30 const size_t kWebSocketCloseCodeLength = 2;
31
32 // Concatenate the data from two IOBufferWithSize objects into a single one.
33 IOBufferWithSize* ConcatenateIOBuffers(
34 const scoped_refptr<IOBufferWithSize>& part1,
35 const scoped_refptr<IOBufferWithSize>& part2) {
36 int newsize = part1->size() + part2->size();
37 IOBufferWithSize* newbuffer = new IOBufferWithSize(newsize);
38 std::copy(part1->data(), part1->data() + part1->size(), newbuffer->data());
39 std::copy(part2->data(),
40 part2->data() + part2->size(),
41 newbuffer->data() + part1->size());
42 return newbuffer;
43 }
44
45 } // namespace
46
47 struct WebSocketChannel::SendBuffer {
48 SendBuffer() : total_bytes(0) {}
49 ScopedVector<WebSocketFrameChunk> frames;
50 size_t total_bytes;
51 };
52
53 // Implementation of WebSocketStream::ConnectDelegate that simply forwards the
54 // calls on to the WebSocketChannel that created it.
55 class WebSocketChannel::ConnectDelegate
56 : public WebSocketStream::ConnectDelegate {
57 public:
58 explicit ConnectDelegate(WebSocketChannel* creator) : creator_(creator) {}
59
60 virtual void OnSuccess(scoped_ptr<WebSocketStream> stream) OVERRIDE {
61 creator_->OnConnectSuccess(stream.Pass());
62 }
63
64 virtual void OnFailure(unsigned short websocket_error) OVERRIDE {
65 creator_->OnConnectFailure(websocket_error);
66 }
67
68 private:
69 // A pointer to the WebSocketChannel that created us. We do not need to worry
70 // about this pointer being stale, because deleting WebSocketChannel cancels
71 // the connect process, deleting this object and preventing its callbacks from
72 // being called.
73 WebSocketChannel* const creator_;
74
75 DISALLOW_COPY_AND_ASSIGN(ConnectDelegate);
76 };
77
78 WebSocketChannel::WebSocketChannel(
79 const GURL& socket_url,
80 scoped_ptr<WebSocketEventInterface> event_interface)
81 : socket_url_(socket_url),
82 event_interface_(event_interface.Pass()),
83 send_quota_low_water_mark_(kDefaultSendQuotaLowWaterMark),
84 send_quota_high_water_mark_(kDefaultSendQuotaHighWaterMark),
85 current_send_quota_(0),
86 state_(FRESHLY_CONSTRUCTED),
87 weak_factory_(this) {}
88
89 WebSocketChannel::~WebSocketChannel() {
90 // The stream may hold a pointer to read_frame_chunks_, and so it needs to be
91 // destroyed first.
92 stream_.reset();
93 }
94
95 void WebSocketChannel::SendAddChannelRequest(
96 const std::vector<std::string>& requested_subprotocols,
97 const GURL& origin,
98 URLRequestContext* url_request_context) {
99 // Delegate to the tested version.
100 SendAddChannelRequestWithFactory(
101 requested_subprotocols,
102 origin,
103 url_request_context,
104 base::Bind(&WebSocketStream::CreateAndConnectStream));
105 }
106
107 void WebSocketChannel::SendAddChannelRequestWithFactory(
108 const std::vector<std::string>& requested_subprotocols,
109 const GURL& origin,
110 URLRequestContext* url_request_context,
111 base::Callback<scoped_ptr<WebSocketStreamRequest>(
112 const GURL&,
113 const std::vector<std::string>&,
114 const GURL&,
115 URLRequestContext*,
116 const BoundNetLog&,
117 scoped_ptr<WebSocketStream::ConnectDelegate>)> factory) {
118 DCHECK_EQ(FRESHLY_CONSTRUCTED, state_);
119 scoped_ptr<WebSocketStream::ConnectDelegate> connect_delegate(
120 new WebSocketChannel::ConnectDelegate(this));
121 stream_request_ = factory.Run(socket_url_,
122 requested_subprotocols,
123 origin,
124 url_request_context,
125 BoundNetLog(),
126 connect_delegate.Pass());
127 state_ = CONNECTING;
128 }
129
130 void WebSocketChannel::OnConnectSuccess(scoped_ptr<WebSocketStream> stream) {
131 DCHECK(stream);
132 DCHECK_EQ(CONNECTING, state_);
133 stream_ = stream.Pass();
134 event_interface_->OnAddChannelResponse(false, stream_->GetSubProtocol());
tyoshino (SeeGerritForStatus) 2013/07/01 05:28:34 what action are the implementors of this interface
Adam Rice 2013/07/01 07:59:05 OnAddChannelResponse(true, ...) ie. "Connection Fa
135 // TODO(ricea): Get flow control information from the WebSocketStream once we
136 // have a multiplexing WebSocketStream.
137 event_interface_->OnFlowControl(send_quota_high_water_mark_);
138 current_send_quota_ = send_quota_high_water_mark_;
139 state_ = CONNECTED;
140 // We don't need this any more.
141 stream_request_.reset();
142 ReadFrames();
143 }
144
145 void WebSocketChannel::OnConnectFailure(unsigned short web_socket_error) {
146 DCHECK_EQ(CONNECTING, state_);
147 event_interface_->OnAddChannelResponse(true, "");
148 stream_request_.reset();
149 state_ = CLOSED;
150 }
151
152 void WebSocketChannel::SendFrame(bool fin,
153 WebSocketFrameHeader::OpCode op_code,
154 const std::vector<char>& data) {
155 if (data.size() > INT_MAX) {
156 NOTREACHED() << "Frame size sanity check failed";
157 return;
158 }
159 if (stream_ == NULL) {
160 LOG(DFATAL) << "Got SendFrame without a connection established; "
161 << "misbehaving renderer? fin=" << fin << " op_code=" << op_code
162 << " data.size()=" << data.size();
163 return;
164 }
165 if (state_ == SEND_CLOSED || state_ == RECV_CLOSED || state_ == CLOSED) {
166 VLOG(1) << "SendFrame called in state " << state_
167 << ". This may be a bug, or a harmless race.";
168 return;
169 }
170 if (state_ != CONNECTED) {
171 NOTREACHED() << "SendFrame() called in state " << state_;
172 return;
173 }
174 if (data.size() > base::checked_numeric_cast<size_t>(current_send_quota_)) {
175 FailChannel(SEND_INTERNAL_ERROR,
176 kWebSocketMuxErrorSendQuotaViolation,
177 "Send quota exceeded");
178 return;
179 }
180 if (!WebSocketFrameHeader::IsKnownDataOpCode(op_code)) {
181 LOG(DFATAL) << "Got SendFrame with bogus op_code " << op_code
182 << "; misbehaving renderer? fin=" << fin
183 << " data.size()=" << data.size();
184 return;
185 }
186 current_send_quota_ -= data.size();
187 // TODO(ricea): If current_send_quota_ has dropped below
188 // send_quota_low_water_mark_, we may want to consider increasing the "low
189 // water mark" and "high water mark", but only if we think we are not
190 // saturating the link to the WebSocket server.
191 // TODO(ricea): For kOpCodeText, do UTF-8 validation?
192 scoped_refptr<IOBufferWithSize> buffer(new IOBufferWithSize(data.size()));
193 std::copy(data.begin(), data.end(), buffer->data());
194 SendIOBufferWithSize(fin, op_code, buffer);
195 }
196
197 void WebSocketChannel::SendIOBufferWithSize(
198 bool fin,
199 WebSocketFrameHeader::OpCode op_code,
200 const scoped_refptr<IOBufferWithSize>& buffer) {
201 DCHECK(state_ == CONNECTED || state_ == RECV_CLOSED);
202 DCHECK(stream_);
203 scoped_ptr<WebSocketFrameHeader> header(new WebSocketFrameHeader(op_code));
204 header->final = fin;
205 header->masked = true;
206 header->payload_length = buffer->size();
207 scoped_ptr<WebSocketFrameChunk> chunk(new WebSocketFrameChunk());
208 chunk->header = header.Pass();
209 chunk->final_chunk = true;
210 chunk->data = buffer;
211 if (currently_sending_) {
212 // Either the link to the WebSocket server is saturated, or we are simply
213 // processing a batch of messages.
214 // TODO(ricea): We need to keep some statistics to work out which situation
215 // we are in and adjust quota appropriately.
216 if (!send_next_) {
217 send_next_.reset(new SendBuffer);
218 }
219 send_next_->frames.push_back(chunk.release());
220 send_next_->total_bytes += buffer->size();
221 } else {
222 currently_sending_.reset(new SendBuffer);
223 currently_sending_->frames.push_back(chunk.release());
224 currently_sending_->total_bytes += buffer->size();
225 WriteFrames();
226 }
227 }
228
229 void WebSocketChannel::WriteFrames() {
230 // This is safe because we own the WebSocketStream and destroying it cancels
231 // all callbacks.
232 int result = stream_->WriteFrames(
233 &(currently_sending_->frames),
234 base::Bind(&WebSocketChannel::OnWriteDone, base::Unretained(this)));
235 if (result != ERR_IO_PENDING) {
236 OnWriteDone(result);
237 }
238 }
239
240 void WebSocketChannel::OnWriteDone(int result) {
241 DCHECK(state_ != FRESHLY_CONSTRUCTED && state_ != CONNECTING);
242 DCHECK_NE(ERR_IO_PENDING, result);
243 DCHECK(currently_sending_);
244 switch (result) {
245 case OK:
246 if (send_next_) {
247 currently_sending_ = send_next_.Pass();
248 WriteFrames();
249 } else {
250 currently_sending_.reset();
251 if (current_send_quota_ < send_quota_low_water_mark_) {
252 // TODO(ricea): Increase low_water_mark and high_water_mark if
253 // throughput is high, reduce them if throughput is low. Low water
254 // mark needs to be >= the bandwidth delay product *of the IPC
255 // channel*. Because factors like context-switch time, thread wake-up
256 // time, and bus speed come into play it is complex and probably needs
257 // to be determined empirically.
258 DCHECK_LE(send_quota_low_water_mark_, send_quota_high_water_mark_);
259 // TODO(ricea): Truncate quota by the quota specified by the remote
260 // server, if the protocol in use supports quota.
261 int fresh_quota = send_quota_high_water_mark_ - current_send_quota_;
262 event_interface_->OnFlowControl(fresh_quota);
263 current_send_quota_ += fresh_quota;
264 }
265 }
266 break;
267
268 // If a recoverable error condition existed, it would go here.
269
270 default:
271 DCHECK_LT(result, 0)
272 << "WriteFrames() should only return OK or ERR_ codes";
273 stream_->Close();
274 state_ = CLOSED;
275 event_interface_->OnDropChannel(kWebSocketErrorAbnormalClosure,
276 "Abnormal Closure");
277 break;
278 }
279 }
280
281 void WebSocketChannel::ReadFrames() {
282 // This use if base::Unretained is safe because we own the WebSocketStream,
tyoshino (SeeGerritForStatus) 2013/07/01 05:05:18 if -> of?
Adam Rice 2013/07/01 07:59:05 Sorry, yes. Fixed.
283 // and any pending reads will be cancelled when it is destroyed.
284 int result = stream_->ReadFrames(
285 &read_frame_chunks_,
286 base::Bind(&WebSocketChannel::OnReadDone, base::Unretained(this)));
287 if (result != ERR_IO_PENDING) {
288 OnReadDone(result);
289 }
290 }
291
292 void WebSocketChannel::OnReadDone(int result) {
293 DCHECK(state_ != FRESHLY_CONSTRUCTED && state_ != CONNECTING);
294 DCHECK_NE(ERR_IO_PENDING, result);
295 switch (result) {
296 case OK:
297 // ReadFrames() must use ERR_CONNECTION_CLOSED for a closed connection
298 // with no data read, not an empty response.
299 DCHECK(!read_frame_chunks_.empty())
300 << "ReadFrames() returned OK, but nothing was read.";
301 for (size_t i = 0; i < read_frame_chunks_.size(); ++i) {
302 scoped_ptr<WebSocketFrameChunk> chunk(read_frame_chunks_[i]);
303 read_frame_chunks_[i] = NULL;
304 ProcessFrameChunk(chunk.Pass());
305 }
306 read_frame_chunks_.clear();
307 // We need to always keep a call to ReadFrames pending.
308 ReadFrames();
309 return;
310
311 case ERR_CONNECTION_CLOSED: {
312 State old_state = state_;
313 state_ = CLOSED;
314 if (old_state != RECV_CLOSED && old_state != CLOSED) {
315 // We need to inform the render process of the unexpected closure.
316 event_interface_->OnDropChannel(kWebSocketErrorAbnormalClosure,
317 "Abnormal Closure");
318 }
319 return;
320 }
321
322 default: {
323 DCHECK_LT(result, 0)
324 << "ReadFrames() should only return OK or ERR_ codes";
325 stream_->Close();
326 State old_state = state_;
327 state_ = CLOSED;
328 if (old_state != RECV_CLOSED && old_state != CLOSED) {
329 event_interface_->OnDropChannel(kWebSocketErrorAbnormalClosure,
330 "Abnormal Closure");
331 }
332 return;
333 }
334 }
335 }
336
337 // TODO(ricea): This method is too long. Break it up.
338 void WebSocketChannel::ProcessFrameChunk(
339 scoped_ptr<WebSocketFrameChunk> chunk) {
340 bool first_chunk = false;
341 if (chunk->header) {
342 first_chunk = true;
343 current_frame_header_.swap(chunk->header);
344 if (current_frame_header_->masked) {
345 // RFC6455 Section 5.1 "A client MUST close a connection if it detects a
346 // masked frame."
347 FailChannel(SEND_REAL_ERROR,
348 kWebSocketErrorProtocolError,
349 "Masked frame from server");
350 return;
351 }
352 }
353 if (!current_frame_header_) {
354 DCHECK(state_ != CONNECTED) << "Unexpected header-less frame received "
355 << "(final_chunk = " << chunk->final_chunk
356 << ", data size = " << chunk->data->size()
357 << ")";
358 return;
359 }
360 scoped_refptr<IOBufferWithSize> data_buffer;
361 data_buffer.swap(chunk->data);
362 WebSocketFrameHeader::OpCode opcode = current_frame_header_->opcode;
363 if (WebSocketFrameHeader::IsKnownControlOpCode(opcode)) {
364 if (chunk->final_chunk) {
365 if (incomplete_control_frame_body_) {
366 VLOG(2) << "Rejoining a split control frame, opcode "
367 << opcode;
368 data_buffer = ConcatenateIOBuffers(incomplete_control_frame_body_,
369 data_buffer);
370 incomplete_control_frame_body_ = NULL;
371 }
372 } else {
373 // TODO(ricea): Enforce a maximum size of 125 bytes on the control frames
374 // we accept.
375 VLOG(2) << "Encountered a split control frame, opcode "
376 << opcode;
377 if (incomplete_control_frame_body_) {
378 // The really horrid case. We need to create a new IOBufferWithSize
379 // combining the new one and the old one. This should virtually never
380 // happen.
381 // TODO(ricea): This algorithm is O(N^2). Use a fixed 127-byte byffer
382 // instead.
383 VLOG(3) << "Hit the really horrid case";
384 incomplete_control_frame_body_ =
385 ConcatenateIOBuffers(incomplete_control_frame_body_, data_buffer);
386 } else {
387 // The merely horrid case. Store the IOBufferWithSize to use when the
388 // rest of the control frame arrives.
389 incomplete_control_frame_body_.swap(data_buffer);
390 }
391 return;
392 }
393 }
394
395 switch (opcode) {
396 case WebSocketFrameHeader::kOpCodeText: // fall-thru
397 case WebSocketFrameHeader::kOpCodeBinary:
398 if (!first_chunk) {
399 opcode = WebSocketFrameHeader::kOpCodeContinuation;
400 }
401 // fall-thru
402 case WebSocketFrameHeader::kOpCodeContinuation:
403 if (state_ == RECV_CLOSED) {
404 FailChannel(SEND_REAL_ERROR,
405 kWebSocketErrorProtocolError,
406 "Data packet received after close");
407 return;
408 } else if (state_ == CONNECTED) {
409 const bool final = chunk->final_chunk && current_frame_header_->final;
410 // TODO(ricea): Can this copy be eliminated?
411 const char* const data_begin = data_buffer->data();
412 const char* const data_end = data_begin + data_buffer->size();
413 const std::vector<char> data(data_begin, data_end);
414 // TODO(ricea): Handle the (improbable) case when ReadFrames returns far
415 // more data at once than we want to send in a single IPC (in which case
416 // we need to buffer the data and return to the event loop with a
417 // callback to send the rest in 32K chunks).
418
419 // Send the received frame to the renderer process.
420 event_interface_->OnDataFrame(final, opcode, data);
421 } else {
422 VLOG(3) << "Ignored data packet received in state " << state_;
423 }
424 break;
425
426 case WebSocketFrameHeader::kOpCodePing:
427 VLOG(1) << "Got Ping of size " << data_buffer->size();
428 if (state_ == RECV_CLOSED) {
429 FailChannel(SEND_REAL_ERROR,
430 kWebSocketErrorProtocolError,
431 "Ping received after Close");
432 return;
433 } else if (state_ == CONNECTED) {
434 SendIOBufferWithSize(
435 true, WebSocketFrameHeader::kOpCodePong, data_buffer);
436 } else {
437 VLOG(3) << "Ignored ping in state " << state_;
438 }
439 break;
440
441 case WebSocketFrameHeader::kOpCodePong:
442 VLOG(1) << "Got Pong of size " << data_buffer->size();
443 if (state_ == RECV_CLOSED) {
444 FailChannel(SEND_REAL_ERROR,
445 kWebSocketErrorProtocolError,
446 "Pong received after Close");
447 return;
448 }
449 // We do not need to do anything with pong messages.
450 break;
451
452 case WebSocketFrameHeader::kOpCodeClose: {
453 unsigned short code = kWebSocketNormalClosure;
454 std::string reason;
455 ParseClose(data_buffer, &code, &reason);
456 // TODO(ricea): Find a way to safely log the message from the close
457 // message (escape control codes and so on).
458 VLOG(1) << "Got Close with code " << code;
459 switch (state_) {
460 case CONNECTED:
461 state_ = RECV_CLOSED;
462 SendClose(code, reason);
463 event_interface_->OnDropChannel(code, reason);
464 break;
465
466 case RECV_CLOSED:
467 FailChannel(SEND_REAL_ERROR,
468 kWebSocketErrorProtocolError,
469 "Close received after Close");
470 break;
471
472 case SEND_CLOSED:
473 state_ = CLOSED;
474 event_interface_->OnDropChannel(code, reason);
475 break;
476
477 default:
478 LOG(DFATAL) << "Got Close in unexpected state " << state_;
479 break;
480 }
481 break;
482 }
483
484 default:
485 FailChannel(SEND_REAL_ERROR,
486 kWebSocketErrorProtocolError,
487 "Unknown opcode");
488 break;
489 }
490 if (chunk->final_chunk) {
491 // Make sure we do not apply this frame header to any future chunks.
492 current_frame_header_.reset();
493 }
494 }
495
496 void WebSocketChannel::SendFlowControl(int64 quota) {
497 DCHECK_EQ(CONNECTED, state_);
498 // TODO(ricea): Add interface to WebSocketStream and implement.
499 // stream_->SendFlowControl(quota);
500 }
501
502 void WebSocketChannel::SendDropChannel(unsigned short code,
503 const std::string& reason) {
504 if (state_ == SEND_CLOSED || state_ == CLOSED) {
505 VLOG(1) << "SendDropChannel called in state " << state_
506 << ". This may be a bug, or a harmless race.";
507 return;
508 }
509 DCHECK_EQ(CONNECTED, state_);
510 // TODO(ricea): Validate |code|? Check that |reason| is valid UTF-8?
511 // TODO(ricea): There should be a timeout for the closing handshake.
512 SendClose(code, reason);
513 }
514
515 void WebSocketChannel::FailChannel(ExposeError expose,
516 unsigned short code,
517 const std::string& reason) {
518 // TODO(ricea): Logging.
519 State old_state = state_;
520 if (state_ == CONNECTED) {
521 unsigned short send_code = kWebSocketErrorGoingAway;
522 std::string send_reason = "Internal Error";
523 if (expose == SEND_REAL_ERROR) {
524 send_code = code;
525 send_reason = reason;
526 }
527 SendClose(send_code, send_reason);
528 }
529 // This method is mostly called in response to an invalid frame, in
530 // which case we should not re-use the header.
531 current_frame_header_.reset();
532 if (old_state != RECV_CLOSED && old_state != CLOSED) {
533 event_interface_->OnDropChannel(code, reason);
534 }
535 }
536
537 void WebSocketChannel::SendClose(unsigned short code,
538 const std::string& reason) {
539 DCHECK(state_ == CONNECTED || state_ == RECV_CLOSED);
540 uint64 payload_length = kWebSocketCloseCodeLength + reason.length();
541 scoped_refptr<IOBufferWithSize> body = new IOBufferWithSize(payload_length);
542 WriteBigEndian(body->data(), code);
543 COMPILE_ASSERT(sizeof(code) == kWebSocketCloseCodeLength,
544 they_should_both_be_two);
545 std::copy(reason.begin(),
546 reason.end(),
547 body->data() + kWebSocketCloseCodeLength);
548 SendIOBufferWithSize(true, WebSocketFrameHeader::kOpCodeClose, body);
549 state_ = state_ == CONNECTED ? SEND_CLOSED : CLOSED;
550 }
551
552 void WebSocketChannel::ParseClose(const scoped_refptr<IOBufferWithSize>& buffer,
553 unsigned short* code,
554 std::string* reason) {
555 const char* data = buffer->data();
556 size_t size = base::checked_numeric_cast<size_t>(buffer->size());
557 reason->clear();
558 if (size < kWebSocketCloseCodeLength) {
559 *code = kWebSocketErrorNoStatusReceived;
560 if (size != 0) {
561 VLOG(1) << "Close frame with payload size " << size << " received "
562 << "(the first byte is " << std::hex << static_cast<int>(data[0])
563 << ")";
564 return;
565 }
566 return;
567 }
568 unsigned short unchecked_code = 0;
569 ReadBigEndian(data, &unchecked_code);
570 COMPILE_ASSERT(sizeof(unchecked_code) == kWebSocketCloseCodeLength,
571 they_should_both_be_two_bytes);
572 if (unchecked_code >=
573 static_cast<unsigned short>(kWebSocketNormalClosure) &&
574 unchecked_code <=
575 static_cast<unsigned short>(kWebSocketErrorPrivateReservedMax)) {
576 *code = unchecked_code;
577 } else {
578 VLOG(1) << "Close frame contained code outside of the valid range: "
579 << unchecked_code;
580 *code = kWebSocketErrorProtocolError;
581 }
582 std::string text(data + kWebSocketCloseCodeLength, data + size);
583 // TODO(ricea): Is this check strict enough? In particular, check the
584 // "Security Considerations" from RFC3629.
585 if (IsStringUTF8(text)) {
586 using std::swap;
587 swap(*reason, text);
588 }
589 }
590
591 } // namespace net
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698