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
56namespace WebCore {
57
58const unsigned long long EventSource::defaultReconnectDelay = 3000;
59
60inline 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
75PassRefPtrWillBeRawPtr<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
109EventSource::~EventSource()
110{
111    ASSERT(m_state == CLOSED);
112    ASSERT(!m_requestInFlight);
113}
114
115void EventSource::scheduleInitialConnect()
116{
117    ASSERT(m_state == CONNECTING);
118    ASSERT(!m_requestInFlight);
119
120    m_connectTimer.startOneShot(0, FROM_HERE);
121}
122
123void 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
156void 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
169void 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
176void EventSource::connectTimerFired(Timer<EventSource>*)
177{
178    connect();
179}
180
181String EventSource::url() const
182{
183    return m_url.string();
184}
185
186bool EventSource::withCredentials() const
187{
188    return m_withCredentials;
189}
190
191EventSource::State EventSource::readyState() const
192{
193    return m_state;
194}
195
196void 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
215const AtomicString& EventSource::interfaceName() const
216{
217    return EventTargetNames::EventSource;
218}
219
220ExecutionContext* EventSource::executionContext() const
221{
222    return ActiveDOMObject::executionContext();
223}
224
225void 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
267void 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
276void 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
293void 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
303void 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
311void EventSource::didFailRedirectCheck()
312{
313    abortConnectionAttempt();
314}
315
316void 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
331void 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
376void 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
425void EventSource::stop()
426{
427    close();
428}
429
430PassRefPtrWillBeRawPtr<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