Bug 849364 - Provide per-websocket way to enable keepalive pings. r=mcmanus
authorJason Duell <jduell.mcbugs@gmail.com>
Thu, 28 Mar 2013 15:52:16 -0700
changeset 126653 6f264e8f22cf605602bc1ac3c167dbed4efb0d8b
parent 126652 1a5385e4ee447dd05bddc802e16b57de10a01f97
child 126654 a92c968b29ae53249bb193772a9d43a4fd9f941b
push idunknown
push userunknown
push dateunknown
reviewersmcmanus
bugs849364
milestone22.0a1
Bug 849364 - Provide per-websocket way to enable keepalive pings. r=mcmanus
netwerk/protocol/websocket/BaseWebSocketChannel.cpp
netwerk/protocol/websocket/BaseWebSocketChannel.h
netwerk/protocol/websocket/PWebSocket.ipdl
netwerk/protocol/websocket/WebSocketChannel.cpp
netwerk/protocol/websocket/WebSocketChannel.h
netwerk/protocol/websocket/WebSocketChannelChild.cpp
netwerk/protocol/websocket/WebSocketChannelParent.cpp
netwerk/protocol/websocket/WebSocketChannelParent.h
netwerk/protocol/websocket/nsIWebSocketChannel.idl
--- a/netwerk/protocol/websocket/BaseWebSocketChannel.cpp
+++ b/netwerk/protocol/websocket/BaseWebSocketChannel.cpp
@@ -15,17 +15,22 @@
 #if defined(PR_LOGGING)
 PRLogModuleInfo *webSocketLog = nullptr;
 #endif
 
 namespace mozilla {
 namespace net {
 
 BaseWebSocketChannel::BaseWebSocketChannel()
-  : mEncrypted(false)
+  : mEncrypted(0)
+  , mWasOpened(0)
+  , mClientSetPingInterval(0)
+  , mClientSetPingTimeout(0)
+  , mPingInterval(0)
+  , mPingResponseTimeout(10000)
 {
 #if defined(PR_LOGGING)
   if (!webSocketLog)
     webSocketLog = PR_NewLogModule("nsWebSocket");
 #endif
 }
 
 //-----------------------------------------------------------------------------
@@ -110,16 +115,56 @@ BaseWebSocketChannel::GetProtocol(nsACSt
 NS_IMETHODIMP
 BaseWebSocketChannel::SetProtocol(const nsACString &aProtocol)
 {
   LOG(("BaseWebSocketChannel::SetProtocol() %p\n", this));
   mProtocol = aProtocol;                        /* the sub protocol */
   return NS_OK;
 }
 
+NS_IMETHODIMP
+BaseWebSocketChannel::GetPingInterval(uint32_t *aMilliSeconds)
+{
+  *aMilliSeconds = mPingInterval;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+BaseWebSocketChannel::SetPingInterval(uint32_t aMilliSeconds)
+{
+  if (mWasOpened) {
+    return NS_ERROR_IN_PROGRESS;
+  }
+
+  mPingInterval = aMilliSeconds;
+  mClientSetPingInterval = 1;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+BaseWebSocketChannel::GetPingTimeout(uint32_t *aMilliSeconds)
+{
+  *aMilliSeconds = mPingResponseTimeout;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+BaseWebSocketChannel::SetPingTimeout(uint32_t aMilliSeconds)
+{
+  if (mWasOpened) {
+    return NS_ERROR_IN_PROGRESS;
+  }
+
+  mPingResponseTimeout = aMilliSeconds;
+  mClientSetPingTimeout = 1;
+
+  return NS_OK;
+}
+
 //-----------------------------------------------------------------------------
 // BaseWebSocketChannel::nsIProtocolHandler
 //-----------------------------------------------------------------------------
 
 
 NS_IMETHODIMP
 BaseWebSocketChannel::GetScheme(nsACString &aScheme)
 {
--- a/netwerk/protocol/websocket/BaseWebSocketChannel.h
+++ b/netwerk/protocol/websocket/BaseWebSocketChannel.h
@@ -37,28 +37,39 @@ class BaseWebSocketChannel : public nsIW
   NS_IMETHOD GetURI(nsIURI **aURI);
   NS_IMETHOD GetNotificationCallbacks(nsIInterfaceRequestor **aNotificationCallbacks);
   NS_IMETHOD SetNotificationCallbacks(nsIInterfaceRequestor *aNotificationCallbacks);
   NS_IMETHOD GetLoadGroup(nsILoadGroup **aLoadGroup);
   NS_IMETHOD SetLoadGroup(nsILoadGroup *aLoadGroup);
   NS_IMETHOD GetExtensions(nsACString &aExtensions);
   NS_IMETHOD GetProtocol(nsACString &aProtocol);
   NS_IMETHOD SetProtocol(const nsACString &aProtocol);
+  NS_IMETHOD GetPingInterval(uint32_t *aMilliSeconds);
+  NS_IMETHOD SetPingInterval(uint32_t aMilliSeconds);
+  NS_IMETHOD GetPingTimeout(uint32_t *aMilliSeconds);
+  NS_IMETHOD SetPingTimeout(uint32_t aMilliSeconds);
 
  protected:
   nsCOMPtr<nsIURI>                mOriginalURI;
   nsCOMPtr<nsIURI>                mURI;
   nsCOMPtr<nsIWebSocketListener>  mListener;
   nsCOMPtr<nsISupports>           mContext;
   nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
   nsCOMPtr<nsILoadGroup>          mLoadGroup;
 
   nsCString                       mProtocol;
   nsCString                       mOrigin;
 
-  bool                            mEncrypted;
   nsCString                       mNegotiatedExtensions;
+
+  uint32_t                        mEncrypted                 : 1;
+  uint32_t                        mWasOpened                 : 1;
+  uint32_t                        mClientSetPingInterval     : 1;
+  uint32_t                        mClientSetPingTimeout      : 1;
+
+  uint32_t                        mPingInterval;         /* milliseconds */
+  uint32_t                        mPingResponseTimeout;  /* milliseconds */
 };
 
 } // namespace net
 } // namespace mozilla
 
 #endif // mozilla_net_BaseWebSocketChannel_h
--- a/netwerk/protocol/websocket/PWebSocket.ipdl
+++ b/netwerk/protocol/websocket/PWebSocket.ipdl
@@ -22,17 +22,22 @@ async protocol PWebSocket
 {
   manager PNecko;
 
 parent:
   // Forwarded methods corresponding to methods on nsIWebSocketChannel
   AsyncOpen(URIParams aURI,
             nsCString aOrigin,
             nsCString aProtocol,
-            bool aSecure);
+            bool aSecure,
+            // ping values only meaningful if client set them
+            uint32_t aPingInterval,
+            bool aClientSetPingInterval,
+            uint32_t aPingTimeout,
+            bool aClientSetPingTimeout);
   Close(uint16_t code, nsCString reason);
   SendMsg(nsCString aMsg);
   SendBinaryMsg(nsCString aMsg);
   SendBinaryStream(InputStreamParams aStream, uint32_t aLength);
 
   DeleteSelf();
 
 child:
--- a/netwerk/protocol/websocket/WebSocketChannel.cpp
+++ b/netwerk/protocol/websocket/WebSocketChannel.cpp
@@ -916,32 +916,29 @@ private:
 
 uint32_t WebSocketChannel::sSerialSeed = 0;
 
 WebSocketChannel::WebSocketChannel() :
   mPort(0),
   mCloseTimeout(20000),
   mOpenTimeout(20000),
   mConnecting(NOT_CONNECTING),
-  mPingTimeout(0),
-  mPingResponseTimeout(10000),
   mMaxConcurrentConnections(200),
   mGotUpgradeOK(0),
   mRecvdHttpUpgradeTransport(0),
   mRequestedClose(0),
   mClientClosed(0),
   mServerClosed(0),
   mStopped(0),
   mCalledOnStop(0),
   mPingOutstanding(0),
   mAllowCompression(1),
   mAutoFollowRedirects(0),
   mReleaseOnTransmit(0),
   mTCPClosed(0),
-  mWasOpened(0),
   mOpenedHttpChannel(0),
   mDataStarted(0),
   mIncrementedSessionCount(0),
   mDecrementedSessionCount(0),
   mMaxMessageSize(INT32_MAX),
   mStopOnClose(NS_OK),
   mServerCloseCode(CLOSE_ABNORMAL),
   mScriptCloseCode(0),
@@ -1148,25 +1145,21 @@ WebSocketChannel::UpdateReadBuffer(uint8
 }
 
 nsresult
 WebSocketChannel::ProcessInput(uint8_t *buffer, uint32_t count)
 {
   LOG(("WebSocketChannel::ProcessInput %p [%d %d]\n", this, count, mBuffered));
   NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "not socket thread");
 
-  // reset the ping timer
-  if (mPingTimer) {
-    // The purpose of ping/pong is to actively probe the peer so that an
-    // unreachable peer is not mistaken for a period of idleness. This
-    // implementation accepts any application level read activity as a sign of
-    // life, it does not necessarily have to be a pong.
-    mPingOutstanding = 0;
-    mPingTimer->SetDelay(mPingTimeout);
-  }
+  // The purpose of ping/pong is to actively probe the peer so that an
+  // unreachable peer is not mistaken for a period of idleness. This
+  // implementation accepts any application level read activity as a sign of
+  // life, it does not necessarily have to be a pong.
+  ResetPingTimer();
 
   uint32_t avail;
 
   if (!mBuffered) {
     // Most of the time we can process right off the stack buffer without
     // having to accumulate anything
     mFramePtr = buffer;
     avail = count;
@@ -2228,16 +2221,30 @@ WebSocketChannel::StartWebsocketData()
   sWebSocketAdmissions->OnConnected(this);
 
   LOG(("WebSocketChannel::StartWebsocketData Notifying Listener %p\n",
        mListener.get()));
 
   if (mListener)
     mListener->OnStart(mContext);
 
+  // Start keepalive ping timer, if we're using keepalive.
+  if (mPingInterval) {
+    nsresult rv;
+    mPingTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+    if (NS_FAILED(rv)) {
+      NS_WARNING("unable to create ping timer. Carrying on.");
+    } else {
+      LOG(("WebSocketChannel will generate ping after %d ms of receive silence\n",
+           mPingInterval));
+      mPingTimer->SetTarget(mSocketThread);
+      mPingTimer->InitWithCallback(this, mPingInterval, nsITimer::TYPE_ONE_SHOT);
+    }
+  }
+
   return mSocketIn->AsyncWait(this, 0, 0, mSocketThread);
 }
 
 void
 WebSocketChannel::ReportConnectionTelemetry()
 { 
   // 3 bits are used. high bit is for wss, middle bit for failed,
   // and low bit for proxy..
@@ -2502,16 +2509,17 @@ WebSocketChannel::Notify(nsITimer *timer
     CleanupConnection();
   } else {
     NS_ABORT_IF_FALSE(0, "Unknown Timer");
   }
 
   return NS_OK;
 }
 
+// nsIWebSocketChannel
 
 NS_IMETHODIMP
 WebSocketChannel::GetSecurityInfo(nsISupports **aSecurityInfo)
 {
   LOG(("WebSocketChannel::GetSecurityInfo() %p\n", this));
   NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread");
 
   if (mTransport) {
@@ -2571,22 +2579,22 @@ WebSocketChannel::AsyncOpen(nsIURI *aURI
       mCloseTimeout = clamped(intpref, 1, 1800) * 1000;
     }
     rv = prefService->GetIntPref("network.websocket.timeout.open", &intpref);
     if (NS_SUCCEEDED(rv)) {
       mOpenTimeout = clamped(intpref, 1, 1800) * 1000;
     }
     rv = prefService->GetIntPref("network.websocket.timeout.ping.request",
                                  &intpref);
-    if (NS_SUCCEEDED(rv)) {
-      mPingTimeout = clamped(intpref, 0, 86400) * 1000;
+    if (NS_SUCCEEDED(rv) && !mClientSetPingInterval) {
+      mPingInterval = clamped(intpref, 0, 86400) * 1000;
     }
     rv = prefService->GetIntPref("network.websocket.timeout.ping.response",
                                  &intpref);
-    if (NS_SUCCEEDED(rv)) {
+    if (NS_SUCCEEDED(rv) && !mClientSetPingTimeout) {
       mPingResponseTimeout = clamped(intpref, 1, 3600) * 1000;
     }
     rv = prefService->GetBoolPref("network.websocket.extensions.stream-deflate",
                                   &boolpref);
     if (NS_SUCCEEDED(rv)) {
       mAllowCompression = boolpref ? 1 : 0;
     }
     rv = prefService->GetBoolPref("network.websocket.auto-follow-http-redirects",
@@ -2612,28 +2620,16 @@ WebSocketChannel::AsyncOpen(nsIURI *aURI
          mMaxConcurrentConnections,
          sWebSocketAdmissions->SessionCount()));
 
     // WebSocket connections are expected to be long lived, so return
     // an error here instead of queueing
     return NS_ERROR_SOCKET_CREATE_FAILED;
   }
 
-  if (mPingTimeout) {
-    mPingTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
-    if (NS_FAILED(rv)) {
-      NS_WARNING("unable to create ping timer. Carrying on.");
-    } else {
-      LOG(("WebSocketChannel will generate ping after %d ms of receive silence\n",
-           mPingTimeout));
-      mPingTimer->SetTarget(mSocketThread);
-      mPingTimer->InitWithCallback(this, mPingTimeout, nsITimer::TYPE_ONE_SHOT);
-    }
-  }
-
   mOriginalURI = aURI;
   mURI = mOriginalURI;
   mOrigin = aOrigin;
 
   nsCOMPtr<nsIURI> localURI;
   nsCOMPtr<nsIChannel> localChannel;
 
   mURI->Clone(getter_AddRefs(localURI));
@@ -2796,16 +2792,18 @@ WebSocketChannel::SendMsgCommon(const ns
     aStream ? new OutboundEnqueuer(this, new OutboundMessage(aStream, aLength))
             : new OutboundEnqueuer(this,
                      new OutboundMessage(aIsBinary ? kMsgTypeBinaryString
                                                    : kMsgTypeString,
                                          new nsCString(*aMsg))),
     nsIEventTarget::DISPATCH_NORMAL);
 }
 
+// nsIHttpUpgradeListener
+
 NS_IMETHODIMP
 WebSocketChannel::OnTransportAvailable(nsISocketTransport *aTransport,
                                        nsIAsyncInputStream *aSocketIn,
                                        nsIAsyncOutputStream *aSocketOut)
 {
   if (!NS_IsMainThread()) {
     return NS_DispatchToMainThread(new CallOnTransportAvailable(this,
                                                                 aTransport,
--- a/netwerk/protocol/websocket/WebSocketChannel.h
+++ b/netwerk/protocol/websocket/WebSocketChannel.h
@@ -146,16 +146,23 @@ private:
   void ApplyMask(uint32_t mask, uint8_t *data, uint64_t len);
 
   bool     IsPersistentFramePtr();
   nsresult ProcessInput(uint8_t *buffer, uint32_t count);
   bool UpdateReadBuffer(uint8_t *buffer, uint32_t count,
                         uint32_t accumulatedFragments,
                         uint32_t *available);
 
+  inline void ResetPingTimer()
+  {
+    if (mPingTimer) {
+      mPingOutstanding = 0;
+      mPingTimer->SetDelay(mPingInterval);
+    }
+  }
 
   nsCOMPtr<nsIEventTarget>                 mSocketThread;
   nsCOMPtr<nsIHttpChannelInternal>         mChannel;
   nsCOMPtr<nsIHttpChannel>                 mHttpChannel;
   nsCOMPtr<nsICancelable>                  mDNSRequest;
   nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback;
   nsCOMPtr<nsIRandomGenerator>             mRandomGenerator;
 
@@ -174,18 +181,16 @@ private:
   uint32_t                        mCloseTimeout;  /* milliseconds */
 
   nsCOMPtr<nsITimer>              mOpenTimer;
   uint32_t                        mOpenTimeout;  /* milliseconds */
   wsConnectingState               mConnecting;   /* 0 if not connecting */
   nsCOMPtr<nsITimer>              mReconnectDelayTimer;
 
   nsCOMPtr<nsITimer>              mPingTimer;
-  uint32_t                        mPingTimeout;  /* milliseconds */
-  uint32_t                        mPingResponseTimeout;  /* milliseconds */
 
   nsCOMPtr<nsITimer>              mLingeringCloseTimer;
   const static int32_t            kLingeringCloseTimeout =   1000;
   const static int32_t            kLingeringCloseThreshold = 50;
 
   int32_t                         mMaxConcurrentConnections;
 
   uint32_t                        mGotUpgradeOK              : 1;
@@ -195,17 +200,16 @@ private:
   uint32_t                        mServerClosed              : 1;
   uint32_t                        mStopped                   : 1;
   uint32_t                        mCalledOnStop              : 1;
   uint32_t                        mPingOutstanding           : 1;
   uint32_t                        mAllowCompression          : 1;
   uint32_t                        mAutoFollowRedirects       : 1;
   uint32_t                        mReleaseOnTransmit         : 1;
   uint32_t                        mTCPClosed                 : 1;
-  uint32_t                        mWasOpened                 : 1;
   uint32_t                        mOpenedHttpChannel         : 1;
   uint32_t                        mDataStarted               : 1;
   uint32_t                        mIncrementedSessionCount   : 1;
   uint32_t                        mDecrementedSessionCount   : 1;
 
   int32_t                         mMaxMessageSize;
   nsresult                        mStopOnClose;
   uint16_t                        mServerCloseCode;
--- a/netwerk/protocol/websocket/WebSocketChannelChild.cpp
+++ b/netwerk/protocol/websocket/WebSocketChannelChild.cpp
@@ -339,24 +339,27 @@ WebSocketChannelChild::AsyncOpen(nsIURI 
   URIParams uri;
   SerializeURI(aURI, uri);
 
   // Corresponding release in DeallocPWebSocket
   AddIPDLReference();
 
   gNeckoChild->SendPWebSocketConstructor(this, tabChild,
                                          IPC::SerializedLoadContext(this));
-  if (!SendAsyncOpen(uri, nsCString(aOrigin), mProtocol, mEncrypted))
+  if (!SendAsyncOpen(uri, nsCString(aOrigin), mProtocol, mEncrypted,
+                     mPingInterval, mClientSetPingInterval,
+                     mPingResponseTimeout, mClientSetPingTimeout))
     return NS_ERROR_UNEXPECTED;
 
   mOriginalURI = aURI;
   mURI = mOriginalURI;
   mListener = aListener;
   mContext = aContext;
   mOrigin = aOrigin;
+  mWasOpened = 1;
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 WebSocketChannelChild::Close(uint16_t code, const nsACString & reason)
 {
   LOG(("WebSocketChannelChild::Close() %p\n", this));
--- a/netwerk/protocol/websocket/WebSocketChannelParent.cpp
+++ b/netwerk/protocol/websocket/WebSocketChannelParent.cpp
@@ -46,17 +46,21 @@ WebSocketChannelParent::RecvDeleteSelf()
   mAuthProvider = nullptr;
   return mIPCOpen ? Send__delete__(this) : true;
 }
 
 bool
 WebSocketChannelParent::RecvAsyncOpen(const URIParams& aURI,
                                       const nsCString& aOrigin,
                                       const nsCString& aProtocol,
-                                      const bool& aSecure)
+                                      const bool& aSecure,
+                                      const uint32_t& aPingInterval,
+                                      const bool& aClientSetPingInterval,
+                                      const uint32_t& aPingTimeout,
+                                      const bool& aClientSetPingTimeout)
 {
   LOG(("WebSocketChannelParent::RecvAsyncOpen() %p\n", this));
 
   nsresult rv;
   nsCOMPtr<nsIURI> uri;
 
   if (aSecure) {
     mChannel =
@@ -77,16 +81,24 @@ WebSocketChannelParent::RecvAsyncOpen(co
     goto fail;
 
   uri = DeserializeURI(aURI);
   if (!uri) {
     rv = NS_ERROR_FAILURE;
     goto fail;
   }
 
+  // only use ping values from child if they were overridden by client code.
+  if (aClientSetPingInterval) {
+    mChannel->SetPingInterval(aPingInterval);
+  }
+  if (aClientSetPingTimeout) {
+    mChannel->SetPingTimeout(aPingTimeout);
+  }
+
   rv = mChannel->AsyncOpen(uri, aOrigin, this, nullptr);
   if (NS_FAILED(rv))
     goto fail;
 
   return true;
 
 fail:
   mChannel = nullptr;
--- a/netwerk/protocol/websocket/WebSocketChannelParent.h
+++ b/netwerk/protocol/websocket/WebSocketChannelParent.h
@@ -33,17 +33,21 @@ class WebSocketChannelParent : public PW
   WebSocketChannelParent(nsIAuthPromptProvider* aAuthProvider,
                          nsILoadContext* aLoadContext,
                          PBOverrideStatus aOverrideStatus);
 
  private:
   bool RecvAsyncOpen(const URIParams& aURI,
                      const nsCString& aOrigin,
                      const nsCString& aProtocol,
-                     const bool& aSecure);
+                     const bool& aSecure,
+                     const uint32_t& aPingInterval,
+                     const bool& aClientSetPingInterval,
+                     const uint32_t& aPingTimeout,
+                     const bool& aClientSetPingTimeout);
   bool RecvClose(const uint16_t & code, const nsCString & reason);
   bool RecvSendMsg(const nsCString& aMsg);
   bool RecvSendBinaryMsg(const nsCString& aMsg);
   bool RecvSendBinaryStream(const InputStreamParams& aStream,
                             const uint32_t& aLength);
   bool RecvDeleteSelf();
 
   void ActorDestroy(ActorDestroyReason why);
--- a/netwerk/protocol/websocket/nsIWebSocketChannel.idl
+++ b/netwerk/protocol/websocket/nsIWebSocketChannel.idl
@@ -128,9 +128,31 @@ interface nsIWebSocketChannel : nsISuppo
 
     /** 
      * Use to send a binary stream (Blob) to Websocket peer.
      *
      * @param aStream The input stream to be sent.  
      */
     void sendBinaryStream(in nsIInputStream aStream, 
                           in unsigned long length);
+
+    /**
+     * This value determines how often (in milliseconds) websocket keepalive
+     * pings are sent.  If set to 0 (the default), no pings are ever sent.
+     *
+     * This value can currently only be set before asyncOpen is called, else 
+     * NS_ERROR_IN_PROGRESS is thrown.
+     *
+     * Be careful using this setting: ping traffic can consume lots of power and
+     * bandwidth over time.
+     */
+    attribute unsigned long pingInterval;
+
+    /**
+     * This value determines how long (in milliseconds) the websocket waits for
+     * the server to reply to a ping that has been sent.
+     *
+     * This value can currently only be set before asyncOpen is called, else 
+     * NS_ERROR_IN_PROGRESS is thrown.
+     */
+    attribute unsigned long pingTimeout;
+
 };