[Resource Timing] Gather timing information with reliable responseEnd time
[WebKit-https.git] / Source / WebCore / page / EventSource.cpp
index 3f0ae37..2eae8cf 100644 (file)
@@ -1,7 +1,6 @@
 /*
- * Copyright (C) 2009 Ericsson AB
- * All rights reserved.
- * Copyright (C) 2010 Apple Inc. All rights reserved.
+ * Copyright (C) 2009, 2012 Ericsson AB. All rights reserved.
+ * Copyright (C) 2010, 2016 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
  */
 
 #include "config.h"
-
-#if ENABLE(EVENTSOURCE)
-
 #include "EventSource.h"
 
-#include "MemoryCache.h"
-#include "DOMWindow.h"
-#include "Event.h"
-#include "EventException.h"
+#include "ContentSecurityPolicy.h"
+#include "EventNames.h"
 #include "ExceptionCode.h"
-#include "PlatformString.h"
 #include "MessageEvent.h"
 #include "ResourceError.h"
 #include "ResourceRequest.h"
 #include "ResourceResponse.h"
-#include "ScriptCallStack.h"
 #include "ScriptExecutionContext.h"
 #include "SecurityOrigin.h"
-#include "SerializedScriptValue.h"
 #include "TextResourceDecoder.h"
 #include "ThreadableLoader.h"
 
 namespace WebCore {
 
-const unsigned long long EventSource::defaultReconnectDelay = 3000;
+const uint64_t EventSource::defaultReconnectDelay = 3000;
 
-inline EventSource::EventSource(const KURL& url, ScriptExecutionContext* context)
-    : ActiveDOMObject(context, this)
+inline EventSource::EventSource(ScriptExecutionContext& context, const URL& url, const Init& eventSourceInit)
+    : ActiveDOMObject(&context)
     , m_url(url)
-    , m_state(CONNECTING)
-    , m_decoder(TextResourceDecoder::create("text/plain", "UTF-8"))
-    , m_reconnectTimer(this, &EventSource::reconnectTimerFired)
-    , m_discardTrailingNewline(false)
-    , m_failSilently(false)
-    , m_requestInFlight(false)
-    , m_reconnectDelay(defaultReconnectDelay)
-    , m_origin(context->securityOrigin()->toString())
+    , m_withCredentials(eventSourceInit.withCredentials)
+    , m_decoder(TextResourceDecoder::create(ASCIILiteral("text/plain"), "UTF-8"))
+    , m_connectTimer(*this, &EventSource::connect)
 {
 }
 
-PassRefPtr<EventSource> EventSource::create(const String& url, ScriptExecutionContext* context, ExceptionCode& ec)
+ExceptionOr<Ref<EventSource>> EventSource::create(ScriptExecutionContext& context, const String& url, const Init& eventSourceInit)
 {
-    if (url.isEmpty()) {
-        ec = SYNTAX_ERR;
-        return 0;
-    }
+    if (url.isEmpty())
+        return Exception { SYNTAX_ERR };
 
-    KURL fullURL = context->completeURL(url);
-    if (!fullURL.isValid()) {
-        ec = SYNTAX_ERR;
-        return 0;
-    }
+    URL fullURL = context.completeURL(url);
+    if (!fullURL.isValid())
+        return Exception { SYNTAX_ERR };
 
-    // FIXME: Should support at least some cross-origin requests.
-    if (!context->securityOrigin()->canRequest(fullURL)) {
-        ec = SECURITY_ERR;
-        return 0;
+    // FIXME: Convert this to check the isolated world's Content Security Policy once webkit.org/b/104520 is resolved.
+    if (!context.shouldBypassMainWorldContentSecurityPolicy() && !context.contentSecurityPolicy()->allowConnectToSource(fullURL)) {
+        // FIXME: Should this be throwing an exception?
+        return Exception { SECURITY_ERR };
     }
 
-    RefPtr<EventSource> source = adoptRef(new EventSource(fullURL, context));
-
-    source->setPendingActivity(source.get());
-    source->connect();
-
-    return source.release();
+    auto source = adoptRef(*new EventSource(context, fullURL, eventSourceInit));
+    source->setPendingActivity(source.ptr());
+    source->scheduleInitialConnect();
+    source->suspendIfNeeded();
+    return WTFMove(source);
 }
 
 EventSource::~EventSource()
 {
+    ASSERT(m_state == CLOSED);
+    ASSERT(!m_requestInFlight);
 }
 
 void EventSource::connect()
 {
-    ResourceRequest request(m_url);
+    ASSERT(m_state == CONNECTING);
+    ASSERT(!m_requestInFlight);
+
+    ResourceRequest request { m_url };
     request.setHTTPMethod("GET");
-    request.setHTTPHeaderField("Accept", "text/event-stream");
-    request.setHTTPHeaderField("Cache-Control", "no-cache");
+    request.setHTTPHeaderField(HTTPHeaderName::Accept, "text/event-stream");
+    request.setHTTPHeaderField(HTTPHeaderName::CacheControl, "no-cache");
     if (!m_lastEventId.isEmpty())
-        request.setHTTPHeaderField("Last-Event-ID", m_lastEventId);
+        request.setHTTPHeaderField(HTTPHeaderName::LastEventID, m_lastEventId);
 
     ThreadableLoaderOptions options;
-    options.sendLoadCallbacks = true;
-    options.sniffContent = false;
-    options.allowCredentials = true;
-
-    m_loader = ThreadableLoader::create(scriptExecutionContext(), this, request, options);
-
-    m_requestInFlight = true;
+    options.sendLoadCallbacks = SendCallbacks;
+    options.credentials = m_withCredentials ? FetchOptions::Credentials::Include : FetchOptions::Credentials::SameOrigin;
+    options.preflightPolicy = PreventPreflight;
+    options.mode = FetchOptions::Mode::Cors;
+    options.cache = FetchOptions::Cache::NoStore;
+    options.dataBufferingPolicy = DoNotBufferData;
+    options.contentSecurityPolicyEnforcement = scriptExecutionContext()->shouldBypassMainWorldContentSecurityPolicy() ? ContentSecurityPolicyEnforcement::DoNotEnforce : ContentSecurityPolicyEnforcement::EnforceConnectSrcDirective;
+
+    ASSERT(scriptExecutionContext());
+    m_loader = ThreadableLoader::create(*scriptExecutionContext(), *this, WTFMove(request), options);
+
+    // FIXME: Can we just use m_loader for this, null it out when it's no longer in flight, and eliminate the m_requestInFlight member?
+    if (m_loader)
+        m_requestInFlight = true;
 }
 
-void EventSource::endRequest()
+void EventSource::networkRequestEnded()
 {
-    if (!m_requestInFlight)
-        return;
+    ASSERT(m_requestInFlight);
 
     m_requestInFlight = false;
 
-    if (!m_failSilently)
-        dispatchEvent(Event::create(eventNames().errorEvent, false, false));
-
     if (m_state != CLOSED)
         scheduleReconnect();
     else
         unsetPendingActivity(this);
 }
 
-void EventSource::scheduleReconnect()
+void EventSource::scheduleInitialConnect()
 {
-    m_state = CONNECTING;
-    m_reconnectTimer.startOneShot(m_reconnectDelay / 1000);
-}
+    ASSERT(m_state == CONNECTING);
+    ASSERT(!m_requestInFlight);
 
-void EventSource::reconnectTimerFired(Timer<EventSource>*)
-{
-    connect();
-}
-
-String EventSource::url() const
-{
-    return m_url.string();
+    m_connectTimer.startOneShot(0);
 }
 
-EventSource::State EventSource::readyState() const
+void EventSource::scheduleReconnect()
 {
-    return m_state;
+    m_state = CONNECTING;
+    m_connectTimer.startOneShot(m_reconnectDelay / 1000.0);
+    dispatchEvent(Event::create(eventNames().errorEvent, false, false));
 }
 
 void EventSource::close()
 {
-    if (m_state == CLOSED)
+    if (m_state == CLOSED) {
+        ASSERT(!m_requestInFlight);
         return;
-
-    if (m_reconnectTimer.isActive()) {
-        m_reconnectTimer.stop();
-        unsetPendingActivity(this);
     }
 
-    m_state = CLOSED;
-    m_failSilently = true;
+    // Stop trying to connect/reconnect if EventSource was explicitly closed or if ActiveDOMObject::stop() was called.
+    if (m_connectTimer.isActive())
+        m_connectTimer.stop();
 
     if (m_requestInFlight)
         m_loader->cancel();
+    else {
+        m_state = CLOSED;
+        unsetPendingActivity(this);
+    }
 }
 
-ScriptExecutionContext* EventSource::scriptExecutionContext() const
+bool EventSource::responseIsValid(const ResourceResponse& response) const
 {
-    return ActiveDOMObject::scriptExecutionContext();
+    // Logs to the console as a side effect.
+
+    // To keep the signal-to-noise ratio low, we don't log anything if the status code is not 200.
+    if (response.httpStatusCode() != 200)
+        return false;
+
+    if (!equalLettersIgnoringASCIICase(response.mimeType(), "text/event-stream")) {
+        auto message = makeString("EventSource's response has a MIME type (\"", response.mimeType(), "\") that is not \"text/event-stream\". Aborting the connection.");
+        // FIXME: Console message would be better with a source code location; where would we get that?
+        scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Error, WTFMove(message));
+        return false;
+    }
+
+    // If we have a charset, the only allowed value is UTF-8 (case-insensitive).
+    auto& charset = response.textEncodingName();
+    if (!charset.isEmpty() && !equalLettersIgnoringASCIICase(charset, "utf-8")) {
+        auto message = makeString("EventSource's response has a charset (\"", charset, "\") that is not UTF-8. Aborting the connection.");
+        // FIXME: Console message would be better with a source code location; where would we get that?
+        scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Error, WTFMove(message));
+        return false;
+    }
+
+    return true;
 }
 
 void EventSource::didReceiveResponse(unsigned long, const ResourceResponse& response)
 {
-    int statusCode = response.httpStatusCode();
-    bool mimeTypeIsValid = response.mimeType() == "text/event-stream";
-    bool responseIsValid = statusCode == 200 && mimeTypeIsValid;
-    if (responseIsValid) {
-        const String& charset = response.textEncodingName();
-        // If we have a charset, the only allowed value is UTF-8 (case-insensitive). This should match
-        // the updated EventSource standard.
-        responseIsValid = charset.isEmpty() || equalIgnoringCase(charset, "UTF-8");
-        if (!responseIsValid) {
-            String message = "EventSource's response has a charset (\"";
-            message += charset;
-            message += "\") that is not UTF-8. Aborting the connection.";
-            // FIXME: We are missing the source line.
-            scriptExecutionContext()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, message, 1, String(), 0);
-        }
-    } else {
-        // To keep the signal-to-noise ratio low, we only log 200-response with an invalid MIME type.
-        if (statusCode == 200 && !mimeTypeIsValid) {
-            String message = "EventSource's response has a MIME type (\"";
-            message += response.mimeType();
-            message += "\") that is not \"text/event-stream\". Aborting the connection.";
-            // FIXME: We are missing the source line.
-            scriptExecutionContext()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, message, 1, String(), 0);
-        }
-    }
+    ASSERT(m_state == CONNECTING);
+    ASSERT(m_requestInFlight);
 
-    if (responseIsValid) {
-        m_state = OPEN;
-        dispatchEvent(Event::create(eventNames().openEvent, false, false));
-    } else {
-        if (statusCode <= 200 || statusCode > 299)
-            m_state = CLOSED;
+    if (!responseIsValid(response)) {
         m_loader->cancel();
+        dispatchEvent(Event::create(eventNames().errorEvent, false, false));
+        return;
     }
+
+    m_eventStreamOrigin = SecurityOrigin::create(response.url())->toString();
+    m_state = OPEN;
+    dispatchEvent(Event::create(eventNames().openEvent, false, false));
 }
 
 void EventSource::didReceiveData(const char* data, int length)
 {
-    append(m_receiveBuf, m_decoder->decode(data, length));
+    ASSERT(m_state == OPEN);
+    ASSERT(m_requestInFlight);
+
+    append(m_receiveBuffer, m_decoder->decode(data, length));
     parseEventStream();
 }
 
-void EventSource::didFinishLoading(unsigned long, double)
+void EventSource::didFinishLoading(unsigned long)
 {
-    if (m_receiveBuf.size() > 0 || m_data.size() > 0) {
-        append(m_receiveBuf, "\n\n");
-        parseEventStream();
-    }
-    m_state = CONNECTING;
-    endRequest();
+    ASSERT(m_state == OPEN);
+    ASSERT(m_requestInFlight);
+
+    append(m_receiveBuffer, m_decoder->flush());
+    parseEventStream();
+
+    // Discard everything that has not been dispatched by now.
+    // FIXME: Why does this need to be done?
+    // If this is important, why isn't it important to clear other data members: m_decoder, m_lastEventId, m_loader?
+    m_receiveBuffer.clear();
+    m_data.clear();
+    m_eventName = { };
+    m_currentlyParsedEventId = { };
+
+    networkRequestEnded();
 }
 
 void EventSource::didFail(const ResourceError& error)
 {
-    int canceled = error.isCancellation();
-    if (((m_state == CONNECTING) && !canceled) || ((m_state == OPEN) && canceled))
+    ASSERT(m_state != CLOSED);
+
+    if (error.isAccessControl()) {
+        String message = makeString("EventSource cannot load ", error.failingURL().string(), ". ", error.localizedDescription());
+        scriptExecutionContext()->addConsoleMessage(MessageSource::JS, MessageLevel::Error, message);
+
+        abortConnectionAttempt();
+        return;
+    }
+
+    ASSERT(m_requestInFlight);
+
+    if (error.isCancellation())
         m_state = CLOSED;
-    endRequest();
+
+    // FIXME: Why don't we need to clear data members here as in didFinishLoading?
+
+    networkRequestEnded();
 }
 
-void EventSource::didFailRedirectCheck()
+void EventSource::abortConnectionAttempt()
 {
-    m_state = CLOSED;
-    m_loader->cancel();
+    ASSERT(m_state == CONNECTING);
+
+    if (m_requestInFlight)
+        m_loader->cancel();
+    else {
+        m_state = CLOSED;
+        unsetPendingActivity(this);
+    }
+
+    ASSERT(m_state == CLOSED);
+    dispatchEvent(Event::create(eventNames().errorEvent, false, false));
 }
 
 void EventSource::parseEventStream()
 {
-    unsigned int bufPos = 0;
-    unsigned int bufSize = m_receiveBuf.size();
-    while (bufPos < bufSize) {
+    unsigned position = 0;
+    unsigned size = m_receiveBuffer.size();
+    while (position < size) {
         if (m_discardTrailingNewline) {
-            if (m_receiveBuf[bufPos] == '\n')
-                bufPos++;
+            if (m_receiveBuffer[position] == '\n')
+                ++position;
             m_discardTrailingNewline = false;
         }
 
-        int lineLength = -1;
-        int fieldLength = -1;
-        for (unsigned int i = bufPos; lineLength < 0 && i < bufSize; i++) {
-            switch (m_receiveBuf[i]) {
+        std::optional<unsigned> lineLength;
+        std::optional<unsigned> fieldLength;
+        for (unsigned i = position; !lineLength && i < size; ++i) {
+            switch (m_receiveBuffer[i]) {
             case ':':
-                if (fieldLength < 0)
-                    fieldLength = i - bufPos;
+                if (!fieldLength)
+                    fieldLength = i - position;
                 break;
             case '\r':
                 m_discardTrailingNewline = true;
+                FALLTHROUGH;
             case '\n':
-                lineLength = i - bufPos;
+                lineLength = i - position;
                 break;
             }
         }
 
-        if (lineLength < 0)
+        if (!lineLength)
             break;
 
-        parseEventStreamLine(bufPos, fieldLength, lineLength);
-        bufPos += lineLength + 1;
+        parseEventStreamLine(position, fieldLength, lineLength.value());
+        position += lineLength.value() + 1;
+
+        // EventSource.close() might've been called by one of the message event handlers.
+        // Per spec, no further messages should be fired after that.
+        if (m_state == CLOSED)
+            break;
     }
 
-    if (bufPos == bufSize)
-        m_receiveBuf.clear();
-    else if (bufPos)
-        m_receiveBuf.remove(0, bufPos);
+    // FIXME: The following operation makes it clear that m_receiveBuffer should be some other type,
+    // perhaps a Deque or a circular buffer of some sort.
+    if (position == size)
+        m_receiveBuffer.clear();
+    else if (position)
+        m_receiveBuffer.remove(0, position);
 }
 
-void EventSource::parseEventStreamLine(unsigned int bufPos, int fieldLength, int lineLength)
+void EventSource::parseEventStreamLine(unsigned position, std::optional<unsigned> fieldLength, unsigned lineLength)
 {
     if (!lineLength) {
-        if (!m_data.isEmpty()) {
-            m_data.removeLast();
-            dispatchEvent(createMessageEvent());
-        }
-        if (!m_eventName.isEmpty())
-            m_eventName = "";
-    } else if (fieldLength) {
-        bool noValue = fieldLength < 0;
-
-        String field(&m_receiveBuf[bufPos], noValue ? lineLength : fieldLength);
-        int step;
-        if (noValue)
-            step = lineLength;
-        else if (m_receiveBuf[bufPos + fieldLength + 1] != ' ')
-            step = fieldLength + 1;
-        else
-            step = fieldLength + 2;
-        bufPos += step;
-        int valueLength = lineLength - step;
-
-        if (field == "data") {
-            if (valueLength)
-                m_data.append(&m_receiveBuf[bufPos], valueLength);
-            m_data.append('\n');
-        } else if (field == "event")
-            m_eventName = valueLength ? String(&m_receiveBuf[bufPos], valueLength) : "";
-        else if (field == "id")
-            m_lastEventId = valueLength ? String(&m_receiveBuf[bufPos], valueLength) : "";
-        else if (field == "retry") {
-            if (!valueLength)
-                m_reconnectDelay = defaultReconnectDelay;
-            else {
-                String value(&m_receiveBuf[bufPos], valueLength);
-                bool ok;
-                unsigned long long retry = value.toUInt64(&ok);
-                if (ok)
-                    m_reconnectDelay = retry;
-            }
+        if (!m_data.isEmpty())
+            dispatchMessageEvent();
+        m_eventName = { };
+        return;
+    }
+
+    if (fieldLength && !fieldLength.value())
+        return;
+
+    StringView field { &m_receiveBuffer[position], fieldLength ? fieldLength.value() : lineLength };
+
+    unsigned step;
+    if (!fieldLength)
+        step = lineLength;
+    else if (m_receiveBuffer[position + fieldLength.value() + 1] != ' ')
+        step = fieldLength.value() + 1;
+    else
+        step = fieldLength.value() + 2;
+    position += step;
+    unsigned valueLength = lineLength - step;
+
+    if (field == "data") {
+        m_data.append(&m_receiveBuffer[position], valueLength);
+        m_data.append('\n');
+    } else if (field == "event")
+        m_eventName = { &m_receiveBuffer[position], valueLength };
+    else if (field == "id")
+        m_currentlyParsedEventId = { &m_receiveBuffer[position], valueLength };
+    else if (field == "retry") {
+        if (!valueLength)
+            m_reconnectDelay = defaultReconnectDelay;
+        else {
+            // FIXME: Do we really want to ignore trailing garbage here? Should we be using the strict version instead?
+            // FIXME: If we can't parse the value, should we leave m_reconnectDelay alone or set it to defaultReconnectDelay?
+            bool ok;
+            auto reconnectDelay = charactersToUInt64(&m_receiveBuffer[position], valueLength, &ok);
+            if (ok)
+                m_reconnectDelay = reconnectDelay;
         }
     }
 }
@@ -339,23 +367,31 @@ void EventSource::stop()
     close();
 }
 
-PassRefPtr<MessageEvent> EventSource::createMessageEvent()
+const char* EventSource::activeDOMObjectName() const
 {
-    RefPtr<MessageEvent> event = MessageEvent::create();
-    event->initMessageEvent(m_eventName.isEmpty() ? eventNames().messageEvent : AtomicString(m_eventName), false, false, SerializedScriptValue::create(String::adopt(m_data)), m_origin, m_lastEventId, 0, 0);
-    return event.release();
+    return "EventSource";
 }
 
-EventTargetData* EventSource::eventTargetData()
+bool EventSource::canSuspendForDocumentSuspension() const
 {
-    return &m_eventTargetData;
+    // FIXME: We should return true here when we can because this object is not actually currently active.
+    return false;
 }
 
-EventTargetData* EventSource::ensureEventTargetData()
+void EventSource::dispatchMessageEvent()
 {
-    return &m_eventTargetData;
+    if (!m_currentlyParsedEventId.isNull())
+        m_lastEventId = WTFMove(m_currentlyParsedEventId);
+
+    auto& name = m_eventName.isEmpty() ? eventNames().messageEvent : m_eventName;
+
+    // Omit the trailing "\n" character.
+    ASSERT(!m_data.isEmpty());
+    unsigned size = m_data.size() - 1;
+    auto data = SerializedScriptValue::create({ m_data.data(), size });
+    m_data = { };
+
+    dispatchEvent(MessageEvent::create(name, WTFMove(data), m_eventStreamOrigin, m_lastEventId));
 }
 
 } // namespace WebCore
-
-#endif // ENABLE(EVENTSOURCE)