OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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/spdy/spdy_proxy_client_socket.h" | |
6 | |
7 #include <algorithm> // min | |
8 | |
9 #include "base/bind.h" | |
10 #include "base/bind_helpers.h" | |
11 #include "base/callback_helpers.h" | |
12 #include "base/logging.h" | |
13 #include "base/strings/string_util.h" | |
14 #include "base/values.h" | |
15 #include "net/base/auth.h" | |
16 #include "net/base/io_buffer.h" | |
17 #include "net/base/net_util.h" | |
18 #include "net/http/http_auth_cache.h" | |
19 #include "net/http/http_auth_handler_factory.h" | |
20 #include "net/http/http_response_headers.h" | |
21 #include "net/http/proxy_connect_redirect_http_stream.h" | |
22 #include "net/spdy/spdy_http_utils.h" | |
23 #include "url/gurl.h" | |
24 | |
25 namespace net { | |
26 | |
27 SpdyProxyClientSocket::SpdyProxyClientSocket( | |
28 const base::WeakPtr<SpdyStream>& spdy_stream, | |
29 const std::string& user_agent, | |
30 const HostPortPair& endpoint, | |
31 const GURL& url, | |
32 const HostPortPair& proxy_server, | |
33 const BoundNetLog& source_net_log, | |
34 HttpAuthCache* auth_cache, | |
35 HttpAuthHandlerFactory* auth_handler_factory) | |
36 : next_state_(STATE_DISCONNECTED), | |
37 spdy_stream_(spdy_stream), | |
38 endpoint_(endpoint), | |
39 auth_(new HttpAuthController(HttpAuth::AUTH_PROXY, | |
40 GURL("https://" + proxy_server.ToString()), | |
41 auth_cache, | |
42 auth_handler_factory)), | |
43 user_buffer_len_(0), | |
44 write_buffer_len_(0), | |
45 was_ever_used_(false), | |
46 redirect_has_load_timing_info_(false), | |
47 net_log_(BoundNetLog::Make(spdy_stream->net_log().net_log(), | |
48 NetLog::SOURCE_PROXY_CLIENT_SOCKET)), | |
49 weak_factory_(this), | |
50 write_callback_weak_factory_(this) { | |
51 request_.method = "CONNECT"; | |
52 request_.url = url; | |
53 if (!user_agent.empty()) | |
54 request_.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, | |
55 user_agent); | |
56 | |
57 net_log_.BeginEvent(NetLog::TYPE_SOCKET_ALIVE, | |
58 source_net_log.source().ToEventParametersCallback()); | |
59 net_log_.AddEvent( | |
60 NetLog::TYPE_SPDY_PROXY_CLIENT_SESSION, | |
61 spdy_stream->net_log().source().ToEventParametersCallback()); | |
62 | |
63 spdy_stream_->SetDelegate(this); | |
64 was_ever_used_ = spdy_stream_->WasEverUsed(); | |
65 } | |
66 | |
67 SpdyProxyClientSocket::~SpdyProxyClientSocket() { | |
68 Disconnect(); | |
69 net_log_.EndEvent(NetLog::TYPE_SOCKET_ALIVE); | |
70 } | |
71 | |
72 const HttpResponseInfo* SpdyProxyClientSocket::GetConnectResponseInfo() const { | |
73 return response_.headers.get() ? &response_ : NULL; | |
74 } | |
75 | |
76 const scoped_refptr<HttpAuthController>& | |
77 SpdyProxyClientSocket::GetAuthController() const { | |
78 return auth_; | |
79 } | |
80 | |
81 int SpdyProxyClientSocket::RestartWithAuth(const CompletionCallback& callback) { | |
82 // A SPDY Stream can only handle a single request, so the underlying | |
83 // stream may not be reused and a new SpdyProxyClientSocket must be | |
84 // created (possibly on top of the same SPDY Session). | |
85 next_state_ = STATE_DISCONNECTED; | |
86 return OK; | |
87 } | |
88 | |
89 bool SpdyProxyClientSocket::IsUsingSpdy() const { | |
90 return true; | |
91 } | |
92 | |
93 NextProto SpdyProxyClientSocket::GetProtocolNegotiated() const { | |
94 // Save the negotiated protocol | |
95 SSLInfo ssl_info; | |
96 bool was_npn_negotiated; | |
97 NextProto protocol_negotiated; | |
98 spdy_stream_->GetSSLInfo(&ssl_info, &was_npn_negotiated, | |
99 &protocol_negotiated); | |
100 return protocol_negotiated; | |
101 } | |
102 | |
103 HttpStream* SpdyProxyClientSocket::CreateConnectResponseStream() { | |
104 return new ProxyConnectRedirectHttpStream( | |
105 redirect_has_load_timing_info_ ? &redirect_load_timing_info_ : NULL); | |
106 } | |
107 | |
108 // Sends a SYN_STREAM frame to the proxy with a CONNECT request | |
109 // for the specified endpoint. Waits for the server to send back | |
110 // a SYN_REPLY frame. OK will be returned if the status is 200. | |
111 // ERR_TUNNEL_CONNECTION_FAILED will be returned for any other status. | |
112 // In any of these cases, Read() may be called to retrieve the HTTP | |
113 // response body. Any other return values should be considered fatal. | |
114 // TODO(rch): handle 407 proxy auth requested correctly, perhaps | |
115 // by creating a new stream for the subsequent request. | |
116 // TODO(rch): create a more appropriate error code to disambiguate | |
117 // the HTTPS Proxy tunnel failure from an HTTP Proxy tunnel failure. | |
118 int SpdyProxyClientSocket::Connect(const CompletionCallback& callback) { | |
119 DCHECK(read_callback_.is_null()); | |
120 if (next_state_ == STATE_OPEN) | |
121 return OK; | |
122 | |
123 DCHECK_EQ(STATE_DISCONNECTED, next_state_); | |
124 next_state_ = STATE_GENERATE_AUTH_TOKEN; | |
125 | |
126 int rv = DoLoop(OK); | |
127 if (rv == ERR_IO_PENDING) | |
128 read_callback_ = callback; | |
129 return rv; | |
130 } | |
131 | |
132 void SpdyProxyClientSocket::Disconnect() { | |
133 read_buffer_queue_.Clear(); | |
134 user_buffer_ = NULL; | |
135 user_buffer_len_ = 0; | |
136 read_callback_.Reset(); | |
137 | |
138 write_buffer_len_ = 0; | |
139 write_callback_.Reset(); | |
140 write_callback_weak_factory_.InvalidateWeakPtrs(); | |
141 | |
142 next_state_ = STATE_DISCONNECTED; | |
143 | |
144 if (spdy_stream_.get()) { | |
145 // This will cause OnClose to be invoked, which takes care of | |
146 // cleaning up all the internal state. | |
147 spdy_stream_->Cancel(); | |
148 DCHECK(!spdy_stream_.get()); | |
149 } | |
150 } | |
151 | |
152 bool SpdyProxyClientSocket::IsConnected() const { | |
153 return next_state_ == STATE_OPEN; | |
154 } | |
155 | |
156 bool SpdyProxyClientSocket::IsConnectedAndIdle() const { | |
157 return IsConnected() && read_buffer_queue_.IsEmpty() && | |
158 spdy_stream_->IsOpen(); | |
159 } | |
160 | |
161 const BoundNetLog& SpdyProxyClientSocket::NetLog() const { | |
162 return net_log_; | |
163 } | |
164 | |
165 void SpdyProxyClientSocket::SetSubresourceSpeculation() { | |
166 // TODO(rch): what should this implementation be? | |
167 } | |
168 | |
169 void SpdyProxyClientSocket::SetOmniboxSpeculation() { | |
170 // TODO(rch): what should this implementation be? | |
171 } | |
172 | |
173 bool SpdyProxyClientSocket::WasEverUsed() const { | |
174 return was_ever_used_ || (spdy_stream_.get() && spdy_stream_->WasEverUsed()); | |
175 } | |
176 | |
177 bool SpdyProxyClientSocket::UsingTCPFastOpen() const { | |
178 return false; | |
179 } | |
180 | |
181 bool SpdyProxyClientSocket::WasNpnNegotiated() const { | |
182 return false; | |
183 } | |
184 | |
185 NextProto SpdyProxyClientSocket::GetNegotiatedProtocol() const { | |
186 return kProtoUnknown; | |
187 } | |
188 | |
189 bool SpdyProxyClientSocket::GetSSLInfo(SSLInfo* ssl_info) { | |
190 bool was_npn_negotiated; | |
191 NextProto protocol_negotiated; | |
192 return spdy_stream_->GetSSLInfo(ssl_info, &was_npn_negotiated, | |
193 &protocol_negotiated); | |
194 } | |
195 | |
196 int SpdyProxyClientSocket::Read(IOBuffer* buf, int buf_len, | |
197 const CompletionCallback& callback) { | |
198 DCHECK(read_callback_.is_null()); | |
199 DCHECK(!user_buffer_.get()); | |
200 | |
201 if (next_state_ == STATE_DISCONNECTED) | |
202 return ERR_SOCKET_NOT_CONNECTED; | |
203 | |
204 if (next_state_ == STATE_CLOSED && read_buffer_queue_.IsEmpty()) { | |
205 return 0; | |
206 } | |
207 | |
208 DCHECK(next_state_ == STATE_OPEN || next_state_ == STATE_CLOSED); | |
209 DCHECK(buf); | |
210 size_t result = PopulateUserReadBuffer(buf->data(), buf_len); | |
211 if (result == 0) { | |
212 user_buffer_ = buf; | |
213 user_buffer_len_ = static_cast<size_t>(buf_len); | |
214 DCHECK(!callback.is_null()); | |
215 read_callback_ = callback; | |
216 return ERR_IO_PENDING; | |
217 } | |
218 user_buffer_ = NULL; | |
219 return result; | |
220 } | |
221 | |
222 size_t SpdyProxyClientSocket::PopulateUserReadBuffer(char* data, size_t len) { | |
223 return read_buffer_queue_.Dequeue(data, len); | |
224 } | |
225 | |
226 int SpdyProxyClientSocket::Write(IOBuffer* buf, int buf_len, | |
227 const CompletionCallback& callback) { | |
228 DCHECK(write_callback_.is_null()); | |
229 if (next_state_ != STATE_OPEN) | |
230 return ERR_SOCKET_NOT_CONNECTED; | |
231 | |
232 DCHECK(spdy_stream_.get()); | |
233 spdy_stream_->SendData(buf, buf_len, MORE_DATA_TO_SEND); | |
234 net_log_.AddByteTransferEvent(NetLog::TYPE_SOCKET_BYTES_SENT, | |
235 buf_len, buf->data()); | |
236 write_callback_ = callback; | |
237 write_buffer_len_ = buf_len; | |
238 return ERR_IO_PENDING; | |
239 } | |
240 | |
241 int SpdyProxyClientSocket::SetReceiveBufferSize(int32 size) { | |
242 // Since this StreamSocket sits on top of a shared SpdySession, it | |
243 // is not safe for callers to change this underlying socket. | |
244 return ERR_NOT_IMPLEMENTED; | |
245 } | |
246 | |
247 int SpdyProxyClientSocket::SetSendBufferSize(int32 size) { | |
248 // Since this StreamSocket sits on top of a shared SpdySession, it | |
249 // is not safe for callers to change this underlying socket. | |
250 return ERR_NOT_IMPLEMENTED; | |
251 } | |
252 | |
253 int SpdyProxyClientSocket::GetPeerAddress(IPEndPoint* address) const { | |
254 if (!IsConnected()) | |
255 return ERR_SOCKET_NOT_CONNECTED; | |
256 return spdy_stream_->GetPeerAddress(address); | |
257 } | |
258 | |
259 int SpdyProxyClientSocket::GetLocalAddress(IPEndPoint* address) const { | |
260 if (!IsConnected()) | |
261 return ERR_SOCKET_NOT_CONNECTED; | |
262 return spdy_stream_->GetLocalAddress(address); | |
263 } | |
264 | |
265 void SpdyProxyClientSocket::LogBlockedTunnelResponse() const { | |
266 ProxyClientSocket::LogBlockedTunnelResponse( | |
267 response_.headers->response_code(), | |
268 request_.url, | |
269 /* is_https_proxy = */ true); | |
270 } | |
271 | |
272 void SpdyProxyClientSocket::RunCallback(const CompletionCallback& callback, | |
273 int result) const { | |
274 callback.Run(result); | |
275 } | |
276 | |
277 void SpdyProxyClientSocket::OnIOComplete(int result) { | |
278 DCHECK_NE(STATE_DISCONNECTED, next_state_); | |
279 int rv = DoLoop(result); | |
280 if (rv != ERR_IO_PENDING) { | |
281 CompletionCallback c = read_callback_; | |
282 read_callback_.Reset(); | |
283 c.Run(rv); | |
284 } | |
285 } | |
286 | |
287 int SpdyProxyClientSocket::DoLoop(int last_io_result) { | |
288 DCHECK_NE(next_state_, STATE_DISCONNECTED); | |
289 int rv = last_io_result; | |
290 do { | |
291 State state = next_state_; | |
292 next_state_ = STATE_DISCONNECTED; | |
293 switch (state) { | |
294 case STATE_GENERATE_AUTH_TOKEN: | |
295 DCHECK_EQ(OK, rv); | |
296 rv = DoGenerateAuthToken(); | |
297 break; | |
298 case STATE_GENERATE_AUTH_TOKEN_COMPLETE: | |
299 rv = DoGenerateAuthTokenComplete(rv); | |
300 break; | |
301 case STATE_SEND_REQUEST: | |
302 DCHECK_EQ(OK, rv); | |
303 net_log_.BeginEvent(NetLog::TYPE_HTTP_TRANSACTION_TUNNEL_SEND_REQUEST); | |
304 rv = DoSendRequest(); | |
305 break; | |
306 case STATE_SEND_REQUEST_COMPLETE: | |
307 net_log_.EndEventWithNetErrorCode( | |
308 NetLog::TYPE_HTTP_TRANSACTION_TUNNEL_SEND_REQUEST, rv); | |
309 rv = DoSendRequestComplete(rv); | |
310 if (rv >= 0 || rv == ERR_IO_PENDING) { | |
311 // Emit extra event so can use the same events as | |
312 // HttpProxyClientSocket. | |
313 net_log_.BeginEvent( | |
314 NetLog::TYPE_HTTP_TRANSACTION_TUNNEL_READ_HEADERS); | |
315 } | |
316 break; | |
317 case STATE_READ_REPLY_COMPLETE: | |
318 rv = DoReadReplyComplete(rv); | |
319 net_log_.EndEventWithNetErrorCode( | |
320 NetLog::TYPE_HTTP_TRANSACTION_TUNNEL_READ_HEADERS, rv); | |
321 break; | |
322 default: | |
323 NOTREACHED() << "bad state"; | |
324 rv = ERR_UNEXPECTED; | |
325 break; | |
326 } | |
327 } while (rv != ERR_IO_PENDING && next_state_ != STATE_DISCONNECTED && | |
328 next_state_ != STATE_OPEN); | |
329 return rv; | |
330 } | |
331 | |
332 int SpdyProxyClientSocket::DoGenerateAuthToken() { | |
333 next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; | |
334 return auth_->MaybeGenerateAuthToken( | |
335 &request_, | |
336 base::Bind(&SpdyProxyClientSocket::OnIOComplete, | |
337 weak_factory_.GetWeakPtr()), | |
338 net_log_); | |
339 } | |
340 | |
341 int SpdyProxyClientSocket::DoGenerateAuthTokenComplete(int result) { | |
342 DCHECK_NE(ERR_IO_PENDING, result); | |
343 if (result == OK) | |
344 next_state_ = STATE_SEND_REQUEST; | |
345 return result; | |
346 } | |
347 | |
348 int SpdyProxyClientSocket::DoSendRequest() { | |
349 next_state_ = STATE_SEND_REQUEST_COMPLETE; | |
350 | |
351 // Add Proxy-Authentication header if necessary. | |
352 HttpRequestHeaders authorization_headers; | |
353 if (auth_->HaveAuth()) { | |
354 auth_->AddAuthorizationHeader(&authorization_headers); | |
355 } | |
356 | |
357 std::string request_line; | |
358 HttpRequestHeaders request_headers; | |
359 BuildTunnelRequest(request_, authorization_headers, endpoint_, &request_line, | |
360 &request_headers); | |
361 | |
362 net_log_.AddEvent( | |
363 NetLog::TYPE_HTTP_TRANSACTION_SEND_TUNNEL_HEADERS, | |
364 base::Bind(&HttpRequestHeaders::NetLogCallback, | |
365 base::Unretained(&request_headers), | |
366 &request_line)); | |
367 | |
368 request_.extra_headers.MergeFrom(request_headers); | |
369 scoped_ptr<SpdyHeaderBlock> headers(new SpdyHeaderBlock()); | |
370 CreateSpdyHeadersFromHttpRequest(request_, request_headers, | |
371 spdy_stream_->GetProtocolVersion(), true, | |
372 headers.get()); | |
373 // Reset the URL to be the endpoint of the connection | |
374 if (spdy_stream_->GetProtocolVersion() > 2) { | |
375 (*headers)[":path"] = endpoint_.ToString(); | |
376 headers->erase(":scheme"); | |
377 } else { | |
378 (*headers)["url"] = endpoint_.ToString(); | |
379 headers->erase("scheme"); | |
380 } | |
381 | |
382 return spdy_stream_->SendRequestHeaders(headers.Pass(), MORE_DATA_TO_SEND); | |
383 } | |
384 | |
385 int SpdyProxyClientSocket::DoSendRequestComplete(int result) { | |
386 if (result < 0) | |
387 return result; | |
388 | |
389 // Wait for SYN_REPLY frame from the server | |
390 next_state_ = STATE_READ_REPLY_COMPLETE; | |
391 return ERR_IO_PENDING; | |
392 } | |
393 | |
394 int SpdyProxyClientSocket::DoReadReplyComplete(int result) { | |
395 // We enter this method directly from DoSendRequestComplete, since | |
396 // we are notified by a callback when the SYN_REPLY frame arrives | |
397 | |
398 if (result < 0) | |
399 return result; | |
400 | |
401 // Require the "HTTP/1.x" status line for SSL CONNECT. | |
402 if (response_.headers->GetParsedHttpVersion() < HttpVersion(1, 0)) | |
403 return ERR_TUNNEL_CONNECTION_FAILED; | |
404 | |
405 net_log_.AddEvent( | |
406 NetLog::TYPE_HTTP_TRANSACTION_READ_TUNNEL_RESPONSE_HEADERS, | |
407 base::Bind(&HttpResponseHeaders::NetLogCallback, response_.headers)); | |
408 | |
409 switch (response_.headers->response_code()) { | |
410 case 200: // OK | |
411 next_state_ = STATE_OPEN; | |
412 return OK; | |
413 | |
414 case 302: // Found / Moved Temporarily | |
415 // Try to return a sanitized response so we can follow auth redirects. | |
416 // If we can't, fail the tunnel connection. | |
417 if (!SanitizeProxyRedirect(&response_)) { | |
418 LogBlockedTunnelResponse(); | |
419 return ERR_TUNNEL_CONNECTION_FAILED; | |
420 } | |
421 | |
422 redirect_has_load_timing_info_ = | |
423 spdy_stream_->GetLoadTimingInfo(&redirect_load_timing_info_); | |
424 // Note that this triggers a RST_STREAM_CANCEL. | |
425 spdy_stream_->DetachDelegate(); | |
426 next_state_ = STATE_DISCONNECTED; | |
427 return ERR_HTTPS_PROXY_TUNNEL_RESPONSE; | |
428 | |
429 case 407: // Proxy Authentication Required | |
430 next_state_ = STATE_OPEN; | |
431 if (!SanitizeProxyAuth(&response_)) { | |
432 LogBlockedTunnelResponse(); | |
433 return ERR_TUNNEL_CONNECTION_FAILED; | |
434 } | |
435 return HandleProxyAuthChallenge(auth_.get(), &response_, net_log_); | |
436 | |
437 default: | |
438 // Ignore response to avoid letting the proxy impersonate the target | |
439 // server. (See http://crbug.com/137891.) | |
440 LogBlockedTunnelResponse(); | |
441 return ERR_TUNNEL_CONNECTION_FAILED; | |
442 } | |
443 } | |
444 | |
445 // SpdyStream::Delegate methods: | |
446 // Called when SYN frame has been sent. | |
447 // Returns true if no more data to be sent after SYN frame. | |
448 void SpdyProxyClientSocket::OnRequestHeadersSent() { | |
449 DCHECK_EQ(next_state_, STATE_SEND_REQUEST_COMPLETE); | |
450 | |
451 OnIOComplete(OK); | |
452 } | |
453 | |
454 SpdyResponseHeadersStatus SpdyProxyClientSocket::OnResponseHeadersUpdated( | |
455 const SpdyHeaderBlock& response_headers) { | |
456 // If we've already received the reply, existing headers are too late. | |
457 // TODO(mbelshe): figure out a way to make HEADERS frames useful after the | |
458 // initial response. | |
459 if (next_state_ != STATE_READ_REPLY_COMPLETE) | |
460 return RESPONSE_HEADERS_ARE_COMPLETE; | |
461 | |
462 // Save the response | |
463 if (!SpdyHeadersToHttpResponse( | |
464 response_headers, spdy_stream_->GetProtocolVersion(), &response_)) | |
465 return RESPONSE_HEADERS_ARE_INCOMPLETE; | |
466 | |
467 OnIOComplete(OK); | |
468 return RESPONSE_HEADERS_ARE_COMPLETE; | |
469 } | |
470 | |
471 // Called when data is received or on EOF (if |buffer| is NULL). | |
472 void SpdyProxyClientSocket::OnDataReceived(scoped_ptr<SpdyBuffer> buffer) { | |
473 if (buffer) { | |
474 net_log_.AddByteTransferEvent(NetLog::TYPE_SOCKET_BYTES_RECEIVED, | |
475 buffer->GetRemainingSize(), | |
476 buffer->GetRemainingData()); | |
477 read_buffer_queue_.Enqueue(buffer.Pass()); | |
478 } else { | |
479 net_log_.AddByteTransferEvent(NetLog::TYPE_SOCKET_BYTES_RECEIVED, 0, NULL); | |
480 } | |
481 | |
482 if (!read_callback_.is_null()) { | |
483 int rv = PopulateUserReadBuffer(user_buffer_->data(), user_buffer_len_); | |
484 CompletionCallback c = read_callback_; | |
485 read_callback_.Reset(); | |
486 user_buffer_ = NULL; | |
487 user_buffer_len_ = 0; | |
488 c.Run(rv); | |
489 } | |
490 } | |
491 | |
492 void SpdyProxyClientSocket::OnDataSent() { | |
493 DCHECK(!write_callback_.is_null()); | |
494 | |
495 int rv = write_buffer_len_; | |
496 write_buffer_len_ = 0; | |
497 | |
498 // Proxy write callbacks result in deep callback chains. Post to allow the | |
499 // stream's write callback chain to unwind (see crbug.com/355511). | |
500 base::MessageLoop::current()->PostTask( | |
501 FROM_HERE, | |
502 base::Bind(&SpdyProxyClientSocket::RunCallback, | |
503 write_callback_weak_factory_.GetWeakPtr(), | |
504 ResetAndReturn(&write_callback_), | |
505 rv)); | |
506 } | |
507 | |
508 void SpdyProxyClientSocket::OnClose(int status) { | |
509 was_ever_used_ = spdy_stream_->WasEverUsed(); | |
510 spdy_stream_.reset(); | |
511 | |
512 bool connecting = next_state_ != STATE_DISCONNECTED && | |
513 next_state_ < STATE_OPEN; | |
514 if (next_state_ == STATE_OPEN) | |
515 next_state_ = STATE_CLOSED; | |
516 else | |
517 next_state_ = STATE_DISCONNECTED; | |
518 | |
519 base::WeakPtr<SpdyProxyClientSocket> weak_ptr = weak_factory_.GetWeakPtr(); | |
520 CompletionCallback write_callback = write_callback_; | |
521 write_callback_.Reset(); | |
522 write_buffer_len_ = 0; | |
523 | |
524 // If we're in the middle of connecting, we need to make sure | |
525 // we invoke the connect callback. | |
526 if (connecting) { | |
527 DCHECK(!read_callback_.is_null()); | |
528 CompletionCallback read_callback = read_callback_; | |
529 read_callback_.Reset(); | |
530 read_callback.Run(status); | |
531 } else if (!read_callback_.is_null()) { | |
532 // If we have a read_callback_, the we need to make sure we call it back. | |
533 OnDataReceived(scoped_ptr<SpdyBuffer>()); | |
534 } | |
535 // This may have been deleted by read_callback_, so check first. | |
536 if (weak_ptr.get() && !write_callback.is_null()) | |
537 write_callback.Run(ERR_CONNECTION_CLOSED); | |
538 } | |
539 | |
540 } // namespace net | |
OLD | NEW |