Home | History | Annotate | Download | only in page
      1 /*
      2  * Copyright (C) 2009 Ericsson AB
      3  * All rights reserved.
      4  * Copyright (C) 2010 Apple Inc. All rights reserved.
      5  * Copyright (C) 2011, Code Aurora Forum. All rights reserved.
      6  *
      7  * Redistribution and use in source and binary forms, with or without
      8  * modification, are permitted provided that the following conditions
      9  * are met:
     10  *
     11  * 1. Redistributions of source code must retain the above copyright
     12  *    notice, this list of conditions and the following disclaimer.
     13  * 2. Redistributions in binary form must reproduce the above copyright
     14  *    notice, this list of conditions and the following disclaimer
     15  *    in the documentation and/or other materials provided with the
     16  *    distribution.
     17  * 3. Neither the name of Ericsson nor the names of its contributors
     18  *    may be used to endorse or promote products derived from this
     19  *    software without specific prior written permission.
     20  *
     21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     22  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     24  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     25  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     28  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     29  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     30  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     31  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     32  */
     33 
     34 #include "config.h"
     35 
     36 #if ENABLE(EVENTSOURCE)
     37 
     38 #include "EventSource.h"
     39 
     40 #include "MemoryCache.h"
     41 #include "DOMWindow.h"
     42 #include "Event.h"
     43 #include "EventException.h"
     44 #include "PlatformString.h"
     45 #include "MessageEvent.h"
     46 #include "ResourceError.h"
     47 #include "ResourceRequest.h"
     48 #include "ResourceResponse.h"
     49 #include "ScriptCallStack.h"
     50 #include "ScriptExecutionContext.h"
     51 #include "SerializedScriptValue.h"
     52 #include "TextResourceDecoder.h"
     53 #include "ThreadableLoader.h"
     54 
     55 namespace WebCore {
     56 
     57 const unsigned long long EventSource::defaultReconnectDelay = 3000;
     58 
     59 inline EventSource::EventSource(const KURL& url, ScriptExecutionContext* context)
     60     : ActiveDOMObject(context, this)
     61     , m_url(url)
     62     , m_state(CONNECTING)
     63     , m_decoder(TextResourceDecoder::create("text/plain", "UTF-8"))
     64     , m_reconnectTimer(this, &EventSource::reconnectTimerFired)
     65     , m_discardTrailingNewline(false)
     66     , m_failSilently(false)
     67     , m_requestInFlight(false)
     68     , m_reconnectDelay(defaultReconnectDelay)
     69     , m_origin(context->securityOrigin()->toString())
     70 {
     71 }
     72 
     73 PassRefPtr<EventSource> EventSource::create(const String& url, ScriptExecutionContext* context, ExceptionCode& ec)
     74 {
     75     if (url.isEmpty()) {
     76         ec = SYNTAX_ERR;
     77         return 0;
     78     }
     79 
     80     KURL fullURL = context->completeURL(url);
     81     if (!fullURL.isValid()) {
     82         ec = SYNTAX_ERR;
     83         return 0;
     84     }
     85 
     86     // FIXME: Should support at least some cross-origin requests.
     87     if (!context->securityOrigin()->canRequest(fullURL)) {
     88         ec = SECURITY_ERR;
     89         return 0;
     90     }
     91 
     92     RefPtr<EventSource> source = adoptRef(new EventSource(fullURL, context));
     93 
     94     source->setPendingActivity(source.get());
     95     source->connect();
     96 
     97     return source.release();
     98 }
     99 
    100 EventSource::~EventSource()
    101 {
    102 }
    103 
    104 void EventSource::connect()
    105 {
    106     ResourceRequest request(m_url);
    107     request.setHTTPMethod("GET");
    108     request.setHTTPHeaderField("Accept", "text/event-stream");
    109     request.setHTTPHeaderField("Cache-Control", "no-cache");
    110     if (!m_lastEventId.isEmpty())
    111         request.setHTTPHeaderField("Last-Event-ID", m_lastEventId);
    112 
    113     ThreadableLoaderOptions options;
    114     options.sendLoadCallbacks = true;
    115     options.sniffContent = false;
    116     options.allowCredentials = true;
    117 
    118     m_loader = ThreadableLoader::create(scriptExecutionContext(), this, request, options);
    119 
    120     m_requestInFlight = true;
    121 }
    122 
    123 void EventSource::endRequest()
    124 {
    125     if (!m_requestInFlight)
    126         return;
    127 
    128     m_requestInFlight = false;
    129 
    130     if (!m_failSilently)
    131         dispatchEvent(Event::create(eventNames().errorEvent, false, false));
    132 
    133     if (m_state != CLOSED)
    134         scheduleReconnect();
    135     else
    136         unsetPendingActivity(this);
    137 }
    138 
    139 void EventSource::scheduleReconnect()
    140 {
    141     m_state = CONNECTING;
    142     m_reconnectTimer.startOneShot(m_reconnectDelay / 1000);
    143 }
    144 
    145 void EventSource::reconnectTimerFired(Timer<EventSource>*)
    146 {
    147     connect();
    148 }
    149 
    150 String EventSource::url() const
    151 {
    152     return m_url.string();
    153 }
    154 
    155 EventSource::State EventSource::readyState() const
    156 {
    157     return m_state;
    158 }
    159 
    160 void EventSource::close()
    161 {
    162     if (m_state == CLOSED)
    163         return;
    164 
    165     if (m_reconnectTimer.isActive()) {
    166         m_reconnectTimer.stop();
    167         unsetPendingActivity(this);
    168     }
    169 
    170     m_state = CLOSED;
    171     m_failSilently = true;
    172 
    173     if (m_requestInFlight)
    174         m_loader->cancel();
    175 }
    176 
    177 ScriptExecutionContext* EventSource::scriptExecutionContext() const
    178 {
    179     return ActiveDOMObject::scriptExecutionContext();
    180 }
    181 
    182 void EventSource::didReceiveResponse(const ResourceResponse& response)
    183 {
    184     int statusCode = response.httpStatusCode();
    185     bool mimeTypeIsValid = response.mimeType() == "text/event-stream";
    186     bool responseIsValid = statusCode == 200 && mimeTypeIsValid;
    187     if (responseIsValid) {
    188         const String& charset = response.textEncodingName();
    189         // If we have a charset, the only allowed value is UTF-8 (case-insensitive). This should match
    190         // the updated EventSource standard.
    191         responseIsValid = charset.isEmpty() || equalIgnoringCase(charset, "UTF-8");
    192         if (!responseIsValid) {
    193             String message = "EventSource's response has a charset (\"";
    194             message += charset;
    195             message += "\") that is not UTF-8. Aborting the connection.";
    196             // FIXME: We are missing the source line.
    197             scriptExecutionContext()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, message, 1, String(), 0);
    198         }
    199     } else {
    200         // To keep the signal-to-noise ratio low, we only log 200-response with an invalid MIME type.
    201         if (statusCode == 200 && !mimeTypeIsValid) {
    202             String message = "EventSource's response has a MIME type (\"";
    203             message += response.mimeType();
    204             message += "\") that is not \"text/event-stream\". Aborting the connection.";
    205             // FIXME: We are missing the source line.
    206             scriptExecutionContext()->addMessage(JSMessageSource, LogMessageType, ErrorMessageLevel, message, 1, String(), 0);
    207         }
    208     }
    209 
    210     if (responseIsValid) {
    211         m_state = OPEN;
    212         dispatchEvent(Event::create(eventNames().openEvent, false, false));
    213     } else {
    214         if (statusCode <= 200 || statusCode > 299)
    215             m_state = CLOSED;
    216         m_loader->cancel();
    217     }
    218 }
    219 
    220 void EventSource::didReceiveData(const char* data, int length)
    221 {
    222     append(m_receiveBuf, m_decoder->decode(data, length));
    223     parseEventStream();
    224 }
    225 
    226 void EventSource::didFinishLoading(unsigned long, double)
    227 {
    228     if (m_receiveBuf.size() > 0 || m_data.size() > 0) {
    229         append(m_receiveBuf, "\n\n");
    230         parseEventStream();
    231     }
    232     m_state = CONNECTING;
    233     endRequest();
    234 }
    235 
    236 void EventSource::didFail(const ResourceError& error)
    237 {
    238     int canceled = error.isCancellation();
    239     if (((m_state == CONNECTING) && !canceled) || ((m_state == OPEN) && canceled))
    240         m_state = CLOSED;
    241     endRequest();
    242 }
    243 
    244 void EventSource::didFailRedirectCheck()
    245 {
    246     m_state = CLOSED;
    247     m_loader->cancel();
    248 }
    249 
    250 void EventSource::parseEventStream()
    251 {
    252     unsigned int bufPos = 0;
    253     unsigned int bufSize = m_receiveBuf.size();
    254     while (bufPos < bufSize) {
    255         if (m_discardTrailingNewline) {
    256             if (m_receiveBuf[bufPos] == '\n')
    257                 bufPos++;
    258             m_discardTrailingNewline = false;
    259         }
    260 
    261         int lineLength = -1;
    262         int fieldLength = -1;
    263         for (unsigned int i = bufPos; lineLength < 0 && i < bufSize; i++) {
    264             switch (m_receiveBuf[i]) {
    265             case ':':
    266                 if (fieldLength < 0)
    267                     fieldLength = i - bufPos;
    268                 break;
    269             case '\r':
    270                 m_discardTrailingNewline = true;
    271             case '\n':
    272                 lineLength = i - bufPos;
    273                 break;
    274             }
    275         }
    276 
    277         if (lineLength < 0)
    278             break;
    279 
    280         parseEventStreamLine(bufPos, fieldLength, lineLength);
    281         bufPos += lineLength + 1;
    282     }
    283 
    284     if (bufPos == bufSize)
    285         m_receiveBuf.clear();
    286     else if (bufPos)
    287         m_receiveBuf.remove(0, bufPos);
    288 }
    289 
    290 void EventSource::parseEventStreamLine(unsigned int bufPos, int fieldLength, int lineLength)
    291 {
    292     if (!lineLength) {
    293         if (!m_data.isEmpty()) {
    294             m_data.removeLast();
    295             dispatchEvent(createMessageEvent());
    296         }
    297         if (!m_eventName.isEmpty())
    298             m_eventName = "";
    299     } else if (fieldLength) {
    300         bool noValue = fieldLength < 0;
    301 
    302         String field(&m_receiveBuf[bufPos], noValue ? lineLength : fieldLength);
    303         int step;
    304         if (noValue)
    305             step = lineLength;
    306         else if (m_receiveBuf[bufPos + fieldLength + 1] != ' ')
    307             step = fieldLength + 1;
    308         else
    309             step = fieldLength + 2;
    310         bufPos += step;
    311         int valueLength = lineLength - step;
    312 
    313         if (field == "data") {
    314             if (valueLength)
    315                 m_data.append(&m_receiveBuf[bufPos], valueLength);
    316             m_data.append('\n');
    317         } else if (field == "event")
    318             m_eventName = valueLength ? String(&m_receiveBuf[bufPos], valueLength) : "";
    319         else if (field == "id")
    320             m_lastEventId = valueLength ? String(&m_receiveBuf[bufPos], valueLength) : "";
    321         else if (field == "retry") {
    322             if (!valueLength)
    323                 m_reconnectDelay = defaultReconnectDelay;
    324             else {
    325                 String value(&m_receiveBuf[bufPos], valueLength);
    326                 bool ok;
    327                 unsigned long long retry = value.toUInt64(&ok);
    328                 if (ok)
    329                     m_reconnectDelay = retry;
    330             }
    331         }
    332     }
    333 }
    334 
    335 void EventSource::stop()
    336 {
    337     close();
    338 }
    339 
    340 PassRefPtr<MessageEvent> EventSource::createMessageEvent()
    341 {
    342     RefPtr<MessageEvent> event = MessageEvent::create();
    343     event->initMessageEvent(m_eventName.isEmpty() ? eventNames().messageEvent : AtomicString(m_eventName), false, false, SerializedScriptValue::create(String::adopt(m_data)), m_origin, m_lastEventId, 0, 0);
    344     return event.release();
    345 }
    346 
    347 EventTargetData* EventSource::eventTargetData()
    348 {
    349     return &m_eventTargetData;
    350 }
    351 
    352 EventTargetData* EventSource::ensureEventTargetData()
    353 {
    354     return &m_eventTargetData;
    355 }
    356 
    357 } // namespace WebCore
    358 
    359 #endif // ENABLE(EVENTSOURCE)
    360