mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
ec409bf4d2
Differential Revision: https://phabricator.services.mozilla.com/D224059
2074 lines
63 KiB
C++
2074 lines
63 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "mozilla/ArrayUtils.h"
|
|
#include "mozilla/Components.h"
|
|
#include "mozilla/DataMutex.h"
|
|
#include "mozilla/DebugOnly.h"
|
|
#include "mozilla/LoadInfo.h"
|
|
#include "mozilla/DOMEventTargetHelper.h"
|
|
#include "mozilla/GlobalFreezeObserver.h"
|
|
#include "mozilla/dom/EventSource.h"
|
|
#include "mozilla/dom/EventSourceBinding.h"
|
|
#include "mozilla/dom/MessageEvent.h"
|
|
#include "mozilla/dom/MessageEventBinding.h"
|
|
#include "mozilla/dom/ScriptSettings.h"
|
|
#include "mozilla/dom/WorkerPrivate.h"
|
|
#include "mozilla/dom/WorkerRef.h"
|
|
#include "mozilla/dom/WorkerRunnable.h"
|
|
#include "mozilla/dom/WorkerScope.h"
|
|
#include "mozilla/dom/EventSourceEventService.h"
|
|
#include "mozilla/ScopeExit.h"
|
|
#include "mozilla/Try.h"
|
|
#include "mozilla/UniquePtrExtensions.h"
|
|
#include "nsComponentManagerUtils.h"
|
|
#include "nsIThreadRetargetableStreamListener.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsIAuthPrompt.h"
|
|
#include "nsIAuthPrompt2.h"
|
|
#include "nsIHttpChannel.h"
|
|
#include "nsIInputStream.h"
|
|
#include "nsIInterfaceRequestorUtils.h"
|
|
#include "nsMimeTypes.h"
|
|
#include "nsIPromptFactory.h"
|
|
#include "nsIWindowWatcher.h"
|
|
#include "nsPresContext.h"
|
|
#include "nsProxyRelease.h"
|
|
#include "nsContentPolicyUtils.h"
|
|
#include "nsIStringBundle.h"
|
|
#include "nsIConsoleService.h"
|
|
#include "nsIScriptObjectPrincipal.h"
|
|
#include "nsJSUtils.h"
|
|
#include "nsIThreadRetargetableRequest.h"
|
|
#include "nsIAsyncVerifyRedirectCallback.h"
|
|
#include "nsIScriptError.h"
|
|
#include "nsContentUtils.h"
|
|
#include "xpcpublic.h"
|
|
#include "nsWrapperCacheInlines.h"
|
|
#include "mozilla/Attributes.h"
|
|
#include "mozilla/StaticPrefs_dom.h"
|
|
#include "nsError.h"
|
|
#include "mozilla/Encoding.h"
|
|
#include "ReferrerInfo.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
#ifdef DEBUG
|
|
static LazyLogModule gEventSourceLog("EventSource");
|
|
#endif
|
|
|
|
#define SPACE_CHAR (char16_t)0x0020
|
|
#define CR_CHAR (char16_t)0x000D
|
|
#define LF_CHAR (char16_t)0x000A
|
|
#define COLON_CHAR (char16_t)0x003A
|
|
|
|
// Reconnection time related values in milliseconds. The default one is equal
|
|
// to the default value of the pref dom.serverEvents.defaultReconnectionTime
|
|
#define MIN_RECONNECTION_TIME_VALUE 500
|
|
#define DEFAULT_RECONNECTION_TIME_VALUE 5000
|
|
#define MAX_RECONNECTION_TIME_VALUE \
|
|
PR_IntervalToMilliseconds(DELAY_INTERVAL_LIMIT)
|
|
|
|
class EventSourceImpl final : public nsIChannelEventSink,
|
|
public nsIInterfaceRequestor,
|
|
public nsISerialEventTarget,
|
|
public nsITimerCallback,
|
|
public nsINamed,
|
|
public nsIThreadRetargetableStreamListener,
|
|
public GlobalTeardownObserver,
|
|
public GlobalFreezeObserver {
|
|
public:
|
|
NS_DECL_THREADSAFE_ISUPPORTS
|
|
NS_DECL_NSIREQUESTOBSERVER
|
|
NS_DECL_NSISTREAMLISTENER
|
|
NS_DECL_NSICHANNELEVENTSINK
|
|
NS_DECL_NSIINTERFACEREQUESTOR
|
|
NS_DECL_NSIEVENTTARGET_FULL
|
|
NS_DECL_NSITIMERCALLBACK
|
|
NS_DECL_NSINAMED
|
|
NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
|
|
|
|
EventSourceImpl(EventSource* aEventSource,
|
|
nsICookieJarSettings* aCookieJarSettings);
|
|
|
|
enum { CONNECTING = 0U, OPEN = 1U, CLOSED = 2U };
|
|
|
|
void Close();
|
|
|
|
void Init(nsIGlobalObject* aWindowGlobal, nsIPrincipal* aPrincipal,
|
|
const nsAString& aURL, ErrorResult& aRv);
|
|
|
|
nsresult GetBaseURI(nsIURI** aBaseURI);
|
|
|
|
void SetupHttpChannel();
|
|
nsresult SetupReferrerInfo(const nsCOMPtr<Document>& aDocument);
|
|
nsresult InitChannelAndRequestEventSource(bool aEventTargetAccessAllowed);
|
|
nsresult ResetConnection();
|
|
void ResetDecoder();
|
|
nsresult SetReconnectionTimeout();
|
|
|
|
void AnnounceConnection();
|
|
void DispatchAllMessageEvents();
|
|
nsresult RestartConnection();
|
|
void ReestablishConnection();
|
|
void DispatchFailConnection();
|
|
void FailConnection();
|
|
|
|
void DisconnectFromOwner() override {
|
|
Close();
|
|
GlobalTeardownObserver::DisconnectFromOwner();
|
|
}
|
|
void FrozenCallback(nsIGlobalObject* aOwner) override {
|
|
DebugOnly<nsresult> rv = Freeze();
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv), "Freeze() failed");
|
|
}
|
|
void ThawedCallback(nsIGlobalObject* aOwner) override {
|
|
DebugOnly<nsresult> rv = Thaw();
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv), "Thaw() failed");
|
|
}
|
|
|
|
nsresult Thaw();
|
|
nsresult Freeze();
|
|
|
|
nsresult PrintErrorOnConsole(const char* aBundleURI, const char* aError,
|
|
const nsTArray<nsString>& aFormatStrings);
|
|
nsresult ConsoleError();
|
|
|
|
static nsresult StreamReaderFunc(nsIInputStream* aInputStream, void* aClosure,
|
|
const char* aFromRawSegment,
|
|
uint32_t aToOffset, uint32_t aCount,
|
|
uint32_t* aWriteCount);
|
|
void ParseSegment(const char* aBuffer, uint32_t aLength);
|
|
nsresult SetFieldAndClear();
|
|
void ClearFields();
|
|
nsresult ResetEvent();
|
|
nsresult DispatchCurrentMessageEvent();
|
|
nsresult ParseCharacter(char16_t aChr);
|
|
nsresult CheckHealthOfRequestCallback(nsIRequest* aRequestCallback);
|
|
nsresult OnRedirectVerifyCallback(nsresult result);
|
|
nsresult ParseURL(const nsAString& aURL);
|
|
nsresult AddGlobalObservers(nsIGlobalObject* aGlobal);
|
|
void RemoveWindowObservers();
|
|
|
|
void CloseInternal();
|
|
void CleanupOnMainThread();
|
|
|
|
bool CreateWorkerRef(WorkerPrivate* aWorkerPrivate);
|
|
void ReleaseWorkerRef();
|
|
|
|
void AssertIsOnTargetThread() const {
|
|
MOZ_DIAGNOSTIC_ASSERT(IsTargetThread());
|
|
}
|
|
|
|
bool IsTargetThread() const { return NS_GetCurrentThread() == mTargetThread; }
|
|
|
|
uint16_t ReadyState() {
|
|
auto lock = mSharedData.Lock();
|
|
if (lock->mEventSource) {
|
|
return lock->mEventSource->mReadyState;
|
|
}
|
|
// EventSourceImpl keeps EventSource alive. If mEventSource is null, it
|
|
// means that the EventSource has been closed.
|
|
return CLOSED;
|
|
}
|
|
|
|
void SetReadyState(uint16_t aReadyState) {
|
|
auto lock = mSharedData.Lock();
|
|
MOZ_ASSERT(lock->mEventSource);
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
lock->mEventSource->mReadyState = aReadyState;
|
|
}
|
|
|
|
bool IsClosed() { return ReadyState() == CLOSED; }
|
|
|
|
RefPtr<EventSource> GetEventSource() {
|
|
AssertIsOnTargetThread();
|
|
auto lock = mSharedData.Lock();
|
|
return lock->mEventSource;
|
|
}
|
|
|
|
/**
|
|
* A simple state machine used to manage the event-source's line buffer
|
|
*
|
|
* PARSE_STATE_OFF -> PARSE_STATE_BEGIN_OF_STREAM
|
|
*
|
|
* PARSE_STATE_BEGIN_OF_STREAM -> PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_BEGIN_OF_LINE |
|
|
* PARSE_STATE_COMMENT |
|
|
* PARSE_STATE_FIELD_NAME
|
|
*
|
|
* PARSE_STATE_CR_CHAR -> PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_COMMENT |
|
|
* PARSE_STATE_FIELD_NAME |
|
|
* PARSE_STATE_BEGIN_OF_LINE
|
|
*
|
|
* PARSE_STATE_COMMENT -> PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_BEGIN_OF_LINE
|
|
*
|
|
* PARSE_STATE_FIELD_NAME -> PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_BEGIN_OF_LINE |
|
|
* PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE
|
|
*
|
|
* PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE -> PARSE_STATE_FIELD_VALUE |
|
|
* PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_BEGIN_OF_LINE
|
|
*
|
|
* PARSE_STATE_FIELD_VALUE -> PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_BEGIN_OF_LINE
|
|
*
|
|
* PARSE_STATE_BEGIN_OF_LINE -> PARSE_STATE_CR_CHAR |
|
|
* PARSE_STATE_COMMENT |
|
|
* PARSE_STATE_FIELD_NAME |
|
|
* PARSE_STATE_BEGIN_OF_LINE
|
|
*
|
|
* Whenever the parser find an empty line or the end-of-file
|
|
* it dispatches the stacked event.
|
|
*
|
|
*/
|
|
enum ParserStatus {
|
|
PARSE_STATE_OFF = 0,
|
|
PARSE_STATE_BEGIN_OF_STREAM,
|
|
PARSE_STATE_CR_CHAR,
|
|
PARSE_STATE_COMMENT,
|
|
PARSE_STATE_FIELD_NAME,
|
|
PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE,
|
|
PARSE_STATE_FIELD_VALUE,
|
|
PARSE_STATE_IGNORE_FIELD_VALUE,
|
|
PARSE_STATE_BEGIN_OF_LINE
|
|
};
|
|
|
|
// Connection related data members. Should only be accessed on main thread.
|
|
nsCOMPtr<nsIURI> mSrc;
|
|
uint32_t mReconnectionTime; // in ms
|
|
nsCOMPtr<nsIPrincipal> mPrincipal;
|
|
nsString mOrigin;
|
|
nsCOMPtr<nsITimer> mTimer;
|
|
nsCOMPtr<nsIHttpChannel> mHttpChannel;
|
|
|
|
struct Message {
|
|
nsString mEventName;
|
|
// We need to be able to distinguish between different states of id field:
|
|
// 1) is not given at all
|
|
// 2) is given but is empty
|
|
// 3) is given and has a value
|
|
// We can't check for the 1st state with a simple nsString.
|
|
Maybe<nsString> mLastEventID;
|
|
nsString mData;
|
|
};
|
|
|
|
// Message related data members. May be set / initialized when initializing
|
|
// EventSourceImpl on target thread but should only be used on target thread.
|
|
nsString mLastEventID;
|
|
UniquePtr<Message> mCurrentMessage;
|
|
nsDeque<Message> mMessagesToDispatch;
|
|
ParserStatus mStatus;
|
|
mozilla::UniquePtr<mozilla::Decoder> mUnicodeDecoder;
|
|
nsString mLastFieldName;
|
|
nsString mLastFieldValue;
|
|
|
|
// EventSourceImpl internal states.
|
|
// WorkerRef to keep the worker alive. (accessed on worker thread only)
|
|
RefPtr<ThreadSafeWorkerRef> mWorkerRef;
|
|
// Whether the window is frozen. May be set on main thread and read on target
|
|
// thread.
|
|
Atomic<bool> mFrozen;
|
|
// There are some messages are going to be dispatched when thaw.
|
|
bool mGoingToDispatchAllMessages;
|
|
// Whether the EventSource is run on main thread.
|
|
const bool mIsMainThread;
|
|
// Whether the EventSourceImpl is going to be destroyed.
|
|
Atomic<bool> mIsShutDown;
|
|
|
|
class EventSourceServiceNotifier final {
|
|
public:
|
|
EventSourceServiceNotifier(RefPtr<EventSourceImpl>&& aEventSourceImpl,
|
|
uint64_t aHttpChannelId, uint64_t aInnerWindowID)
|
|
: mEventSourceImpl(std::move(aEventSourceImpl)),
|
|
mHttpChannelId(aHttpChannelId),
|
|
mInnerWindowID(aInnerWindowID),
|
|
mConnectionOpened(false) {
|
|
AssertIsOnMainThread();
|
|
mService = EventSourceEventService::GetOrCreate();
|
|
}
|
|
|
|
void ConnectionOpened() {
|
|
mEventSourceImpl->AssertIsOnTargetThread();
|
|
mService->EventSourceConnectionOpened(mHttpChannelId, mInnerWindowID);
|
|
mConnectionOpened = true;
|
|
}
|
|
|
|
void EventReceived(const nsAString& aEventName,
|
|
const nsAString& aLastEventID, const nsAString& aData,
|
|
uint32_t aRetry, DOMHighResTimeStamp aTimeStamp) {
|
|
mEventSourceImpl->AssertIsOnTargetThread();
|
|
mService->EventReceived(mHttpChannelId, mInnerWindowID, aEventName,
|
|
aLastEventID, aData, aRetry, aTimeStamp);
|
|
}
|
|
|
|
~EventSourceServiceNotifier() {
|
|
// It is safe to call this on any thread because
|
|
// EventSourceConnectionClosed method is thread safe and
|
|
// NS_ReleaseOnMainThread explicitly releases the service on the main
|
|
// thread.
|
|
if (mConnectionOpened) {
|
|
// We want to notify about connection being closed only if we told
|
|
// it was ever opened. The check is needed if OnStartRequest is called
|
|
// on the main thread while close() is called on a worker thread.
|
|
mService->EventSourceConnectionClosed(mHttpChannelId, mInnerWindowID);
|
|
}
|
|
NS_ReleaseOnMainThread("EventSourceServiceNotifier::mService",
|
|
mService.forget());
|
|
}
|
|
|
|
private:
|
|
RefPtr<EventSourceEventService> mService;
|
|
RefPtr<EventSourceImpl> mEventSourceImpl;
|
|
uint64_t mHttpChannelId;
|
|
uint64_t mInnerWindowID;
|
|
bool mConnectionOpened;
|
|
};
|
|
|
|
struct SharedData {
|
|
RefPtr<EventSource> mEventSource;
|
|
UniquePtr<EventSourceServiceNotifier> mServiceNotifier;
|
|
};
|
|
|
|
DataMutex<SharedData> mSharedData;
|
|
|
|
// Event Source owner information:
|
|
// - the script file name
|
|
// - source code line number and column number where the Event Source object
|
|
// was constructed.
|
|
// - the ID of the inner window where the script lives. Note that this may not
|
|
// be the same as the Event Source owner window.
|
|
// These attributes are used for error reporting. Should only be accessed on
|
|
// target thread
|
|
JSCallingLocation mCallingLocation;
|
|
uint64_t mInnerWindowID;
|
|
|
|
private:
|
|
nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
|
|
|
|
// Pointer to the target thread for checking whether we are
|
|
// on the target thread. This is intentionally a non-owning
|
|
// pointer in order not to affect the thread destruction
|
|
// sequence. This pointer must only be compared for equality
|
|
// and must not be dereferenced.
|
|
nsIThread* mTargetThread;
|
|
|
|
// prevent bad usage
|
|
EventSourceImpl(const EventSourceImpl& x) = delete;
|
|
EventSourceImpl& operator=(const EventSourceImpl& x) = delete;
|
|
~EventSourceImpl() {
|
|
if (IsClosed()) {
|
|
return;
|
|
}
|
|
// If we threw during Init we never called Close
|
|
SetReadyState(CLOSED);
|
|
CloseInternal();
|
|
}
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(EventSourceImpl, nsIStreamListener, nsIRequestObserver,
|
|
nsIChannelEventSink, nsIInterfaceRequestor,
|
|
nsISerialEventTarget, nsIEventTarget,
|
|
nsIThreadRetargetableStreamListener, nsITimerCallback,
|
|
nsINamed)
|
|
|
|
EventSourceImpl::EventSourceImpl(EventSource* aEventSource,
|
|
nsICookieJarSettings* aCookieJarSettings)
|
|
: mReconnectionTime(0),
|
|
mStatus(PARSE_STATE_OFF),
|
|
mFrozen(false),
|
|
mGoingToDispatchAllMessages(false),
|
|
mIsMainThread(NS_IsMainThread()),
|
|
mIsShutDown(false),
|
|
mSharedData(SharedData{aEventSource}, "EventSourceImpl::mSharedData"),
|
|
mInnerWindowID(0),
|
|
mCookieJarSettings(aCookieJarSettings),
|
|
mTargetThread(NS_GetCurrentThread()) {
|
|
MOZ_ASSERT(aEventSource);
|
|
SetReadyState(CONNECTING);
|
|
}
|
|
|
|
class CleanupRunnable final : public WorkerMainThreadRunnable {
|
|
public:
|
|
explicit CleanupRunnable(RefPtr<EventSourceImpl>&& aEventSourceImpl)
|
|
: WorkerMainThreadRunnable(GetCurrentThreadWorkerPrivate(),
|
|
"EventSource :: Cleanup"_ns),
|
|
mESImpl(std::move(aEventSourceImpl)) {
|
|
MOZ_ASSERT(mESImpl);
|
|
}
|
|
|
|
bool MainThreadRun() override {
|
|
MOZ_ASSERT(mESImpl);
|
|
mESImpl->CleanupOnMainThread();
|
|
// We want to ensure the shortest possible remaining lifetime
|
|
// and not depend on the Runnable's destruction.
|
|
mESImpl = nullptr;
|
|
return true;
|
|
}
|
|
|
|
protected:
|
|
RefPtr<EventSourceImpl> mESImpl;
|
|
};
|
|
|
|
void EventSourceImpl::Close() {
|
|
if (IsClosed()) {
|
|
return;
|
|
}
|
|
|
|
SetReadyState(CLOSED);
|
|
// CloseInternal potentially kills ourself, ensure
|
|
// to not access any members afterwards.
|
|
CloseInternal();
|
|
}
|
|
|
|
void EventSourceImpl::CloseInternal() {
|
|
AssertIsOnTargetThread();
|
|
MOZ_ASSERT(IsClosed());
|
|
|
|
RefPtr<EventSource> myES;
|
|
{
|
|
auto lock = mSharedData.Lock();
|
|
// We want to ensure to release ourself even if we have
|
|
// the shutdown case, thus we put aside a pointer
|
|
// to the EventSource and null it out right now.
|
|
myES = std::move(lock->mEventSource);
|
|
lock->mEventSource = nullptr;
|
|
lock->mServiceNotifier = nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
if (mIsShutDown) {
|
|
return;
|
|
}
|
|
|
|
// Invoke CleanupOnMainThread before cleaning any members. It will call
|
|
// ShutDown, which is supposed to be called before cleaning any members.
|
|
if (NS_IsMainThread()) {
|
|
CleanupOnMainThread();
|
|
} else {
|
|
ErrorResult rv;
|
|
// run CleanupOnMainThread synchronously on main thread since it touches
|
|
// observers and members only can be accessed on main thread.
|
|
RefPtr<CleanupRunnable> runnable = new CleanupRunnable(this);
|
|
runnable->Dispatch(GetCurrentThreadWorkerPrivate(), Killing, rv);
|
|
MOZ_ASSERT(!rv.Failed());
|
|
ReleaseWorkerRef();
|
|
}
|
|
|
|
while (mMessagesToDispatch.GetSize() != 0) {
|
|
delete mMessagesToDispatch.PopFront();
|
|
}
|
|
mFrozen = false;
|
|
ResetDecoder();
|
|
mUnicodeDecoder = nullptr;
|
|
// Release the object on its owner. Don't access to any members
|
|
// after it.
|
|
myES->mESImpl = nullptr;
|
|
}
|
|
|
|
void EventSourceImpl::CleanupOnMainThread() {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(IsClosed());
|
|
|
|
// Call ShutDown before cleaning any members.
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
mIsShutDown = true;
|
|
|
|
if (mIsMainThread) {
|
|
RemoveWindowObservers();
|
|
}
|
|
|
|
if (mTimer) {
|
|
mTimer->Cancel();
|
|
mTimer = nullptr;
|
|
}
|
|
|
|
ResetConnection();
|
|
mPrincipal = nullptr;
|
|
mSrc = nullptr;
|
|
}
|
|
|
|
class ConnectRunnable final : public WorkerMainThreadRunnable {
|
|
public:
|
|
explicit ConnectRunnable(WorkerPrivate* aWorkerPrivate,
|
|
RefPtr<EventSourceImpl> aEventSourceImpl)
|
|
: WorkerMainThreadRunnable(aWorkerPrivate, "EventSource :: Connect"_ns),
|
|
mESImpl(std::move(aEventSourceImpl)) {
|
|
MOZ_ASSERT(aWorkerPrivate);
|
|
aWorkerPrivate->AssertIsOnWorkerThread();
|
|
MOZ_ASSERT(mESImpl);
|
|
}
|
|
|
|
bool MainThreadRun() override {
|
|
MOZ_ASSERT(mESImpl);
|
|
// We are allowed to access the event target since this runnable is
|
|
// synchronized with the thread the event target lives on.
|
|
mESImpl->InitChannelAndRequestEventSource(true);
|
|
// We want to ensure the shortest possible remaining lifetime
|
|
// and not depend on the Runnable's destruction.
|
|
mESImpl = nullptr;
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
RefPtr<EventSourceImpl> mESImpl;
|
|
};
|
|
|
|
nsresult EventSourceImpl::ParseURL(const nsAString& aURL) {
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
// get the src
|
|
nsCOMPtr<nsIURI> baseURI;
|
|
nsresult rv = GetBaseURI(getter_AddRefs(baseURI));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsIURI> srcURI;
|
|
nsCOMPtr<Document> doc =
|
|
mIsMainThread ? GetEventSource()->GetDocumentIfCurrent() : nullptr;
|
|
if (doc) {
|
|
rv = NS_NewURI(getter_AddRefs(srcURI), aURL, doc->GetDocumentCharacterSet(),
|
|
baseURI);
|
|
} else {
|
|
rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI);
|
|
}
|
|
|
|
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);
|
|
|
|
nsAutoString origin;
|
|
rv = nsContentUtils::GetWebExposedOriginSerialization(srcURI, origin);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsAutoCString spec;
|
|
rv = srcURI->GetSpec(spec);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// This assignment doesn't require extra synchronization because this function
|
|
// is only ever called from EventSourceImpl::Init(), which is either called
|
|
// directly if mEventSource was created on the main thread, or via a
|
|
// synchronous runnable if it was created on a worker thread.
|
|
{
|
|
// We can't use GetEventSource() here because it would modify the refcount,
|
|
// and that's not allowed off the owning thread.
|
|
auto lock = mSharedData.Lock();
|
|
lock->mEventSource->mOriginalURL = NS_ConvertUTF8toUTF16(spec);
|
|
}
|
|
mSrc = srcURI;
|
|
mOrigin = origin;
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::AddGlobalObservers(nsIGlobalObject* aGlobal) {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(mIsMainThread);
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
|
|
GlobalTeardownObserver::BindToOwner(aGlobal);
|
|
GlobalFreezeObserver::BindToOwner(aGlobal);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::RemoveWindowObservers() {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(mIsMainThread);
|
|
MOZ_ASSERT(IsClosed());
|
|
GlobalTeardownObserver::DisconnectFromOwner();
|
|
DisconnectFreezeObserver();
|
|
}
|
|
|
|
void EventSourceImpl::Init(nsIGlobalObject* aWindowGlobal,
|
|
nsIPrincipal* aPrincipal, const nsAString& aURL,
|
|
ErrorResult& aRv) {
|
|
// aWindowGlobal should only exist for main-thread EventSource
|
|
MOZ_ASSERT_IF(aWindowGlobal, mIsMainThread);
|
|
MOZ_ASSERT(aPrincipal);
|
|
MOZ_ASSERT(ReadyState() == CONNECTING);
|
|
mPrincipal = aPrincipal;
|
|
aRv = ParseURL(aURL);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return;
|
|
}
|
|
// The conditional here is historical and not necessarily sane.
|
|
if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) {
|
|
mCallingLocation = JSCallingLocation::Get();
|
|
if (mIsMainThread) {
|
|
mInnerWindowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx);
|
|
}
|
|
}
|
|
|
|
if (mIsMainThread) {
|
|
// We observe when the window freezes and thaws.
|
|
//
|
|
// This will store raw pointer of us in GTO, which sounds scary as the
|
|
// object can be accessed cross thread. But it should be safe as GTO and
|
|
// EventSourceImpl lifetime should be managed in the same main thread.
|
|
//
|
|
// XXX(krosylight): But what about workers? See bug 1910585.
|
|
aRv = AddGlobalObservers(aWindowGlobal);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
mReconnectionTime = StaticPrefs::dom_serverEvents_defaultReconnectionTime();
|
|
if (!mReconnectionTime) {
|
|
mReconnectionTime = DEFAULT_RECONNECTION_TIME_VALUE;
|
|
}
|
|
|
|
mUnicodeDecoder = UTF_8_ENCODING->NewDecoderWithBOMRemoval();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EventSourceImpl::nsIStreamListener
|
|
//-----------------------------------------------------------------------------
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::OnStartRequest(nsIRequest* aRequest) {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
nsresult rv = CheckHealthOfRequestCallback(aRequest);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest, &rv);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsresult status;
|
|
rv = aRequest->GetStatus(&status);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (NS_FAILED(status)) {
|
|
// EventSource::OnStopRequest will evaluate if it shall either reestablish
|
|
// or fail the connection, based on the status.
|
|
return status;
|
|
}
|
|
|
|
uint32_t httpStatus;
|
|
rv = httpChannel->GetResponseStatus(&httpStatus);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (httpStatus != 200) {
|
|
DispatchFailConnection();
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
nsAutoCString contentType;
|
|
rv = httpChannel->GetContentType(contentType);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (!contentType.EqualsLiteral(TEXT_EVENT_STREAM)) {
|
|
DispatchFailConnection();
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
if (!mIsMainThread) {
|
|
// Try to retarget to worker thread, otherwise fall back to main thread.
|
|
nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(httpChannel);
|
|
if (rr) {
|
|
rv = rr->RetargetDeliveryTo(this);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
NS_WARNING("Retargeting failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
auto lock = mSharedData.Lock();
|
|
lock->mServiceNotifier = MakeUnique<EventSourceServiceNotifier>(
|
|
this, mHttpChannel->ChannelId(), mInnerWindowID);
|
|
}
|
|
rv = Dispatch(NewRunnableMethod("dom::EventSourceImpl::AnnounceConnection",
|
|
this, &EventSourceImpl::AnnounceConnection),
|
|
NS_DISPATCH_NORMAL);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
mStatus = PARSE_STATE_BEGIN_OF_STREAM;
|
|
return NS_OK;
|
|
}
|
|
|
|
// this method parses the characters as they become available instead of
|
|
// buffering them.
|
|
nsresult EventSourceImpl::StreamReaderFunc(nsIInputStream* aInputStream,
|
|
void* aClosure,
|
|
const char* aFromRawSegment,
|
|
uint32_t aToOffset, uint32_t aCount,
|
|
uint32_t* aWriteCount) {
|
|
// The EventSourceImpl instance is hold alive on the
|
|
// synchronously calling stack, so raw pointer is fine here.
|
|
EventSourceImpl* thisObject = static_cast<EventSourceImpl*>(aClosure);
|
|
if (!thisObject || !aWriteCount) {
|
|
NS_WARNING(
|
|
"EventSource cannot read from stream: no aClosure or aWriteCount");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
thisObject->AssertIsOnTargetThread();
|
|
MOZ_ASSERT(!thisObject->mIsShutDown);
|
|
thisObject->ParseSegment((const char*)aFromRawSegment, aCount);
|
|
*aWriteCount = aCount;
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::ParseSegment(const char* aBuffer, uint32_t aLength) {
|
|
AssertIsOnTargetThread();
|
|
if (IsClosed()) {
|
|
return;
|
|
}
|
|
char16_t buffer[1024];
|
|
auto dst = Span(buffer);
|
|
auto src = AsBytes(Span(aBuffer, aLength));
|
|
// XXX EOF handling is https://bugzilla.mozilla.org/show_bug.cgi?id=1369018
|
|
for (;;) {
|
|
uint32_t result;
|
|
size_t read;
|
|
size_t written;
|
|
std::tie(result, read, written, std::ignore) =
|
|
mUnicodeDecoder->DecodeToUTF16(src, dst, false);
|
|
for (auto c : dst.To(written)) {
|
|
nsresult rv = ParseCharacter(c);
|
|
NS_ENSURE_SUCCESS_VOID(rv);
|
|
}
|
|
if (result == kInputEmpty) {
|
|
return;
|
|
}
|
|
src = src.From(read);
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::OnDataAvailable(nsIRequest* aRequest,
|
|
nsIInputStream* aInputStream, uint64_t aOffset,
|
|
uint32_t aCount) {
|
|
AssertIsOnTargetThread();
|
|
NS_ENSURE_ARG_POINTER(aInputStream);
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
nsresult rv = CheckHealthOfRequestCallback(aRequest);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
uint32_t totalRead;
|
|
return aInputStream->ReadSegments(EventSourceImpl::StreamReaderFunc, this,
|
|
aCount, &totalRead);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
|
|
AssertIsOnMainThread();
|
|
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
MOZ_ASSERT(mSrc);
|
|
// "Network errors that prevents the connection from being established in the
|
|
// first place (e.g. DNS errors), must cause the user agent to asynchronously
|
|
// reestablish the connection.
|
|
//
|
|
// (...) the cancelation of the fetch algorithm by the user agent (e.g. in
|
|
// response to window.stop() or the user canceling the network connection
|
|
// manually) must cause the user agent to fail the connection.
|
|
// There could be additional network errors that are not covered in the above
|
|
// checks
|
|
// See Bug 1808511
|
|
if (NS_FAILED(aStatusCode) && aStatusCode != NS_ERROR_CONNECTION_REFUSED &&
|
|
aStatusCode != NS_ERROR_NET_TIMEOUT &&
|
|
aStatusCode != NS_ERROR_NET_RESET &&
|
|
aStatusCode != NS_ERROR_NET_INTERRUPT &&
|
|
aStatusCode != NS_ERROR_NET_PARTIAL_TRANSFER &&
|
|
aStatusCode != NS_ERROR_NET_TIMEOUT_EXTERNAL &&
|
|
aStatusCode != NS_ERROR_PROXY_CONNECTION_REFUSED &&
|
|
aStatusCode != NS_ERROR_DNS_LOOKUP_QUEUE_FULL &&
|
|
aStatusCode != NS_ERROR_INVALID_CONTENT_ENCODING) {
|
|
DispatchFailConnection();
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
nsresult rv = CheckHealthOfRequestCallback(aRequest);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv =
|
|
Dispatch(NewRunnableMethod("dom::EventSourceImpl::ReestablishConnection",
|
|
this, &EventSourceImpl::ReestablishConnection),
|
|
NS_DISPATCH_NORMAL);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EventSourceImpl::nsIChannelEventSink
|
|
//-----------------------------------------------------------------------------
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::AsyncOnChannelRedirect(
|
|
nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
|
|
nsIAsyncVerifyRedirectCallback* aCallback) {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
nsCOMPtr<nsIRequest> aOldRequest = aOldChannel;
|
|
MOZ_ASSERT(aOldRequest, "Redirect from a null request?");
|
|
|
|
nsresult rv = CheckHealthOfRequestCallback(aOldRequest);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
MOZ_ASSERT(aNewChannel, "Redirect without a channel?");
|
|
|
|
nsCOMPtr<nsIURI> newURI;
|
|
rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
bool isValidScheme = newURI->SchemeIs("http") || newURI->SchemeIs("https");
|
|
|
|
rv =
|
|
mIsMainThread ? GetEventSource()->CheckCurrentGlobalCorrectness() : NS_OK;
|
|
if (NS_FAILED(rv) || !isValidScheme) {
|
|
DispatchFailConnection();
|
|
return NS_ERROR_DOM_SECURITY_ERR;
|
|
}
|
|
|
|
// update our channel
|
|
|
|
mHttpChannel = do_QueryInterface(aNewChannel);
|
|
NS_ENSURE_STATE(mHttpChannel);
|
|
|
|
SetupHttpChannel();
|
|
// The HTTP impl already copies over the referrer info on
|
|
// redirects, so we don't need to SetupReferrerInfo().
|
|
|
|
if ((aFlags & nsIChannelEventSink::REDIRECT_PERMANENT) != 0) {
|
|
rv = NS_GetFinalChannelURI(mHttpChannel, getter_AddRefs(mSrc));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
aCallback->OnRedirectVerifyCallback(NS_OK);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EventSourceImpl::nsIInterfaceRequestor
|
|
//-----------------------------------------------------------------------------
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::GetInterface(const nsIID& aIID, void** aResult) {
|
|
AssertIsOnMainThread();
|
|
|
|
if (IsClosed()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
|
|
*aResult = static_cast<nsIChannelEventSink*>(this);
|
|
NS_ADDREF_THIS();
|
|
return NS_OK;
|
|
}
|
|
|
|
if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) ||
|
|
aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) {
|
|
nsresult rv;
|
|
nsCOMPtr<nsIPromptFactory> wwatch =
|
|
do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsPIDOMWindowOuter> window;
|
|
|
|
// To avoid a data race we may only access the event target if it lives on
|
|
// the main thread.
|
|
if (mIsMainThread) {
|
|
auto lock = mSharedData.Lock();
|
|
rv = lock->mEventSource->CheckCurrentGlobalCorrectness();
|
|
NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED);
|
|
|
|
if (nsGlobalWindowInner* win = lock->mEventSource->GetOwnerWindow()) {
|
|
window = win->GetOuterWindow();
|
|
}
|
|
}
|
|
|
|
// Get the an auth prompter for our window so that the parenting
|
|
// of the dialogs works as it should when using tabs.
|
|
|
|
return wwatch->GetPrompt(window, aIID, aResult);
|
|
}
|
|
|
|
return QueryInterface(aIID, aResult);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::IsOnCurrentThread(bool* aResult) {
|
|
*aResult = IsTargetThread();
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP_(bool)
|
|
EventSourceImpl::IsOnCurrentThreadInfallible() { return IsTargetThread(); }
|
|
|
|
nsresult EventSourceImpl::GetBaseURI(nsIURI** aBaseURI) {
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
NS_ENSURE_ARG_POINTER(aBaseURI);
|
|
|
|
*aBaseURI = nullptr;
|
|
|
|
nsCOMPtr<nsIURI> baseURI;
|
|
|
|
// first we try from document->GetBaseURI()
|
|
nsCOMPtr<Document> doc =
|
|
mIsMainThread ? GetEventSource()->GetDocumentIfCurrent() : nullptr;
|
|
if (doc) {
|
|
baseURI = doc->GetBaseURI();
|
|
}
|
|
|
|
// otherwise we get from the doc's principal
|
|
if (!baseURI) {
|
|
auto* basePrin = BasePrincipal::Cast(mPrincipal);
|
|
nsresult rv = basePrin->GetURI(getter_AddRefs(baseURI));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
NS_ENSURE_STATE(baseURI);
|
|
|
|
baseURI.forget(aBaseURI);
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::SetupHttpChannel() {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
nsresult rv = mHttpChannel->SetRequestMethod("GET"_ns);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
|
|
/* set the http request headers */
|
|
|
|
rv = mHttpChannel->SetRequestHeader(
|
|
"Accept"_ns, nsLiteralCString(TEXT_EVENT_STREAM), false);
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
|
|
// LOAD_BYPASS_CACHE already adds the Cache-Control: no-cache header
|
|
|
|
if (mLastEventID.IsEmpty()) {
|
|
return;
|
|
}
|
|
NS_ConvertUTF16toUTF8 eventId(mLastEventID);
|
|
rv = mHttpChannel->SetRequestHeader("Last-Event-ID"_ns, eventId, false);
|
|
#ifdef DEBUG
|
|
if (NS_FAILED(rv)) {
|
|
MOZ_LOG(gEventSourceLog, LogLevel::Warning,
|
|
("SetupHttpChannel. rv=%x (%s)", uint32_t(rv), eventId.get()));
|
|
}
|
|
#endif
|
|
Unused << rv;
|
|
}
|
|
|
|
nsresult EventSourceImpl::SetupReferrerInfo(
|
|
const nsCOMPtr<Document>& aDocument) {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
|
|
if (aDocument) {
|
|
auto referrerInfo = MakeRefPtr<ReferrerInfo>(*aDocument);
|
|
nsresult rv = mHttpChannel->SetReferrerInfoWithoutClone(referrerInfo);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::InitChannelAndRequestEventSource(
|
|
const bool aEventTargetAccessAllowed) {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
bool isValidScheme = mSrc->SchemeIs("http") || mSrc->SchemeIs("https");
|
|
|
|
MOZ_ASSERT_IF(mIsMainThread, aEventTargetAccessAllowed);
|
|
|
|
nsresult rv = aEventTargetAccessAllowed ? [this]() {
|
|
// We can't call GetEventSource() because we're not
|
|
// allowed to touch the refcount off the worker thread
|
|
// due to an assertion, event if it would have otherwise
|
|
// been safe.
|
|
auto lock = mSharedData.Lock();
|
|
return lock->mEventSource->CheckCurrentGlobalCorrectness();
|
|
}()
|
|
: NS_OK;
|
|
if (NS_FAILED(rv) || !isValidScheme) {
|
|
DispatchFailConnection();
|
|
return NS_ERROR_DOM_SECURITY_ERR;
|
|
}
|
|
|
|
nsCOMPtr<Document> doc;
|
|
nsSecurityFlags securityFlags =
|
|
nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
|
|
{
|
|
auto lock = mSharedData.Lock();
|
|
doc = aEventTargetAccessAllowed ? lock->mEventSource->GetDocumentIfCurrent()
|
|
: nullptr;
|
|
|
|
if (lock->mEventSource->mWithCredentials) {
|
|
securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
|
|
}
|
|
}
|
|
|
|
// The html spec requires we use fetch cache mode of "no-store". This
|
|
// maps to LOAD_BYPASS_CACHE and LOAD_INHIBIT_CACHING in necko.
|
|
nsLoadFlags loadFlags;
|
|
loadFlags = nsIRequest::LOAD_BACKGROUND | nsIRequest::LOAD_BYPASS_CACHE |
|
|
nsIRequest::INHIBIT_CACHING;
|
|
|
|
nsCOMPtr<nsIChannel> channel;
|
|
// If we have the document, use it
|
|
if (doc) {
|
|
MOZ_ASSERT(mCookieJarSettings == doc->CookieJarSettings());
|
|
|
|
nsCOMPtr<nsILoadGroup> loadGroup = doc->GetDocumentLoadGroup();
|
|
rv = NS_NewChannel(getter_AddRefs(channel), mSrc, doc, securityFlags,
|
|
nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE,
|
|
nullptr, // aPerformanceStorage
|
|
loadGroup,
|
|
nullptr, // aCallbacks
|
|
loadFlags); // aLoadFlags
|
|
} else {
|
|
// otherwise use the principal
|
|
rv = NS_NewChannel(getter_AddRefs(channel), mSrc, mPrincipal, securityFlags,
|
|
nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE,
|
|
mCookieJarSettings,
|
|
nullptr, // aPerformanceStorage
|
|
nullptr, // loadGroup
|
|
nullptr, // aCallbacks
|
|
loadFlags); // aLoadFlags
|
|
}
|
|
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mHttpChannel = do_QueryInterface(channel);
|
|
NS_ENSURE_TRUE(mHttpChannel, NS_ERROR_NO_INTERFACE);
|
|
|
|
SetupHttpChannel();
|
|
rv = SetupReferrerInfo(doc);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
#ifdef DEBUG
|
|
{
|
|
nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks;
|
|
mHttpChannel->GetNotificationCallbacks(
|
|
getter_AddRefs(notificationCallbacks));
|
|
MOZ_ASSERT(!notificationCallbacks);
|
|
}
|
|
#endif
|
|
|
|
mHttpChannel->SetNotificationCallbacks(this);
|
|
|
|
// Start reading from the channel
|
|
rv = mHttpChannel->AsyncOpen(this);
|
|
if (NS_FAILED(rv)) {
|
|
DispatchFailConnection();
|
|
return rv;
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
void EventSourceImpl::AnnounceConnection() {
|
|
AssertIsOnTargetThread();
|
|
if (ReadyState() != CONNECTING) {
|
|
NS_WARNING("Unexpected mReadyState!!!");
|
|
return;
|
|
}
|
|
|
|
{
|
|
auto lock = mSharedData.Lock();
|
|
if (lock->mServiceNotifier) {
|
|
lock->mServiceNotifier->ConnectionOpened();
|
|
}
|
|
}
|
|
|
|
// When a user agent is to announce the connection, the user agent must set
|
|
// the readyState attribute to OPEN and queue a task to fire a simple event
|
|
// named open at the EventSource object.
|
|
|
|
SetReadyState(OPEN);
|
|
|
|
nsresult rv = GetEventSource()->CheckCurrentGlobalCorrectness();
|
|
if (NS_FAILED(rv)) {
|
|
return;
|
|
}
|
|
// We can't hold the mutex while dispatching the event because the mutex is
|
|
// not reentrant, and content might call back into our code.
|
|
rv = GetEventSource()->CreateAndDispatchSimpleEvent(u"open"_ns);
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("Failed to dispatch the error event!!!");
|
|
return;
|
|
}
|
|
}
|
|
|
|
nsresult EventSourceImpl::ResetConnection() {
|
|
AssertIsOnMainThread();
|
|
if (mHttpChannel) {
|
|
mHttpChannel->Cancel(NS_ERROR_ABORT);
|
|
mHttpChannel = nullptr;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::ResetDecoder() {
|
|
AssertIsOnTargetThread();
|
|
if (mUnicodeDecoder) {
|
|
UTF_8_ENCODING->NewDecoderWithBOMRemovalInto(*mUnicodeDecoder);
|
|
}
|
|
mStatus = PARSE_STATE_OFF;
|
|
ClearFields();
|
|
}
|
|
|
|
class CallRestartConnection final : public WorkerMainThreadRunnable {
|
|
public:
|
|
explicit CallRestartConnection(RefPtr<EventSourceImpl>&& aEventSourceImpl)
|
|
: WorkerMainThreadRunnable(aEventSourceImpl->mWorkerRef->Private(),
|
|
"EventSource :: RestartConnection"_ns),
|
|
mESImpl(std::move(aEventSourceImpl)) {
|
|
MOZ_ASSERT(mESImpl);
|
|
}
|
|
|
|
bool MainThreadRun() override {
|
|
MOZ_ASSERT(mESImpl);
|
|
mESImpl->RestartConnection();
|
|
// We want to ensure the shortest possible remaining lifetime
|
|
// and not depend on the Runnable's destruction.
|
|
mESImpl = nullptr;
|
|
return true;
|
|
}
|
|
|
|
protected:
|
|
RefPtr<EventSourceImpl> mESImpl;
|
|
};
|
|
|
|
nsresult EventSourceImpl::RestartConnection() {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
nsresult rv = ResetConnection();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = SetReconnectionTimeout();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::ReestablishConnection() {
|
|
AssertIsOnTargetThread();
|
|
if (IsClosed()) {
|
|
return;
|
|
}
|
|
|
|
nsresult rv;
|
|
if (mIsMainThread) {
|
|
rv = RestartConnection();
|
|
} else {
|
|
RefPtr<CallRestartConnection> runnable = new CallRestartConnection(this);
|
|
ErrorResult result;
|
|
runnable->Dispatch(GetCurrentThreadWorkerPrivate(), Canceling, result);
|
|
MOZ_ASSERT(!result.Failed());
|
|
rv = result.StealNSResult();
|
|
}
|
|
if (NS_FAILED(rv)) {
|
|
return;
|
|
}
|
|
|
|
rv = GetEventSource()->CheckCurrentGlobalCorrectness();
|
|
if (NS_FAILED(rv)) {
|
|
return;
|
|
}
|
|
|
|
SetReadyState(CONNECTING);
|
|
ResetDecoder();
|
|
// We can't hold the mutex while dispatching the event because the mutex is
|
|
// not reentrant, and content might call back into our code.
|
|
rv = GetEventSource()->CreateAndDispatchSimpleEvent(u"error"_ns);
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("Failed to dispatch the error event!!!");
|
|
return;
|
|
}
|
|
}
|
|
|
|
nsresult EventSourceImpl::SetReconnectionTimeout() {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
// the timer will be used whenever the requests are going finished.
|
|
if (!mTimer) {
|
|
mTimer = NS_NewTimer();
|
|
NS_ENSURE_STATE(mTimer);
|
|
}
|
|
|
|
MOZ_TRY(mTimer->InitWithCallback(this, mReconnectionTime,
|
|
nsITimer::TYPE_ONE_SHOT));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::PrintErrorOnConsole(
|
|
const char* aBundleURI, const char* aError,
|
|
const nsTArray<nsString>& aFormatStrings) {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
nsCOMPtr<nsIStringBundleService> bundleService =
|
|
mozilla::components::StringBundle::Service();
|
|
NS_ENSURE_STATE(bundleService);
|
|
|
|
nsCOMPtr<nsIStringBundle> strBundle;
|
|
nsresult rv =
|
|
bundleService->CreateBundle(aBundleURI, getter_AddRefs(strBundle));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsIConsoleService> console(
|
|
do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsIScriptError> errObj(
|
|
do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// Localize the error message
|
|
nsAutoString message;
|
|
if (!aFormatStrings.IsEmpty()) {
|
|
rv = strBundle->FormatStringFromName(aError, aFormatStrings, message);
|
|
} else {
|
|
rv = strBundle->GetStringFromName(aError, message);
|
|
}
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
rv = errObj->InitWithWindowID(
|
|
message, mCallingLocation.FileName(), mCallingLocation.mLine,
|
|
mCallingLocation.mColumn, nsIScriptError::errorFlag, "Event Source",
|
|
mInnerWindowID);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// print the error message directly to the JS console
|
|
rv = console->LogMessage(errObj);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::ConsoleError() {
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
nsAutoCString targetSpec;
|
|
nsresult rv = mSrc->GetSpec(targetSpec);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
AutoTArray<nsString, 1> formatStrings;
|
|
CopyUTF8toUTF16(targetSpec, *formatStrings.AppendElement());
|
|
|
|
if (ReadyState() == CONNECTING) {
|
|
rv = PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
|
|
"connectionFailure", formatStrings);
|
|
} else {
|
|
rv = PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
|
|
"netInterrupt", formatStrings);
|
|
}
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::DispatchFailConnection() {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return;
|
|
}
|
|
nsresult rv = ConsoleError();
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("Failed to print to the console error");
|
|
}
|
|
rv = Dispatch(NewRunnableMethod("dom::EventSourceImpl::FailConnection", this,
|
|
&EventSourceImpl::FailConnection),
|
|
NS_DISPATCH_NORMAL);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
// if the worker is shutting down, the dispatching of normal WorkerRunnables
|
|
// fails.
|
|
return;
|
|
}
|
|
}
|
|
|
|
void EventSourceImpl::FailConnection() {
|
|
AssertIsOnTargetThread();
|
|
if (IsClosed()) {
|
|
return;
|
|
}
|
|
// Must change state to closed before firing event to content.
|
|
SetReadyState(CLOSED);
|
|
// When a user agent is to fail the connection, the user agent must set the
|
|
// readyState attribute to CLOSED and queue a task to fire a simple event
|
|
// named error at the EventSource object.
|
|
nsresult rv = GetEventSource()->CheckCurrentGlobalCorrectness();
|
|
if (NS_SUCCEEDED(rv)) {
|
|
// We can't hold the mutex while dispatching the event because the mutex
|
|
// is not reentrant, and content might call back into our code.
|
|
rv = GetEventSource()->CreateAndDispatchSimpleEvent(u"error"_ns);
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("Failed to dispatch the error event!!!");
|
|
}
|
|
}
|
|
// Call CloseInternal in the end of function because it may release
|
|
// EventSourceImpl.
|
|
CloseInternal();
|
|
}
|
|
|
|
NS_IMETHODIMP EventSourceImpl::Notify(nsITimer* aTimer) {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(!mHttpChannel, "the channel hasn't been cancelled!!");
|
|
|
|
if (!mFrozen) {
|
|
nsresult rv = InitChannelAndRequestEventSource(mIsMainThread);
|
|
if (NS_FAILED(rv)) {
|
|
NS_WARNING("InitChannelAndRequestEventSource() failed");
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP EventSourceImpl::GetName(nsACString& aName) {
|
|
aName.AssignLiteral("EventSourceImpl");
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::Thaw() {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed() || !mFrozen) {
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(!mHttpChannel, "the connection hasn't been closed!!!");
|
|
|
|
mFrozen = false;
|
|
nsresult rv;
|
|
if (!mGoingToDispatchAllMessages && mMessagesToDispatch.GetSize() > 0) {
|
|
nsCOMPtr<nsIRunnable> event =
|
|
NewRunnableMethod("dom::EventSourceImpl::DispatchAllMessageEvents",
|
|
this, &EventSourceImpl::DispatchAllMessageEvents);
|
|
NS_ENSURE_STATE(event);
|
|
|
|
mGoingToDispatchAllMessages = true;
|
|
|
|
rv = Dispatch(event.forget(), NS_DISPATCH_NORMAL);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
rv = InitChannelAndRequestEventSource(mIsMainThread);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::Freeze() {
|
|
AssertIsOnMainThread();
|
|
if (IsClosed() || mFrozen) {
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(!mHttpChannel, "the connection hasn't been closed!!!");
|
|
mFrozen = true;
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::DispatchCurrentMessageEvent() {
|
|
AssertIsOnTargetThread();
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
UniquePtr<Message> message(std::move(mCurrentMessage));
|
|
ClearFields();
|
|
|
|
if (!message || message->mData.IsEmpty()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// removes the trailing LF from mData
|
|
MOZ_ASSERT(message->mData.CharAt(message->mData.Length() - 1) == LF_CHAR,
|
|
"Invalid trailing character! LF was expected instead.");
|
|
message->mData.SetLength(message->mData.Length() - 1);
|
|
|
|
if (message->mEventName.IsEmpty()) {
|
|
message->mEventName.AssignLiteral("message");
|
|
}
|
|
|
|
mMessagesToDispatch.Push(message.release());
|
|
|
|
if (!mGoingToDispatchAllMessages) {
|
|
nsCOMPtr<nsIRunnable> event =
|
|
NewRunnableMethod("dom::EventSourceImpl::DispatchAllMessageEvents",
|
|
this, &EventSourceImpl::DispatchAllMessageEvents);
|
|
NS_ENSURE_STATE(event);
|
|
|
|
mGoingToDispatchAllMessages = true;
|
|
|
|
return Dispatch(event.forget(), NS_DISPATCH_NORMAL);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void EventSourceImpl::DispatchAllMessageEvents() {
|
|
AssertIsOnTargetThread();
|
|
mGoingToDispatchAllMessages = false;
|
|
|
|
if (IsClosed() || mFrozen) {
|
|
return;
|
|
}
|
|
|
|
nsresult rv;
|
|
AutoJSAPI jsapi;
|
|
{
|
|
auto lock = mSharedData.Lock();
|
|
rv = lock->mEventSource->CheckCurrentGlobalCorrectness();
|
|
if (NS_FAILED(rv)) {
|
|
return;
|
|
}
|
|
|
|
if (NS_WARN_IF(!jsapi.Init(lock->mEventSource->GetOwnerGlobal()))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
JSContext* cx = jsapi.cx();
|
|
|
|
while (mMessagesToDispatch.GetSize() > 0) {
|
|
UniquePtr<Message> message(mMessagesToDispatch.PopFront());
|
|
|
|
if (message->mLastEventID.isSome()) {
|
|
mLastEventID.Assign(message->mLastEventID.value());
|
|
}
|
|
|
|
if (message->mLastEventID.isNothing() && !mLastEventID.IsEmpty()) {
|
|
message->mLastEventID = Some(mLastEventID);
|
|
}
|
|
|
|
{
|
|
auto lock = mSharedData.Lock();
|
|
if (lock->mServiceNotifier) {
|
|
lock->mServiceNotifier->EventReceived(message->mEventName, mLastEventID,
|
|
message->mData, mReconnectionTime,
|
|
PR_Now());
|
|
}
|
|
}
|
|
|
|
// Now we can turn our string into a jsval
|
|
JS::Rooted<JS::Value> jsData(cx);
|
|
{
|
|
JSString* jsString;
|
|
jsString = JS_NewUCStringCopyN(cx, message->mData.get(),
|
|
message->mData.Length());
|
|
NS_ENSURE_TRUE_VOID(jsString);
|
|
|
|
jsData.setString(jsString);
|
|
}
|
|
|
|
// create an event that uses the MessageEvent interface,
|
|
// which does not bubble, is not cancelable, and has no default action
|
|
|
|
RefPtr<EventSource> eventSource = GetEventSource();
|
|
RefPtr<MessageEvent> event =
|
|
new MessageEvent(eventSource, nullptr, nullptr);
|
|
|
|
event->InitMessageEvent(nullptr, message->mEventName, CanBubble::eNo,
|
|
Cancelable::eNo, jsData, mOrigin, mLastEventID,
|
|
nullptr, Sequence<OwningNonNull<MessagePort>>());
|
|
event->SetTrusted(true);
|
|
|
|
// We can't hold the mutex while dispatching the event because the mutex is
|
|
// not reentrant, and content might call back into our code.
|
|
IgnoredErrorResult err;
|
|
eventSource->DispatchEvent(*event, err);
|
|
if (err.Failed()) {
|
|
NS_WARNING("Failed to dispatch the message event!!!");
|
|
return;
|
|
}
|
|
|
|
if (IsClosed() || mFrozen) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EventSourceImpl::ClearFields() {
|
|
AssertIsOnTargetThread();
|
|
mCurrentMessage = nullptr;
|
|
mLastFieldName.Truncate();
|
|
mLastFieldValue.Truncate();
|
|
}
|
|
|
|
nsresult EventSourceImpl::SetFieldAndClear() {
|
|
MOZ_ASSERT(!mIsShutDown);
|
|
AssertIsOnTargetThread();
|
|
if (mLastFieldName.IsEmpty()) {
|
|
mLastFieldValue.Truncate();
|
|
return NS_OK;
|
|
}
|
|
if (!mCurrentMessage) {
|
|
mCurrentMessage = MakeUnique<Message>();
|
|
}
|
|
char16_t first_char;
|
|
first_char = mLastFieldName.CharAt(0);
|
|
|
|
// with no case folding performed
|
|
switch (first_char) {
|
|
case char16_t('d'):
|
|
if (mLastFieldName.EqualsLiteral("data")) {
|
|
// If the field name is "data" append the field value to the data
|
|
// buffer, then append a single U+000A LINE FEED (LF) character
|
|
// to the data buffer.
|
|
mCurrentMessage->mData.Append(mLastFieldValue);
|
|
mCurrentMessage->mData.Append(LF_CHAR);
|
|
}
|
|
break;
|
|
|
|
case char16_t('e'):
|
|
if (mLastFieldName.EqualsLiteral("event")) {
|
|
mCurrentMessage->mEventName.Assign(mLastFieldValue);
|
|
}
|
|
break;
|
|
|
|
case char16_t('i'):
|
|
if (mLastFieldName.EqualsLiteral("id")) {
|
|
mCurrentMessage->mLastEventID = Some(mLastFieldValue);
|
|
}
|
|
break;
|
|
|
|
case char16_t('r'):
|
|
if (mLastFieldName.EqualsLiteral("retry")) {
|
|
uint32_t newValue = 0;
|
|
uint32_t i = 0; // we must ensure that there are only digits
|
|
bool assign = true;
|
|
for (i = 0; i < mLastFieldValue.Length(); ++i) {
|
|
if (mLastFieldValue.CharAt(i) < (char16_t)'0' ||
|
|
mLastFieldValue.CharAt(i) > (char16_t)'9') {
|
|
assign = false;
|
|
break;
|
|
}
|
|
newValue = newValue * 10 + (((uint32_t)mLastFieldValue.CharAt(i)) -
|
|
((uint32_t)((char16_t)'0')));
|
|
}
|
|
|
|
if (assign) {
|
|
if (newValue < MIN_RECONNECTION_TIME_VALUE) {
|
|
mReconnectionTime = MIN_RECONNECTION_TIME_VALUE;
|
|
} else if (newValue > MAX_RECONNECTION_TIME_VALUE) {
|
|
mReconnectionTime = MAX_RECONNECTION_TIME_VALUE;
|
|
} else {
|
|
mReconnectionTime = newValue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
mLastFieldName.Truncate();
|
|
mLastFieldValue.Truncate();
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::CheckHealthOfRequestCallback(
|
|
nsIRequest* aRequestCallback) {
|
|
// This function could be run on target thread if http channel support
|
|
// nsIThreadRetargetableRequest. otherwise, it's run on main thread.
|
|
|
|
// check if we have been closed or if the request has been canceled
|
|
// or if we have been frozen
|
|
if (IsClosed() || mFrozen || !mHttpChannel) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequestCallback);
|
|
NS_ENSURE_STATE(httpChannel);
|
|
|
|
if (httpChannel != mHttpChannel) {
|
|
NS_WARNING("wrong channel from request callback");
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult EventSourceImpl::ParseCharacter(char16_t aChr) {
|
|
AssertIsOnTargetThread();
|
|
nsresult rv;
|
|
|
|
if (IsClosed()) {
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
switch (mStatus) {
|
|
case PARSE_STATE_OFF:
|
|
NS_ERROR("Invalid state");
|
|
return NS_ERROR_FAILURE;
|
|
break;
|
|
|
|
case PARSE_STATE_BEGIN_OF_STREAM:
|
|
if (aChr == CR_CHAR) {
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
} else if (aChr == COLON_CHAR) {
|
|
mStatus = PARSE_STATE_COMMENT;
|
|
} else {
|
|
mLastFieldName += aChr;
|
|
mStatus = PARSE_STATE_FIELD_NAME;
|
|
}
|
|
break;
|
|
|
|
case PARSE_STATE_CR_CHAR:
|
|
if (aChr == CR_CHAR) {
|
|
rv = DispatchCurrentMessageEvent(); // there is an empty line (CRCR)
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
} else if (aChr == LF_CHAR) {
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
} else if (aChr == COLON_CHAR) {
|
|
mStatus = PARSE_STATE_COMMENT;
|
|
} else {
|
|
mLastFieldName += aChr;
|
|
mStatus = PARSE_STATE_FIELD_NAME;
|
|
}
|
|
|
|
break;
|
|
|
|
case PARSE_STATE_COMMENT:
|
|
if (aChr == CR_CHAR) {
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
}
|
|
|
|
break;
|
|
|
|
case PARSE_STATE_FIELD_NAME:
|
|
if (aChr == CR_CHAR) {
|
|
rv = SetFieldAndClear();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
rv = SetFieldAndClear();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
} else if (aChr == COLON_CHAR) {
|
|
mStatus = PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE;
|
|
} else {
|
|
mLastFieldName += aChr;
|
|
}
|
|
|
|
break;
|
|
|
|
case PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE:
|
|
if (aChr == CR_CHAR) {
|
|
rv = SetFieldAndClear();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
rv = SetFieldAndClear();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
} else if (aChr == SPACE_CHAR) {
|
|
mStatus = PARSE_STATE_FIELD_VALUE;
|
|
} else {
|
|
mLastFieldValue += aChr;
|
|
mStatus = PARSE_STATE_FIELD_VALUE;
|
|
}
|
|
|
|
break;
|
|
|
|
case PARSE_STATE_FIELD_VALUE:
|
|
if (aChr == CR_CHAR) {
|
|
rv = SetFieldAndClear();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
rv = SetFieldAndClear();
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
} else if (aChr != 0) {
|
|
// Avoid appending the null char to the field value.
|
|
mLastFieldValue += aChr;
|
|
} else if (mLastFieldName.EqualsLiteral("id")) {
|
|
// Ignore the whole id field if aChr is null
|
|
mStatus = PARSE_STATE_IGNORE_FIELD_VALUE;
|
|
mLastFieldValue.Truncate();
|
|
}
|
|
|
|
break;
|
|
|
|
case PARSE_STATE_IGNORE_FIELD_VALUE:
|
|
if (aChr == CR_CHAR) {
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
}
|
|
break;
|
|
|
|
case PARSE_STATE_BEGIN_OF_LINE:
|
|
if (aChr == CR_CHAR) {
|
|
rv = DispatchCurrentMessageEvent(); // there is an empty line
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_CR_CHAR;
|
|
} else if (aChr == LF_CHAR) {
|
|
rv = DispatchCurrentMessageEvent(); // there is an empty line
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
mStatus = PARSE_STATE_BEGIN_OF_LINE;
|
|
} else if (aChr == COLON_CHAR) {
|
|
mStatus = PARSE_STATE_COMMENT;
|
|
} else if (aChr != 0) {
|
|
// Avoid appending the null char to the field name.
|
|
mLastFieldName += aChr;
|
|
mStatus = PARSE_STATE_FIELD_NAME;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
namespace {
|
|
|
|
class WorkerRunnableDispatcher final : public WorkerThreadRunnable {
|
|
RefPtr<EventSourceImpl> mEventSourceImpl;
|
|
|
|
public:
|
|
WorkerRunnableDispatcher(RefPtr<EventSourceImpl>&& aImpl,
|
|
WorkerPrivate* aWorkerPrivate,
|
|
already_AddRefed<nsIRunnable> aEvent)
|
|
: WorkerThreadRunnable("WorkerRunnableDispatcher"),
|
|
mEventSourceImpl(std::move(aImpl)),
|
|
mEvent(std::move(aEvent)) {}
|
|
|
|
bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
|
|
aWorkerPrivate->AssertIsOnWorkerThread();
|
|
return !NS_FAILED(mEvent->Run());
|
|
}
|
|
|
|
void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
|
|
bool aRunResult) override {
|
|
// Ensure we drop the RefPtr on the worker thread
|
|
// and to not keep us alive longer than needed.
|
|
mEventSourceImpl = nullptr;
|
|
}
|
|
|
|
bool PreDispatch(WorkerPrivate* aWorkerPrivate) override {
|
|
// We don't call WorkerRunnable::PreDispatch because it would assert the
|
|
// wrong thing about which thread we're on. We're on whichever thread the
|
|
// channel implementation is running on (probably the main thread or
|
|
// transport thread).
|
|
return true;
|
|
}
|
|
|
|
void PostDispatch(WorkerPrivate* aWorkerPrivate,
|
|
bool aDispatchResult) override {
|
|
// We don't call WorkerRunnable::PostDispatch because it would assert the
|
|
// wrong thing about which thread we're on. We're on whichever thread the
|
|
// channel implementation is running on (probably the main thread or
|
|
// transport thread).
|
|
}
|
|
|
|
private:
|
|
nsCOMPtr<nsIRunnable> mEvent;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
bool EventSourceImpl::CreateWorkerRef(WorkerPrivate* aWorkerPrivate) {
|
|
MOZ_ASSERT(!mWorkerRef);
|
|
MOZ_ASSERT(aWorkerPrivate);
|
|
aWorkerPrivate->AssertIsOnWorkerThread();
|
|
|
|
if (mIsShutDown) {
|
|
return false;
|
|
}
|
|
|
|
RefPtr<EventSourceImpl> self = this;
|
|
RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
|
|
aWorkerPrivate, "EventSource", [self]() { self->Close(); });
|
|
|
|
if (NS_WARN_IF(!workerRef)) {
|
|
return false;
|
|
}
|
|
|
|
mWorkerRef = new ThreadSafeWorkerRef(workerRef);
|
|
return true;
|
|
}
|
|
|
|
void EventSourceImpl::ReleaseWorkerRef() {
|
|
MOZ_ASSERT(IsClosed());
|
|
MOZ_ASSERT(IsCurrentThreadRunningWorker());
|
|
mWorkerRef = nullptr;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EventSourceImpl::nsIEventTarget
|
|
//-----------------------------------------------------------------------------
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) {
|
|
nsCOMPtr<nsIRunnable> event(aEvent);
|
|
return Dispatch(event.forget(), aFlags);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::Dispatch(already_AddRefed<nsIRunnable> aEvent,
|
|
uint32_t aFlags) {
|
|
nsCOMPtr<nsIRunnable> event_ref(aEvent);
|
|
if (mIsMainThread) {
|
|
return NS_DispatchToMainThread(event_ref.forget());
|
|
}
|
|
|
|
if (mIsShutDown) {
|
|
// We want to avoid clutter about errors in our shutdown logs,
|
|
// so just report NS_OK (we have no explicit return value
|
|
// for shutdown).
|
|
return NS_OK;
|
|
}
|
|
|
|
// If the target is a worker, we have to use a custom WorkerRunnableDispatcher
|
|
// runnable.
|
|
RefPtr<WorkerRunnableDispatcher> event = new WorkerRunnableDispatcher(
|
|
this, mWorkerRef->Private(), event_ref.forget());
|
|
|
|
if (!event->Dispatch(mWorkerRef->Private())) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
|
|
uint32_t aDelayMs) {
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::RegisterShutdownTask(nsITargetShutdownTask*) {
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::UnregisterShutdownTask(nsITargetShutdownTask*) {
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EventSourceImpl::nsIThreadRetargetableStreamListener
|
|
//-----------------------------------------------------------------------------
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::CheckListenerChain() {
|
|
MOZ_ASSERT(NS_IsMainThread(), "Should be on the main thread!");
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventSourceImpl::OnDataFinished(nsresult) { return NS_OK; }
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// EventSource
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
EventSource::EventSource(nsIGlobalObject* aGlobal,
|
|
nsICookieJarSettings* aCookieJarSettings,
|
|
bool aWithCredentials)
|
|
: DOMEventTargetHelper(aGlobal),
|
|
mWithCredentials(aWithCredentials),
|
|
mIsMainThread(NS_IsMainThread()) {
|
|
MOZ_ASSERT(aGlobal);
|
|
MOZ_ASSERT(aCookieJarSettings);
|
|
mESImpl = new EventSourceImpl(this, aCookieJarSettings);
|
|
}
|
|
|
|
EventSource::~EventSource() = default;
|
|
|
|
nsresult EventSource::CreateAndDispatchSimpleEvent(const nsAString& aName) {
|
|
RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
|
|
// it doesn't bubble, and it isn't cancelable
|
|
event->InitEvent(aName, false, false);
|
|
event->SetTrusted(true);
|
|
ErrorResult rv;
|
|
DispatchEvent(*event, rv);
|
|
return rv.StealNSResult();
|
|
}
|
|
|
|
/* static */
|
|
already_AddRefed<EventSource> EventSource::Constructor(
|
|
const GlobalObject& aGlobal, const nsAString& aURL,
|
|
const EventSourceInit& aEventSourceInitDict, ErrorResult& aRv) {
|
|
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (NS_WARN_IF(!global)) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
|
|
nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(global);
|
|
if (ownerWindow) {
|
|
Document* doc = ownerWindow->GetExtantDoc();
|
|
if (NS_WARN_IF(!doc)) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
cookieJarSettings = doc->CookieJarSettings();
|
|
} else {
|
|
// Worker side.
|
|
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
|
if (!workerPrivate) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
cookieJarSettings = workerPrivate->CookieJarSettings();
|
|
}
|
|
|
|
RefPtr<EventSource> eventSource = new EventSource(
|
|
global, cookieJarSettings, aEventSourceInitDict.mWithCredentials);
|
|
|
|
if (NS_IsMainThread()) {
|
|
// Get principal from document and init EventSourceImpl
|
|
nsCOMPtr<nsIScriptObjectPrincipal> scriptPrincipal =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!scriptPrincipal) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
nsCOMPtr<nsIPrincipal> principal = scriptPrincipal->GetPrincipal();
|
|
if (!principal) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
eventSource->mESImpl->Init(global, principal, aURL, aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
eventSource->mESImpl->InitChannelAndRequestEventSource(true);
|
|
return eventSource.forget();
|
|
}
|
|
|
|
// Worker side.
|
|
{
|
|
// Scope for possible failures that need cleanup
|
|
auto guardESImpl = MakeScopeExit([&] { eventSource->mESImpl = nullptr; });
|
|
|
|
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
|
MOZ_ASSERT(workerPrivate);
|
|
|
|
eventSource->mESImpl->mInnerWindowID = workerPrivate->WindowID();
|
|
|
|
eventSource->mESImpl->Init(nullptr, workerPrivate->GetPrincipal(), aURL,
|
|
aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
// In workers we have to keep the worker alive using a WorkerRef in order
|
|
// to dispatch messages correctly.
|
|
if (!eventSource->mESImpl->CreateWorkerRef(workerPrivate)) {
|
|
// The worker is already shutting down. Let's return an already closed
|
|
// object, but marked as Connecting.
|
|
if (eventSource->mESImpl) {
|
|
// mESImpl is nulled by this call such that EventSourceImpl is
|
|
// released before returning the object, otherwise
|
|
// it will set EventSource to a CLOSED state in its DTOR..
|
|
eventSource->mESImpl->Close();
|
|
}
|
|
eventSource->mReadyState = EventSourceImpl::CONNECTING;
|
|
|
|
guardESImpl.release();
|
|
return eventSource.forget();
|
|
}
|
|
|
|
// Let's connect to the server.
|
|
RefPtr<ConnectRunnable> connectRunnable =
|
|
new ConnectRunnable(workerPrivate, eventSource->mESImpl);
|
|
connectRunnable->Dispatch(workerPrivate, Canceling, aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
// End of scope for possible failures
|
|
guardESImpl.release();
|
|
}
|
|
|
|
return eventSource.forget();
|
|
}
|
|
|
|
// nsWrapperCache
|
|
JSObject* EventSource::WrapObject(JSContext* aCx,
|
|
JS::Handle<JSObject*> aGivenProto) {
|
|
return EventSource_Binding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
void EventSource::Close() {
|
|
AssertIsOnTargetThread();
|
|
if (mESImpl) {
|
|
// Close potentially kills ourself, ensure
|
|
// to not access any members afterwards.
|
|
mESImpl->Close();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// EventSource::nsISupports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(EventSource)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(EventSource,
|
|
DOMEventTargetHelper)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(EventSource,
|
|
DOMEventTargetHelper)
|
|
if (tmp->mESImpl) {
|
|
// IsCertainlyaliveForCC will return true and cause the cycle
|
|
// collector to skip this instance when mESImpl is non-null and
|
|
// points back to ourself.
|
|
// mESImpl is initialized to be non-null in the constructor
|
|
// and should have been wiped out in our close function.
|
|
MOZ_ASSERT_UNREACHABLE("Paranoia cleanup that should never happen.");
|
|
tmp->Close();
|
|
}
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
bool EventSource::IsCertainlyAliveForCC() const {
|
|
// Until we are double linked forth and back, we want to stay alive.
|
|
if (!mESImpl) {
|
|
return false;
|
|
}
|
|
auto lock = mESImpl->mSharedData.Lock();
|
|
return lock->mEventSource == this;
|
|
}
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EventSource)
|
|
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
|
|
|
|
NS_IMPL_ADDREF_INHERITED(EventSource, DOMEventTargetHelper)
|
|
NS_IMPL_RELEASE_INHERITED(EventSource, DOMEventTargetHelper)
|
|
|
|
} // namespace mozilla::dom
|