Home | History | Annotate | Download | only in page
      1 /*
      2  * Copyright (C) 2009, 2012 Ericsson AB. All rights reserved.
      3  * Copyright (C) 2010 Apple Inc. All rights reserved.
      4  * Copyright (C) 2011, Code Aurora Forum. All rights reserved.
      5  *
      6  * Redistribution and use in source and binary forms, with or without
      7  * modification, are permitted provided that the following conditions
      8  * are met:
      9  *
     10  * 1. Redistributions of source code must retain the above copyright
     11  *    notice, this list of conditions and the following disclaimer.
     12  * 2. Redistributions in binary form must reproduce the above copyright
     13  *    notice, this list of conditions and the following disclaimer
     14  *    in the documentation and/or other materials provided with the
     15  *    distribution.
     16  * 3. Neither the name of Ericsson nor the names of its contributors
     17  *    may be used to endorse or promote products derived from this
     18  *    software without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31  */
     32 
     33 #include "config.h"
     34 #include "core/page/EventSource.h"
     35 
     36 #include "bindings/v8/Dictionary.h"
     37 #include "bindings/v8/ExceptionState.h"
     38 #include "bindings/v8/ScriptController.h"
     39 #include "bindings/v8/SerializedScriptValue.h"
     40 #include "core/dom/Document.h"
     41 #include "core/dom/Event.h"
     42 #include "core/dom/ExceptionCode.h"
     43 #include "core/dom/MessageEvent.h"
     44 #include "core/dom/ScriptExecutionContext.h"
     45 #include "core/loader/TextResourceDecoder.h"
     46 #include "core/loader/ThreadableLoader.h"
     47 #include "core/page/ContentSecurityPolicy.h"
     48 #include "core/page/DOMWindow.h"
     49 #include "core/page/Frame.h"
     50 #include "core/platform/network/ResourceError.h"
     51 #include "core/platform/network/ResourceRequest.h"
     52 #include "core/platform/network/ResourceResponse.h"
     53 #include "weborigin/SecurityOrigin.h"
     54 #include "wtf/text/StringBuilder.h"
     55 
     56 namespace WebCore {
     57 
     58 const unsigned long long EventSource::defaultReconnectDelay = 3000;
     59 
     60 inline EventSource::EventSource(ScriptExecutionContext* context, const KURL& url, const Dictionary& eventSourceInit)
     61     : ActiveDOMObject(context)
     62     , m_url(url)
     63     , m_withCredentials(false)
     64     , m_state(CONNECTING)
     65     , m_decoder(TextResourceDecoder::create("text/plain", "UTF-8"))
     66     , m_reconnectTimer(this, &EventSource::reconnectTimerFired)
     67     , m_discardTrailingNewline(false)
     68     , m_requestInFlight(false)
     69     , m_reconnectDelay(defaultReconnectDelay)
     70 {
     71     ScriptWrappable::init(this);
     72     eventSourceInit.get("withCredentials", m_withCredentials);
     73 }
     74 
     75 PassRefPtr<EventSource> EventSource::create(ScriptExecutionContext* context, const String& url, const Dictionary& eventSourceInit, ExceptionState& es)
     76 {
     77     if (url.isEmpty()) {
     78         es.throwDOMException(SyntaxError, "Cannot open an EventSource to an empty URL.");
     79         return 0;
     80     }
     81 
     82     KURL fullURL = context->completeURL(url);
     83     if (!fullURL.isValid()) {
     84         es.throwDOMException(SyntaxError, "Cannot open an EventSource to '" + url + "'. The URL is invalid.");
     85         return 0;
     86     }
     87 
     88     // FIXME: Convert this to check the isolated world's Content Security Policy once webkit.org/b/104520 is solved.
     89     bool shouldBypassMainWorldContentSecurityPolicy = false;
     90     if (context->isDocument()) {
     91         Document* document = toDocument(context);
     92         shouldBypassMainWorldContentSecurityPolicy = document->frame()->script()->shouldBypassMainWorldContentSecurityPolicy();
     93     }
     94     if (!shouldBypassMainWorldContentSecurityPolicy && !context->contentSecurityPolicy()->allowConnectToSource(fullURL)) {
     95         es.throwDOMException(SecurityError, "Refused to connect to '" + fullURL.elidedString() + "' because it violates the document's Content Security Policy.");
     96         return 0;
     97     }
     98 
     99     RefPtr<EventSource> source = adoptRef(new EventSource(context, fullURL, eventSourceInit));
    100 
    101     source->setPendingActivity(source.get());
    102     source->connect();
    103     source->suspendIfNeeded();
    104 
    105     return source.release();
    106 }
    107 
    108 EventSource::~EventSource()
    109 {
    110     ASSERT(m_state == CLOSED);
    111     ASSERT(!m_requestInFlight);
    112 }
    113 
    114 void EventSource::connect()
    115 {
    116     ASSERT(m_state == CONNECTING);
    117     ASSERT(!m_requestInFlight);
    118 
    119     ResourceRequest request(m_url);
    120     request.setHTTPMethod("GET");
    121     request.setHTTPHeaderField("Accept", "text/event-stream");
    122     request.setHTTPHeaderField("Cache-Control", "no-cache");
    123     if (!m_lastEventId.isEmpty())
    124         request.setHTTPHeaderField("Last-Event-ID", m_lastEventId);
    125 
    126     SecurityOrigin* origin = scriptExecutionContext()->securityOrigin();
    127 
    128     ThreadableLoaderOptions options;
    129     options.sendLoadCallbacks = SendCallbacks;
    130     options.sniffContent = DoNotSniffContent;
    131     options.allowCredentials = (origin->canRequest(m_url) || m_withCredentials) ? AllowStoredCredentials : DoNotAllowStoredCredentials;
    132     options.credentialsRequested = m_withCredentials ? ClientRequestedCredentials : ClientDidNotRequestCredentials;
    133     options.preflightPolicy = PreventPreflight;
    134     options.crossOriginRequestPolicy = UseAccessControl;
    135     options.dataBufferingPolicy = DoNotBufferData;
    136     options.securityOrigin = origin;
    137     options.contentSecurityPolicyEnforcement = ContentSecurityPolicy::shouldBypassMainWorld(scriptExecutionContext()) ? DoNotEnforceContentSecurityPolicy : EnforceConnectSrcDirective;
    138 
    139     m_loader = ThreadableLoader::create(scriptExecutionContext(), this, request, options);
    140 
    141     if (m_loader)
    142         m_requestInFlight = true;
    143 }
    144 
    145 void EventSource::networkRequestEnded()
    146 {
    147     if (!m_requestInFlight)
    148         return;
    149 
    150     m_requestInFlight = false;
    151 
    152     if (m_state != CLOSED)
    153         scheduleReconnect();
    154     else
    155         unsetPendingActivity(this);
    156 }
    157 
    158 void EventSource::scheduleReconnect()
    159 {
    160     m_state = CONNECTING;
    161     m_reconnectTimer.startOneShot(m_reconnectDelay / 1000.0);
    162     dispatchEvent(Event::create(eventNames().errorEvent, false, false));
    163 }
    164 
    165 void EventSource::reconnectTimerFired(Timer<EventSource>*)
    166 {
    167     connect();
    168 }
    169 
    170 String EventSource::url() const
    171 {
    172     return m_url.string();
    173 }
    174 
    175 bool EventSource::withCredentials() const
    176 {
    177     return m_withCredentials;
    178 }
    179 
    180 EventSource::State EventSource::readyState() const
    181 {
    182     return m_state;
    183 }
    184 
    185 void EventSource::close()
    186 {
    187     if (m_state == CLOSED) {
    188         ASSERT(!m_requestInFlight);
    189         return;
    190     }
    191 
    192     // Stop trying to reconnect if EventSource was explicitly closed or if ActiveDOMObject::stop() was called.
    193     if (m_reconnectTimer.isActive()) {
    194         m_reconnectTimer.stop();
    195         unsetPendingActivity(this);
    196     }
    197 
    198     if (m_requestInFlight)
    199         m_loader->cancel();
    200 
    201     m_state = CLOSED;
    202 }
    203 
    204 const AtomicString& EventSource::interfaceName() const
    205 {
    206     return eventNames().interfaceForEventSource;
    207 }
    208 
    209 ScriptExecutionContext* EventSource::scriptExecutionContext() const
    210 {
    211     return ActiveDOMObject::scriptExecutionContext();
    212 }
    213 
    214 void EventSource::didReceiveResponse(unsigned long, const ResourceResponse& response)
    215 {
    216     ASSERT(m_state == CONNECTING);
    217     ASSERT(m_requestInFlight);
    218 
    219     m_eventStreamOrigin = SecurityOrigin::create(response.url())->toString();
    220     int statusCode = response.httpStatusCode();
    221     bool mimeTypeIsValid = response.mimeType() == "text/event-stream";
    222     bool responseIsValid = statusCode == 200 && mimeTypeIsValid;
    223     if (responseIsValid) {
    224         const String& charset = response.textEncodingName();
    225         // If we have a charset, the only allowed value is UTF-8 (case-insensitive).
    226         responseIsValid = charset.isEmpty() || equalIgnoringCase(charset, "UTF-8");
    227         if (!responseIsValid) {
    228             StringBuilder message;
    229             message.appendLiteral("EventSource's response has a charset (\"");
    230             message.append(charset);
    231             message.appendLiteral("\") that is not UTF-8. Aborting the connection.");
    232             // FIXME: We are missing the source line.
    233             scriptExecutionContext()->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message.toString());
    234         }
    235     } else {
    236         // To keep the signal-to-noise ratio low, we only log 200-response with an invalid MIME type.
    237         if (statusCode == 200 && !mimeTypeIsValid) {
    238             StringBuilder message;
    239             message.appendLiteral("EventSource's response has a MIME type (\"");
    240             message.append(response.mimeType());
    241             message.appendLiteral("\") that is not \"text/event-stream\". Aborting the connection.");
    242             // FIXME: We are missing the source line.
    243             scriptExecutionContext()->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message.toString());
    244         }
    245     }
    246 
    247     if (responseIsValid) {
    248         m_state = OPEN;
    249         dispatchEvent(Event::create(eventNames().openEvent, false, false));
    250     } else {
    251         m_loader->cancel();
    252         dispatchEvent(Event::create(eventNames().errorEvent, false, false));
    253     }
    254 }
    255 
    256 void EventSource::didReceiveData(const char* data, int length)
    257 {
    258     ASSERT(m_state == OPEN);
    259     ASSERT(m_requestInFlight);
    260 
    261     append(m_receiveBuf, m_decoder->decode(data, length));
    262     parseEventStream();
    263 }
    264 
    265 void EventSource::didFinishLoading(unsigned long, double)
    266 {
    267     ASSERT(m_state == OPEN);
    268     ASSERT(m_requestInFlight);
    269 
    270     if (m_receiveBuf.size() > 0 || m_data.size() > 0) {
    271         parseEventStream();
    272 
    273         // Discard everything that has not been dispatched by now.
    274         m_receiveBuf.clear();
    275         m_data.clear();
    276         m_eventName = "";
    277         m_currentlyParsedEventId = String();
    278     }
    279     networkRequestEnded();
    280 }
    281 
    282 void EventSource::didFail(const ResourceError& error)
    283 {
    284     ASSERT(m_state != CLOSED);
    285     ASSERT(m_requestInFlight);
    286 
    287     if (error.isCancellation())
    288         m_state = CLOSED;
    289     networkRequestEnded();
    290 }
    291 
    292 void EventSource::didFailAccessControlCheck(const ResourceError& error)
    293 {
    294     String message = "EventSource cannot load " + error.failingURL() + ". " + error.localizedDescription();
    295     scriptExecutionContext()->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message);
    296 
    297     abortConnectionAttempt();
    298 }
    299 
    300 void EventSource::didFailRedirectCheck()
    301 {
    302     abortConnectionAttempt();
    303 }
    304 
    305 void EventSource::abortConnectionAttempt()
    306 {
    307     ASSERT(m_state == CONNECTING);
    308     ASSERT(m_requestInFlight);
    309 
    310     m_loader->cancel();
    311 
    312     ASSERT(m_state == CLOSED);
    313     dispatchEvent(Event::create(eventNames().errorEvent, false, false));
    314 }
    315 
    316 void EventSource::parseEventStream()
    317 {
    318     unsigned int bufPos = 0;
    319     unsigned int bufSize = m_receiveBuf.size();
    320     while (bufPos < bufSize) {
    321         if (m_discardTrailingNewline) {
    322             if (m_receiveBuf[bufPos] == '\n')
    323                 bufPos++;
    324             m_discardTrailingNewline = false;
    325         }
    326 
    327         int lineLength = -1;
    328         int fieldLength = -1;
    329         for (unsigned int i = bufPos; lineLength < 0 && i < bufSize; i++) {
    330             switch (m_receiveBuf[i]) {
    331             case ':':
    332                 if (fieldLength < 0)
    333                     fieldLength = i - bufPos;
    334                 break;
    335             case '\r':
    336                 m_discardTrailingNewline = true;
    337             case '\n':
    338                 lineLength = i - bufPos;
    339                 break;
    340             }
    341         }
    342 
    343         if (lineLength < 0)
    344             break;
    345 
    346         parseEventStreamLine(bufPos, fieldLength, lineLength);
    347         bufPos += lineLength + 1;
    348 
    349         // EventSource.close() might've been called by one of the message event handlers.
    350         // Per spec, no further messages should be fired after that.
    351         if (m_state == CLOSED)
    352             break;
    353     }
    354 
    355     if (bufPos == bufSize)
    356         m_receiveBuf.clear();
    357     else if (bufPos)
    358         m_receiveBuf.remove(0, bufPos);
    359 }
    360 
    361 void EventSource::parseEventStreamLine(unsigned bufPos, int fieldLength, int lineLength)
    362 {
    363     if (!lineLength) {
    364         if (!m_data.isEmpty()) {
    365             m_data.removeLast();
    366             if (!m_currentlyParsedEventId.isNull()) {
    367                 m_lastEventId.swap(m_currentlyParsedEventId);
    368                 m_currentlyParsedEventId = String();
    369             }
    370             dispatchEvent(createMessageEvent());
    371         }
    372         if (!m_eventName.isEmpty())
    373             m_eventName = "";
    374     } else if (fieldLength) {
    375         bool noValue = fieldLength < 0;
    376 
    377         String field(&m_receiveBuf[bufPos], noValue ? lineLength : fieldLength);
    378         int step;
    379         if (noValue)
    380             step = lineLength;
    381         else if (m_receiveBuf[bufPos + fieldLength + 1] != ' ')
    382             step = fieldLength + 1;
    383         else
    384             step = fieldLength + 2;
    385         bufPos += step;
    386         int valueLength = lineLength - step;
    387 
    388         if (field == "data") {
    389             if (valueLength)
    390                 m_data.append(&m_receiveBuf[bufPos], valueLength);
    391             m_data.append('\n');
    392         } else if (field == "event")
    393             m_eventName = valueLength ? String(&m_receiveBuf[bufPos], valueLength) : "";
    394         else if (field == "id")
    395             m_currentlyParsedEventId = valueLength ? String(&m_receiveBuf[bufPos], valueLength) : "";
    396         else if (field == "retry") {
    397             if (!valueLength)
    398                 m_reconnectDelay = defaultReconnectDelay;
    399             else {
    400                 String value(&m_receiveBuf[bufPos], valueLength);
    401                 bool ok;
    402                 unsigned long long retry = value.toUInt64(&ok);
    403                 if (ok)
    404                     m_reconnectDelay = retry;
    405             }
    406         }
    407     }
    408 }
    409 
    410 void EventSource::stop()
    411 {
    412     close();
    413 }
    414 
    415 PassRefPtr<MessageEvent> EventSource::createMessageEvent()
    416 {
    417     RefPtr<MessageEvent> event = MessageEvent::create();
    418     event->initMessageEvent(m_eventName.isEmpty() ? eventNames().messageEvent : AtomicString(m_eventName), false, false, SerializedScriptValue::create(String(m_data)), m_eventStreamOrigin, m_lastEventId, 0, nullptr);
    419     m_data.clear();
    420     return event.release();
    421 }
    422 
    423 EventTargetData* EventSource::eventTargetData()
    424 {
    425     return &m_eventTargetData;
    426 }
    427 
    428 EventTargetData* EventSource::ensureEventTargetData()
    429 {
    430     return &m_eventTargetData;
    431 }
    432 
    433 } // namespace WebCore
    434