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/ExceptionCode.h"
     42 #include "core/dom/ExecutionContext.h"
     43 #include "core/events/Event.h"
     44 #include "core/events/MessageEvent.h"
     45 #include "core/frame/LocalDOMWindow.h"
     46 #include "core/frame/LocalFrame.h"
     47 #include "core/frame/csp/ContentSecurityPolicy.h"
     48 #include "core/html/parser/TextResourceDecoder.h"
     49 #include "core/loader/ThreadableLoader.h"
     50 #include "platform/network/ResourceError.h"
     51 #include "platform/network/ResourceRequest.h"
     52 #include "platform/network/ResourceResponse.h"
     53 #include "platform/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(ExecutionContext* 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_connectTimer(this, &EventSource::connectTimerFired)
     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 PassRefPtrWillBeRawPtr<EventSource> EventSource::create(ExecutionContext* context, const String& url, const Dictionary& eventSourceInit, ExceptionState& exceptionState)
     76 {
     77     if (url.isEmpty()) {
     78         exceptionState.throwDOMException(SyntaxError, "Cannot open an EventSource to an empty URL.");
     79         return nullptr;
     80     }
     81 
     82     KURL fullURL = context->completeURL(url);
     83     if (!fullURL.isValid()) {
     84         exceptionState.throwDOMException(SyntaxError, "Cannot open an EventSource to '" + url + "'. The URL is invalid.");
     85         return nullptr;
     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         // We can safely expose the URL to JavaScript, as this exception is generate synchronously before any redirects take place.
     96         exceptionState.throwSecurityError("Refused to connect to '" + fullURL.elidedString() + "' because it violates the document's Content Security Policy.");
     97         return nullptr;
     98     }
     99 
    100     RefPtrWillBeRawPtr<EventSource> source = adoptRefWillBeRefCountedGarbageCollected(new EventSource(context, fullURL, eventSourceInit));
    101 
    102     source->setPendingActivity(source.get());
    103     source->scheduleInitialConnect();
    104     source->suspendIfNeeded();
    105 
    106     return source.release();
    107 }
    108 
    109 EventSource::~EventSource()
    110 {
    111     ASSERT(m_state == CLOSED);
    112     ASSERT(!m_requestInFlight);
    113 }
    114 
    115 void EventSource::scheduleInitialConnect()
    116 {
    117     ASSERT(m_state == CONNECTING);
    118     ASSERT(!m_requestInFlight);
    119 
    120     m_connectTimer.startOneShot(0, FROM_HERE);
    121 }
    122 
    123 void EventSource::connect()
    124 {
    125     ASSERT(m_state == CONNECTING);
    126     ASSERT(!m_requestInFlight);
    127     ASSERT(executionContext());
    128 
    129     ExecutionContext& executionContext = *this->executionContext();
    130     ResourceRequest request(m_url);
    131     request.setHTTPMethod("GET");
    132     request.setHTTPHeaderField("Accept", "text/event-stream");
    133     request.setHTTPHeaderField("Cache-Control", "no-cache");
    134     if (!m_lastEventId.isEmpty())
    135         request.setHTTPHeaderField("Last-Event-ID", m_lastEventId);
    136 
    137     SecurityOrigin* origin = executionContext.securityOrigin();
    138 
    139     ThreadableLoaderOptions options;
    140     options.preflightPolicy = PreventPreflight;
    141     options.crossOriginRequestPolicy = UseAccessControl;
    142     options.contentSecurityPolicyEnforcement = ContentSecurityPolicy::shouldBypassMainWorld(&executionContext) ? DoNotEnforceContentSecurityPolicy : EnforceConnectSrcDirective;
    143 
    144     ResourceLoaderOptions resourceLoaderOptions;
    145     resourceLoaderOptions.allowCredentials = (origin->canRequest(m_url) || m_withCredentials) ? AllowStoredCredentials : DoNotAllowStoredCredentials;
    146     resourceLoaderOptions.credentialsRequested = m_withCredentials ? ClientRequestedCredentials : ClientDidNotRequestCredentials;
    147     resourceLoaderOptions.dataBufferingPolicy = DoNotBufferData;
    148     resourceLoaderOptions.securityOrigin = origin;
    149 
    150     m_loader = ThreadableLoader::create(executionContext, this, request, options, resourceLoaderOptions);
    151 
    152     if (m_loader)
    153         m_requestInFlight = true;
    154 }
    155 
    156 void EventSource::networkRequestEnded()
    157 {
    158     if (!m_requestInFlight)
    159         return;
    160 
    161     m_requestInFlight = false;
    162 
    163     if (m_state != CLOSED)
    164         scheduleReconnect();
    165     else
    166         unsetPendingActivity(this);
    167 }
    168 
    169 void EventSource::scheduleReconnect()
    170 {
    171     m_state = CONNECTING;
    172     m_connectTimer.startOneShot(m_reconnectDelay / 1000.0, FROM_HERE);
    173     dispatchEvent(Event::create(EventTypeNames::error));
    174 }
    175 
    176 void EventSource::connectTimerFired(Timer<EventSource>*)
    177 {
    178     connect();
    179 }
    180 
    181 String EventSource::url() const
    182 {
    183     return m_url.string();
    184 }
    185 
    186 bool EventSource::withCredentials() const
    187 {
    188     return m_withCredentials;
    189 }
    190 
    191 EventSource::State EventSource::readyState() const
    192 {
    193     return m_state;
    194 }
    195 
    196 void EventSource::close()
    197 {
    198     if (m_state == CLOSED) {
    199         ASSERT(!m_requestInFlight);
    200         return;
    201     }
    202 
    203     // Stop trying to reconnect if EventSource was explicitly closed or if ActiveDOMObject::stop() was called.
    204     if (m_connectTimer.isActive()) {
    205         m_connectTimer.stop();
    206         unsetPendingActivity(this);
    207     }
    208 
    209     if (m_requestInFlight)
    210         m_loader->cancel();
    211 
    212     m_state = CLOSED;
    213 }
    214 
    215 const AtomicString& EventSource::interfaceName() const
    216 {
    217     return EventTargetNames::EventSource;
    218 }
    219 
    220 ExecutionContext* EventSource::executionContext() const
    221 {
    222     return ActiveDOMObject::executionContext();
    223 }
    224 
    225 void EventSource::didReceiveResponse(unsigned long, const ResourceResponse& response)
    226 {
    227     ASSERT(m_state == CONNECTING);
    228     ASSERT(m_requestInFlight);
    229 
    230     m_eventStreamOrigin = SecurityOrigin::create(response.url())->toString();
    231     int statusCode = response.httpStatusCode();
    232     bool mimeTypeIsValid = response.mimeType() == "text/event-stream";
    233     bool responseIsValid = statusCode == 200 && mimeTypeIsValid;
    234     if (responseIsValid) {
    235         const String& charset = response.textEncodingName();
    236         // If we have a charset, the only allowed value is UTF-8 (case-insensitive).
    237         responseIsValid = charset.isEmpty() || equalIgnoringCase(charset, "UTF-8");
    238         if (!responseIsValid) {
    239             StringBuilder message;
    240             message.appendLiteral("EventSource's response has a charset (\"");
    241             message.append(charset);
    242             message.appendLiteral("\") that is not UTF-8. Aborting the connection.");
    243             // FIXME: We are missing the source line.
    244             executionContext()->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message.toString());
    245         }
    246     } else {
    247         // To keep the signal-to-noise ratio low, we only log 200-response with an invalid MIME type.
    248         if (statusCode == 200 && !mimeTypeIsValid) {
    249             StringBuilder message;
    250             message.appendLiteral("EventSource's response has a MIME type (\"");
    251             message.append(response.mimeType());
    252             message.appendLiteral("\") that is not \"text/event-stream\". Aborting the connection.");
    253             // FIXME: We are missing the source line.
    254             executionContext()->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message.toString());
    255         }
    256     }
    257 
    258     if (responseIsValid) {
    259         m_state = OPEN;
    260         dispatchEvent(Event::create(EventTypeNames::open));
    261     } else {
    262         m_loader->cancel();
    263         dispatchEvent(Event::create(EventTypeNames::error));
    264     }
    265 }
    266 
    267 void EventSource::didReceiveData(const char* data, int length)
    268 {
    269     ASSERT(m_state == OPEN);
    270     ASSERT(m_requestInFlight);
    271 
    272     append(m_receiveBuf, m_decoder->decode(data, length));
    273     parseEventStream();
    274 }
    275 
    276 void EventSource::didFinishLoading(unsigned long, double)
    277 {
    278     ASSERT(m_state == OPEN);
    279     ASSERT(m_requestInFlight);
    280 
    281     if (m_receiveBuf.size() > 0 || m_data.size() > 0) {
    282         parseEventStream();
    283 
    284         // Discard everything that has not been dispatched by now.
    285         m_receiveBuf.clear();
    286         m_data.clear();
    287         m_eventName = emptyAtom;
    288         m_currentlyParsedEventId = nullAtom;
    289     }
    290     networkRequestEnded();
    291 }
    292 
    293 void EventSource::didFail(const ResourceError& error)
    294 {
    295     ASSERT(m_state != CLOSED);
    296     ASSERT(m_requestInFlight);
    297 
    298     if (error.isCancellation())
    299         m_state = CLOSED;
    300     networkRequestEnded();
    301 }
    302 
    303 void EventSource::didFailAccessControlCheck(const ResourceError& error)
    304 {
    305     String message = "EventSource cannot load " + error.failingURL() + ". " + error.localizedDescription();
    306     executionContext()->addConsoleMessage(JSMessageSource, ErrorMessageLevel, message);
    307 
    308     abortConnectionAttempt();
    309 }
    310 
    311 void EventSource::didFailRedirectCheck()
    312 {
    313     abortConnectionAttempt();
    314 }
    315 
    316 void EventSource::abortConnectionAttempt()
    317 {
    318     ASSERT(m_state == CONNECTING);
    319 
    320     if (m_requestInFlight) {
    321         m_loader->cancel();
    322     } else {
    323         m_state = CLOSED;
    324         unsetPendingActivity(this);
    325     }
    326 
    327     ASSERT(m_state == CLOSED);
    328     dispatchEvent(Event::create(EventTypeNames::error));
    329 }
    330 
    331 void EventSource::parseEventStream()
    332 {
    333     unsigned bufPos = 0;
    334     unsigned bufSize = m_receiveBuf.size();
    335     while (bufPos < bufSize) {
    336         if (m_discardTrailingNewline) {
    337             if (m_receiveBuf[bufPos] == '\n')
    338                 bufPos++;
    339             m_discardTrailingNewline = false;
    340         }
    341 
    342         int lineLength = -1;
    343         int fieldLength = -1;
    344         for (unsigned i = bufPos; lineLength < 0 && i < bufSize; i++) {
    345             switch (m_receiveBuf[i]) {
    346             case ':':
    347                 if (fieldLength < 0)
    348                     fieldLength = i - bufPos;
    349                 break;
    350             case '\r':
    351                 m_discardTrailingNewline = true;
    352             case '\n':
    353                 lineLength = i - bufPos;
    354                 break;
    355             }
    356         }
    357 
    358         if (lineLength < 0)
    359             break;
    360 
    361         parseEventStreamLine(bufPos, fieldLength, lineLength);
    362         bufPos += lineLength + 1;
    363 
    364         // EventSource.close() might've been called by one of the message event handlers.
    365         // Per spec, no further messages should be fired after that.
    366         if (m_state == CLOSED)
    367             break;
    368     }
    369 
    370     if (bufPos == bufSize)
    371         m_receiveBuf.clear();
    372     else if (bufPos)
    373         m_receiveBuf.remove(0, bufPos);
    374 }
    375 
    376 void EventSource::parseEventStreamLine(unsigned bufPos, int fieldLength, int lineLength)
    377 {
    378     if (!lineLength) {
    379         if (!m_data.isEmpty()) {
    380             m_data.removeLast();
    381             if (!m_currentlyParsedEventId.isNull()) {
    382                 m_lastEventId = m_currentlyParsedEventId;
    383                 m_currentlyParsedEventId = nullAtom;
    384             }
    385             dispatchEvent(createMessageEvent());
    386         }
    387         if (!m_eventName.isEmpty())
    388             m_eventName = emptyAtom;
    389     } else if (fieldLength) {
    390         bool noValue = fieldLength < 0;
    391 
    392         String field(&m_receiveBuf[bufPos], noValue ? lineLength : fieldLength);
    393         int step;
    394         if (noValue)
    395             step = lineLength;
    396         else if (m_receiveBuf[bufPos + fieldLength + 1] != ' ')
    397             step = fieldLength + 1;
    398         else
    399             step = fieldLength + 2;
    400         bufPos += step;
    401         int valueLength = lineLength - step;
    402 
    403         if (field == "data") {
    404             if (valueLength)
    405                 m_data.append(&m_receiveBuf[bufPos], valueLength);
    406             m_data.append('\n');
    407         } else if (field == "event") {
    408             m_eventName = valueLength ? AtomicString(&m_receiveBuf[bufPos], valueLength) : "";
    409         } else if (field == "id") {
    410             m_currentlyParsedEventId = valueLength ? AtomicString(&m_receiveBuf[bufPos], valueLength) : "";
    411         } else if (field == "retry") {
    412             if (!valueLength)
    413                 m_reconnectDelay = defaultReconnectDelay;
    414             else {
    415                 String value(&m_receiveBuf[bufPos], valueLength);
    416                 bool ok;
    417                 unsigned long long retry = value.toUInt64(&ok);
    418                 if (ok)
    419                     m_reconnectDelay = retry;
    420             }
    421         }
    422     }
    423 }
    424 
    425 void EventSource::stop()
    426 {
    427     close();
    428 }
    429 
    430 PassRefPtrWillBeRawPtr<MessageEvent> EventSource::createMessageEvent()
    431 {
    432     RefPtrWillBeRawPtr<MessageEvent> event = MessageEvent::create();
    433     event->initMessageEvent(m_eventName.isEmpty() ? EventTypeNames::message : m_eventName, false, false, SerializedScriptValue::create(String(m_data)), m_eventStreamOrigin, m_lastEventId, 0, nullptr);
    434     m_data.clear();
    435     return event.release();
    436 }
    437 
    438 } // namespace WebCore
    439