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