| /* |
| * Copyright (C) 2009, 2012 Ericsson AB. All rights reserved. |
| * Copyright (C) 2010 Apple Inc. All rights reserved. |
| * Copyright (C) 2011, Code Aurora Forum. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * 3. Neither the name of Ericsson nor the names of its contributors |
| * may be used to endorse or promote products derived from this |
| * software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "modules/eventsource/EventSource.h" |
| |
| #include <memory> |
| #include "bindings/core/v8/ExceptionState.h" |
| #include "bindings/core/v8/ScriptController.h" |
| #include "bindings/core/v8/serialization/SerializedScriptValue.h" |
| #include "bindings/core/v8/serialization/SerializedScriptValueFactory.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/ExceptionCode.h" |
| #include "core/dom/ExecutionContext.h" |
| #include "core/dom/TaskRunnerHelper.h" |
| #include "core/events/Event.h" |
| #include "core/events/MessageEvent.h" |
| #include "core/frame/LocalDOMWindow.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/UseCounter.h" |
| #include "core/frame/csp/ContentSecurityPolicy.h" |
| #include "core/inspector/ConsoleMessage.h" |
| #include "core/loader/ThreadableLoader.h" |
| #include "core/probe/CoreProbes.h" |
| #include "modules/eventsource/EventSourceInit.h" |
| #include "platform/HTTPNames.h" |
| #include "platform/loader/fetch/ResourceError.h" |
| #include "platform/loader/fetch/ResourceLoaderOptions.h" |
| #include "platform/loader/fetch/ResourceRequest.h" |
| #include "platform/loader/fetch/ResourceResponse.h" |
| #include "platform/weborigin/SecurityOrigin.h" |
| #include "platform/wtf/text/StringBuilder.h" |
| #include "public/platform/WebURLRequest.h" |
| |
| namespace blink { |
| |
| const unsigned long long EventSource::kDefaultReconnectDelay = 3000; |
| |
| inline EventSource::EventSource(ExecutionContext* context, |
| const KURL& url, |
| const EventSourceInit& event_source_init) |
| : ContextLifecycleObserver(context), |
| url_(url), |
| current_url_(url), |
| with_credentials_(event_source_init.withCredentials()), |
| state_(kConnecting), |
| connect_timer_(TaskRunnerHelper::Get(TaskType::kRemoteEvent, context), |
| this, |
| &EventSource::ConnectTimerFired), |
| reconnect_delay_(kDefaultReconnectDelay) {} |
| |
| EventSource* EventSource::Create(ExecutionContext* context, |
| const String& url, |
| const EventSourceInit& event_source_init, |
| ExceptionState& exception_state) { |
| if (context->IsDocument()) |
| UseCounter::Count(ToDocument(context), WebFeature::kEventSourceDocument); |
| else |
| UseCounter::Count(context, WebFeature::kEventSourceWorker); |
| |
| if (url.IsEmpty()) { |
| exception_state.ThrowDOMException( |
| kSyntaxError, "Cannot open an EventSource to an empty URL."); |
| return nullptr; |
| } |
| |
| KURL full_url = context->CompleteURL(url); |
| if (!full_url.IsValid()) { |
| exception_state.ThrowDOMException( |
| kSyntaxError, |
| "Cannot open an EventSource to '" + url + "'. The URL is invalid."); |
| return nullptr; |
| } |
| |
| EventSource* source = new EventSource(context, full_url, event_source_init); |
| |
| source->ScheduleInitialConnect(); |
| return source; |
| } |
| |
| EventSource::~EventSource() { |
| DCHECK_EQ(kClosed, state_); |
| DCHECK(!loader_); |
| } |
| |
| void EventSource::Dispose() { |
| probe::detachClientRequest(GetExecutionContext(), this); |
| } |
| |
| void EventSource::ScheduleInitialConnect() { |
| DCHECK_EQ(kConnecting, state_); |
| DCHECK(!loader_); |
| |
| connect_timer_.StartOneShot(0, BLINK_FROM_HERE); |
| } |
| |
| void EventSource::Connect() { |
| DCHECK_EQ(kConnecting, state_); |
| DCHECK(!loader_); |
| DCHECK(GetExecutionContext()); |
| |
| ExecutionContext& execution_context = *this->GetExecutionContext(); |
| ResourceRequest request(current_url_); |
| request.SetHTTPMethod(HTTPNames::GET); |
| request.SetHTTPHeaderField(HTTPNames::Accept, "text/event-stream"); |
| request.SetHTTPHeaderField(HTTPNames::Cache_Control, "no-cache"); |
| request.SetRequestContext(WebURLRequest::kRequestContextEventSource); |
| request.SetFetchCredentialsMode( |
| with_credentials_ ? WebURLRequest::kFetchCredentialsModeInclude |
| : WebURLRequest::kFetchCredentialsModeSameOrigin); |
| request.SetExternalRequestStateFromRequestorAddressSpace( |
| execution_context.GetSecurityContext().AddressSpace()); |
| if (parser_ && !parser_->LastEventId().IsEmpty()) { |
| // HTTP headers are Latin-1 byte strings, but the Last-Event-ID header is |
| // encoded as UTF-8. |
| // TODO(davidben): This should be captured in the type of |
| // setHTTPHeaderField's arguments. |
| CString last_event_id_utf8 = parser_->LastEventId().Utf8(); |
| request.SetHTTPHeaderField( |
| HTTPNames::Last_Event_ID, |
| AtomicString(reinterpret_cast<const LChar*>(last_event_id_utf8.data()), |
| last_event_id_utf8.length())); |
| } |
| |
| SecurityOrigin* origin = execution_context.GetSecurityOrigin(); |
| |
| ThreadableLoaderOptions options; |
| options.preflight_policy = kPreventPreflight; |
| options.fetch_request_mode = WebURLRequest::kFetchRequestModeCORS; |
| options.content_security_policy_enforcement = |
| ContentSecurityPolicy::ShouldBypassMainWorld(&execution_context) |
| ? kDoNotEnforceContentSecurityPolicy |
| : kEnforceContentSecurityPolicy; |
| |
| ResourceLoaderOptions resource_loader_options; |
| resource_loader_options.data_buffering_policy = kDoNotBufferData; |
| resource_loader_options.security_origin = origin; |
| |
| probe::willSendEventSourceRequest(&execution_context, this); |
| // probe::documentThreadableLoaderStartedLoadingForClient |
| // will be called synchronously. |
| loader_ = ThreadableLoader::Create(execution_context, this, options, |
| resource_loader_options); |
| loader_->Start(request); |
| } |
| |
| void EventSource::NetworkRequestEnded() { |
| probe::didFinishEventSourceRequest(GetExecutionContext(), this); |
| |
| loader_ = nullptr; |
| |
| if (state_ != kClosed) |
| ScheduleReconnect(); |
| } |
| |
| void EventSource::ScheduleReconnect() { |
| state_ = kConnecting; |
| connect_timer_.StartOneShot(reconnect_delay_ / 1000.0, BLINK_FROM_HERE); |
| DispatchEvent(Event::Create(EventTypeNames::error)); |
| } |
| |
| void EventSource::ConnectTimerFired(TimerBase*) { |
| Connect(); |
| } |
| |
| String EventSource::url() const { |
| return url_.GetString(); |
| } |
| |
| bool EventSource::withCredentials() const { |
| return with_credentials_; |
| } |
| |
| EventSource::State EventSource::readyState() const { |
| return state_; |
| } |
| |
| void EventSource::close() { |
| if (state_ == kClosed) { |
| DCHECK(!loader_); |
| return; |
| } |
| if (parser_) |
| parser_->Stop(); |
| |
| // Stop trying to reconnect if EventSource was explicitly closed or if |
| // contextDestroyed() was called. |
| if (connect_timer_.IsActive()) { |
| connect_timer_.Stop(); |
| } |
| |
| if (loader_) { |
| loader_->Cancel(); |
| loader_ = nullptr; |
| } |
| |
| state_ = kClosed; |
| } |
| |
| const AtomicString& EventSource::InterfaceName() const { |
| return EventTargetNames::EventSource; |
| } |
| |
| ExecutionContext* EventSource::GetExecutionContext() const { |
| return ContextLifecycleObserver::GetExecutionContext(); |
| } |
| |
| void EventSource::DidReceiveResponse( |
| unsigned long, |
| const ResourceResponse& response, |
| std::unique_ptr<WebDataConsumerHandle> handle) { |
| DCHECK(!handle); |
| DCHECK_EQ(kConnecting, state_); |
| DCHECK(loader_); |
| |
| current_url_ = response.Url(); |
| event_stream_origin_ = SecurityOrigin::Create(response.Url())->ToString(); |
| int status_code = response.HttpStatusCode(); |
| bool mime_type_is_valid = response.MimeType() == "text/event-stream"; |
| bool response_is_valid = status_code == 200 && mime_type_is_valid; |
| if (response_is_valid) { |
| const String& charset = response.TextEncodingName(); |
| // If we have a charset, the only allowed value is UTF-8 (case-insensitive). |
| response_is_valid = |
| charset.IsEmpty() || DeprecatedEqualIgnoringCase(charset, "UTF-8"); |
| if (!response_is_valid) { |
| StringBuilder message; |
| message.Append("EventSource's response has a charset (\""); |
| message.Append(charset); |
| message.Append("\") that is not UTF-8. Aborting the connection."); |
| // FIXME: We are missing the source line. |
| GetExecutionContext()->AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kErrorMessageLevel, message.ToString())); |
| } |
| } else { |
| // To keep the signal-to-noise ratio low, we only log 200-response with an |
| // invalid MIME type. |
| if (status_code == 200 && !mime_type_is_valid) { |
| StringBuilder message; |
| message.Append("EventSource's response has a MIME type (\""); |
| message.Append(response.MimeType()); |
| message.Append( |
| "\") that is not \"text/event-stream\". Aborting the connection."); |
| // FIXME: We are missing the source line. |
| GetExecutionContext()->AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kErrorMessageLevel, message.ToString())); |
| } |
| } |
| |
| if (response_is_valid) { |
| state_ = kOpen; |
| AtomicString last_event_id; |
| if (parser_) { |
| // The new parser takes over the event ID. |
| last_event_id = parser_->LastEventId(); |
| } |
| parser_ = new EventSourceParser(last_event_id, this); |
| DispatchEvent(Event::Create(EventTypeNames::open)); |
| } else { |
| loader_->Cancel(); |
| DispatchEvent(Event::Create(EventTypeNames::error)); |
| } |
| } |
| |
| void EventSource::DidReceiveData(const char* data, unsigned length) { |
| DCHECK_EQ(kOpen, state_); |
| DCHECK(loader_); |
| DCHECK(parser_); |
| |
| parser_->AddBytes(data, length); |
| } |
| |
| void EventSource::DidFinishLoading(unsigned long, double) { |
| DCHECK_EQ(kOpen, state_); |
| DCHECK(loader_); |
| |
| NetworkRequestEnded(); |
| } |
| |
| void EventSource::DidFail(const ResourceError& error) { |
| DCHECK_NE(kClosed, state_); |
| DCHECK(loader_); |
| |
| if (error.IsAccessCheck()) { |
| DidFailAccessControlCheck(error); |
| return; |
| } |
| |
| if (error.IsCancellation()) |
| state_ = kClosed; |
| NetworkRequestEnded(); |
| } |
| |
| void EventSource::DidFailAccessControlCheck(const ResourceError& error) { |
| DCHECK(loader_); |
| |
| String message = "EventSource cannot load " + error.FailingURL() + ". " + |
| error.LocalizedDescription(); |
| GetExecutionContext()->AddConsoleMessage( |
| ConsoleMessage::Create(kJSMessageSource, kErrorMessageLevel, message)); |
| |
| AbortConnectionAttempt(); |
| } |
| |
| void EventSource::DidFailRedirectCheck() { |
| DCHECK(loader_); |
| |
| AbortConnectionAttempt(); |
| } |
| |
| void EventSource::OnMessageEvent(const AtomicString& event_type, |
| const String& data, |
| const AtomicString& last_event_id) { |
| MessageEvent* e = MessageEvent::Create(); |
| e->initMessageEvent(event_type, false, false, data, event_stream_origin_, |
| last_event_id, 0, nullptr); |
| |
| probe::willDispatchEventSourceEvent(GetExecutionContext(), this, event_type, |
| last_event_id, data); |
| DispatchEvent(e); |
| } |
| |
| void EventSource::OnReconnectionTimeSet(unsigned long long reconnection_time) { |
| reconnect_delay_ = reconnection_time; |
| } |
| |
| void EventSource::AbortConnectionAttempt() { |
| DCHECK_EQ(kConnecting, state_); |
| |
| loader_ = nullptr; |
| state_ = kClosed; |
| NetworkRequestEnded(); |
| |
| DispatchEvent(Event::Create(EventTypeNames::error)); |
| } |
| |
| void EventSource::ContextDestroyed(ExecutionContext*) { |
| probe::detachClientRequest(GetExecutionContext(), this); |
| close(); |
| } |
| |
| bool EventSource::HasPendingActivity() const { |
| return state_ != kClosed; |
| } |
| |
| DEFINE_TRACE(EventSource) { |
| visitor->Trace(parser_); |
| visitor->Trace(loader_); |
| EventTargetWithInlineData::Trace(visitor); |
| ContextLifecycleObserver::Trace(visitor); |
| EventSourceParser::Client::Trace(visitor); |
| } |
| |
| } // namespace blink |